只有特别努力,才能看起来毫不费力!
前言
这是一个Vue源码系列文章,建议从第一篇文章 Vue源码系列(一):Vue源码解读的正确姿势 开始阅读。 文章是我个人学习源码的一个历程,这边分享出来希望对大家有所帮助。
文章持续更新中,期望点赞加关注 🙏
查找Vue构造函数
在上一篇文章 (Vue源码系列(一):Vue源码解读的正确姿势) 中已经写了一个测试案例,并且关联到了源码文件。
接下来将说两种找到vue构造函数的方法:
debugger 查找
debugger查找这种方法比较简单,但很不程序员,不用一步一步的去查找,逻辑不够严谨。
具体如何debugger查找,从上一篇文章中的断点(debugger)开始说起。如图就是执行到断点的代码
接下来为了照顾不会打断点的同学,说一下上图中,右上角的几个断点调试涉及到的图标的作用
- 图标1:resume script execution - 恢复脚本执行,也就是跳过断点(debugger)继续执行脚本的意思
- 图标2:step over next function call - 遇到函数,会直接执行完这个函数,进入下一步,不显示具体执行函数的细节。
- 图标3:step into next function call - 遇到函数,不会直接运行完这个函数,而是进入函数内部,一步一步地执行,显示具体执行函数的细节。
- 图标4:step out of current function - 利用图标3进入函数内部,我们可以通过此图标功能来执行完函数内部剩下的代码。
- 图标5:step - 一步一步的来且遇到函数进入,可理解为图标2 和 图标3 的结合
- 图标6:deactivate breakpoints - 停用断点,就是debugger不起作用了😉
- 图标7:don’t pause on exceptions - 不暂停异常,就是取消谷歌浏览器的异常捕获,需要勾选☑️ Pasue on caught exceptions
如图点击两次图标5我们就找到了Vue的构造函数。
那么如何找到图中 Vue的构造函数 在源码中的位置呢?
只需要右键选择 Reveal in sidebar (在侧边栏中显示) 就可以看到源码中 Vue的构造函数 的位置了。
如图源码位置在:vue —> src —> core —> instance —> index.js 中。
...
function Vue (options) {
// 在非生产环境(开发环境)下, 如果不是使用new来调用这个构造器则会报警告 忽略
// if (process.env.NODE_ENV !== 'production' &&
// !(this instanceof Vue)
// ) {
// warn('Vue is a constructor and should be called with the `new` keyword')
// }
this._init(options)
}
...
然后继续点击两次图标5,再右键选择 Reveal in sidebar (在侧边栏中显示) 就可以看到源码中 this._init(options) 被定义的位置了。
这种方法轻松简单就能找到我们的Vue构造函数。
从打包的入口文件开始层层查找
首先先看一下咱们运行项目的命令相关代码,找一下打包的入口文件:
// package.json
"scripts": {
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap"
......
}
从代码可以看出运行 npm run dev 是的一些信息,打包使用的是:rollup, 配置文件走的是:scripts/config.js,环境变量中 TARGET中的值为:web-full-dev
那么接下来找到 config.js 中的 web-full-dev,我们就能找到打包的入口文件:
做一下代码分析:
// 运行时+编译器的一个开发版本(代码比较大,信息比较全,适合学习)
...
'web-full-dev': {
// 入口文件
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
...
针对上面的resolve方法也做一下解析
const aliases = require('./alias')
// 整个resolve方法底层依赖的是path.resolve()方法
const resolve = p => {
const base = p.split('/')[0] // 根据上面的传参"/"前面的就是"web" 所以 base = web
if (aliases[base]) {
// 映射的 "aliases[base]" 需要到 "require('./alias')" 中查看,具体映射的地址是:"src/platforms/web"
// "p.slice(base.length + 1)"其实就是上面的传参 "/" 后面的内容
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
return path.resolve(__dirname, '../', p)
}
}
因此真正的打包入口地址就是:src/platforms/web/entry-runtime-with-compiler.js
接着继续分析一下打包的入口文件: 首先说明一下,文件分析以注释为主,且所有性能监控的代码都不做考虑 istanbul ignore if,所有的提示文案( 如:warn() )直接删除,不做解释。
// src/platforms/web/entry-runtime-with-compiler.js
import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'
// 文件本身没有定义 Vue, 而是依赖./runtime/index的Vue
import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'
// 获取宿主元素的方法
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
// 扩展 $mount 方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
// 获取选项 $options
const options = this.$options
// resolve template/el and convert to render function
// 如果 render 选项不存在
if (!options.render) {
// 则查找 template
let template = options.template
// 如果 template 存在,
if (template) {
// 则判断一下 template 的写法
if (typeof template === 'string') { // 如果是字符串模板 例如:"<div> template </div>"
if (template.charAt(0) === '#') { // 如果是宿主元素的选择器,例如:"#app"
// 则调用上面的 idToTemplate() 方法查找
template = idToTemplate(template)
}
// 如果是一个dom元素
} else if (template.nodeType) {
// 则使用它的 innerHTML
template = template.innerHTML
} else {
return this
}
// 如果设置了 el
} else if (el) {
// 则以 el 的 outerHTML 作为 template
template = getOuterHTML(el)
}
// 如果存在 template 选项,则编译它获取 render 函数
if (template) {
// 编译的过程:把 template 变为 render 函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
// 最终获得的 render 函数将赋值给 选项 options
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 执行默认的挂载
return mount.call(this, el, hydrating)
}
/**
* 总结一下:
* new Vue({
* el: "#app",
* template: "<div> template </div>",
* template: "#app",
* render(h){ return h("div", "render")},
* data: {}
* })
* 在用户同时设置了 el、template、render的时候,优先级的判断为:render > template > el
*/
// 获取 outerHTML 的方法
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
Vue.compile = compileToFunctions
export default Vue
上面 entry-runtime-with-compiler.js 文件中的Vue来自于 './runtime/index',那我们自己分析 './runtime/index' 文件
// 能看到 Vue也不是在这里定义的,一样是导入的,那么这个文件主要做了什么呢?
import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'
import {
query,
mustUseProp,
isReservedTag,
isReservedAttr,
getTagNamespace,
isUnknownElement
} from 'web/util/index'
import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'
// install platform specific utils
// 这是一些配置,不重要,直接跳过
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// install platform runtime directives & components
// 这是一些扩展,不重要,直接跳过
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// install platform patch function
// 安装了一个 patch 函数,也可以叫补丁函数或者更新函数。主要的作用就是把:虚拟dom 转化为真实的dom(vdom => dom)
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
// 实现了 $mount 方法:其实就只调用了一个mountComponent()方法
// $mount的最终目的就是:把虚拟dom 转化为真实的dom,并且追加到宿主元素中去(vdom => dom => append)
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
继续查看文件中 Vue的依赖文件 "core/index"
// src/core/index.js
// 同样的 Vue 不是在文件中定义的,一样是导入的,同样的也看看这个文件主要做了什么呢?
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
// 整个文件其实主要就做了一件事:初始化全局的静态API:Vue.use/set/component/delete......等方法
initGlobalAPI(Vue)
...
继续查看文件中 Vue的依赖文件 "./instance/index"
// src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
// 没有 Vue 的导入,所以可以判定这个文件就是对 Vue的定义
// Vue构造函数的声明
function Vue (options) {
// 初始化方法,
this._init(options)
}
// 从文件中可以看出 上面的 _init() 是从下面的混入中获得的,那么具体从哪个中得到的咱们分析一下
// 初始化混入
initMixin(Vue)
// state的混入
stateMixin(Vue)
// events的混入
eventsMixin(Vue)
// 生命周期的混入
lifecycleMixin(Vue)
// 渲染函数的混入
renderMixin(Vue)
// 上面的这些混入其实就是初始化实例的方法和属性
// 其实通过名字咱们就不难发现, _init() 方法肯定是在 初始化的混入中:initMixin()
export default Vue
其实通过名字咱们就不难发现,_init() 方法肯定是在 初始化的混入中:initMixin(), 那就继续看 initMixin() 所在的文件。
源码分析
如下代码中 Vue.prototype._init 方法,也就是今天我们聊的主题:Vue初始化都做了什么?所要说的东西。
// src/core/instance/init.js
...
export function initMixin (Vue: Class<Component>) {
// 接受用户传进来的选项:options
Vue.prototype._init = function (options?: Object) {
// vue的实例
const vm: Component = this
// vue实例标识符 _uid标志
vm._uid = uid++
// vue标志, 避免被 Observe 观察
vm._isVue = true
// 选项合并:用户选项和系统默认的选项需要合并
// 处理组件的配置内容,将传入的options与构造函数本身的options进行合并(插件的策略都是默认配置和传入配置进行合并)
if (options && options._isComponent) {
// 子组件:优化内部组件(子组件)实例化,且动态的options合并相当慢,这里只有需要处理一些特殊的参数属性。减少原型链的动态查找,提高执行效率
initInternalComponent(vm, options)
} else {
// 根组件: 将全局配置选项合并到根组件的配置上,其实就是一个选项合并
vm.$options = mergeOptions(
// 获取当前构造函数的基本options
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// expose real self
vm._self = vm
// 下面的方法才是整个初始化最重要的核心代码
initLifecycle(vm) // 初始化vue实例生命周期相关的属性,组件关系属性的初始化,定义了比如:$root、$parent、$children、$refs
initEvents(vm) // 初始化自定义组件事件的监听,若存在父监听事件,则添加到该实例上
initRender(vm) // 初始化render渲染所需的slots、渲染函数等。其实就两件事1、插槽的处理、2、$createElm 也就是 render 函数中的 h 的声明
callHook(vm, 'beforeCreate') // 调用生命周期的钩子函数,在这里就能看出一个组件在创建之前和之后分别做了哪些初始化
// provide/inject 隔代传参
// provide:在祖辈中可以直接提供一个数据
// inject:在后代中可以通过inject注入后直接使用
initInjections(vm) // 隔代传参时 先inject。作为一个组件,在要给后辈组件提供数据之前,需要先把祖辈传下来的数据注入进来
initState(vm) // 对props,methods,data,computed,watch进行初始化,包括响应式的处理
initProvide(vm) // 在把祖辈传下来的数据注入进来以后 再provide
// 总而言之,上面的三个初始化其实就是:对组件的数据和状态的初始化
callHook(vm, 'created') // created 初始化完成,可以执行挂载了
// 例绑定到对应DOM元素上
// 组件构造函数设置了el选项,会自动挂载,所以就不用再$mount去挂载
// 组件构造函数没有设置el的, $mount过程不在这里
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
...
Vue初始化都做了什么?
到此整个Vue初始化就完成了,具体的细化代码之后的文章会依依总结。
下面将对 Vue初始化都做了什么,也就是本篇文章做一个总结:
1、选项合并,处理组件的配置内容,将传入的options与构造函数本身的options进行合并(用户选项和系统默认的选项进行合并)
2、初始化vue实例生命周期相关的属性,定义了比如:parent、refs
3、初始化自定义组件事件的监听,若存在父监听事件,则添加到该实例上
4、初始化render渲染所需的slots、渲染函数等。其实就两件事:插槽的处理 和 $createElm的声明,也就是 render 函数中的 h 的声明
5、调用 beforeCreate 钩子函数,在这里就能看出一个组件在创建前和后分别做了哪些初始化
6、初始化注入数据,隔代传参时 先inject。作为一个组件,在要给后辈组件提供数据之前,需要先把祖辈传下来的数据注入进来
7、对props,methods,data,computed,watch进行初始化,包括响应式的处理
8、在把祖辈传下来的数据注入进来以后 再初始化provide
9、调用 created 钩子函数,初始化完成,可以执行挂载了
10、挂载到对应DOM元素上。如果组件构造函数设置了el选项,会自动挂载,所以就不用再手动调用$mount去挂载
结语
到此整个初始化过程就结束了,下一章将是 响应式原理的解析
文章完成再把链接贴上来,希望大家多多支持,欢迎点赞加关注🙏