Vue实例的源码解析
接下来我将用两个例子来讲解一个vm
实例从无到有再到渲染页面的过程,可能比较复杂,望君能沉静下来。
这里只讨论了关于实例的创建以及虚拟节点的创建和挂载部分,并没有讨论生成实例后的数据处理部分。关于数据处理部分我会在后续的文章中详细介绍。
示例一:
示例一的代码如下:
<body>
<div id="app">
<span>{
{message}}</span>
</div>
<script src="./vue-dev/dist/vue.js"></script>
<script>
var vm = new Vue({
el:'#app',
data:{
message:123
}
})
</script>
</body>
这个实例很简单,没有用到嵌套组件。接下来我们来讲解一下具体的流程。每一个组件实例都会经历三个阶段(在没有经过数据更改的情况下),分别是:数据处理阶段
,生成vnode阶段
和生成真实节点挂载阶段
。
数据处理阶段:
当我们执行new Vue
时候,会执行Vue
构造函数:
function Vue (options) {
//options是我们传入的配置项
//这个就是大名鼎鼎的vue构造函数,所有的vue项目的开始的地方
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
//这里的this 其实使 vm实例,看是否用了new Vue
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
//这里的this是我们执行new Vue 时构造函数内部生成的实例对象,也可以理解为vm
this._init(options)
//从这个函数进入我们用initMixin(Vue)初始化添加的_init函数
}
给Vue
构造函数传入我们的配置项options
。此时vm
实例已经创建完成,然后调用this._init(options)
来初始化我们的数据。
vm._init()
函数位于src/core/instance/init.js
。具体代码如下:
Vue.prototype._init = function (options?: Object) {
//定义一个vm并指向this
const vm: Component = this
// a uid
//为这个vm实例添加一个唯一的uid,每一个实例都有一个唯一的标识符
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${
vm._uid}`
endTag = `vue-perf-end:${
vm._uid}`
mark(startTag)
}
//监听对象变化时用于过滤vm
// a flag to avoid this being observed
vm._isVue = true
// merge options
// _isComponent是内部创建子组件时才会添加为true的属性
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
//合并options
vm.$options = mergeOptions(
//resolveConstructorOptions函数在后面有定义
//向该函数传入的是vm.constructor也就是Vue
resolveConstructorOptions(vm.constructor),
options || {
},
vm
)
//vm.$options合并两项
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${
vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
这段代码非常好懂,首先是将我们的vm
实例上挂载uid
来标识每一个组件实例,然后添加_isVue
。表示的是Vue
组件,然后判断是否是一个组件,虽然说我们的根组件也是组件,但是它这里所说的组件其实表示的是子组件。例如你的模板中引入了一个组件。然显然我们的根实例不是子组件那种类型的,然后就会走else
分支,该分支的作用是调用mergeOptions
函数来合并Vue.options
和我们传入的options
,合并成一个总的options
然后添加到vm.$options
上。接着就是进行数据代理处理。当处理完之后会执行:
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
这些都是进行一些事件和数据的初始化的,在初始化的过程中会伴随着一些钩子函数的执行。组件的数据处理就集中在这里,我们这里不对数据处理的具体操作进行展开,因为后续会单独的讲解数据处理的部分。当vue
数据处理完成后,就进入到了下一个阶段:生成vnode阶段
生成vnode阶段:
当数据处理完之后会执行:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
因为根组件实例的$el
是存在的,所以会执行vm.$mount(vm.$options.el)
这个函数。该函数位于:./src/platforms/web/entry-runtime-with-complier.js
文件中。具体的代码如下:
Vue.prototype.$mount = function (
el?: string | Element,//我们传入的el类型有两种,一种是字符串'#app',另一种是一个元素对象,例如document.getElementById('app')
hydrating?: boolean
): Component {
el = el && query(el)//query函数主要是返回一个元素对象,如果我们传入的el存在,那么就返回该元素的对象形式,如果不存在,那么就会默认是一个div元素对象
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
//这里告诉我们el不能是body和html。原因是它会发生覆盖,这样就会将原来的模板完全覆盖掉。
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options//这里的this指向的是vm实例对象
// resolve template/el and convert to render function
if (!options.render) {
//如果我们没有render函数。那么会进入该区域代码。
let template = options.template //获取模板
if (template) {
//如果存在template配置项,
if (typeof template === 'string') {
//如果配置项的类型为字符串。
if (template.charAt(0) === '#') {
//这里我们只处理template为#xxx的格式的模板,也就是类似于template:'#app'这种
template = idToTemplate(template)//该函数返回的是template模板内部的节点的字符串形式。
/* istanbul ignore if */
//这里是template的错误处理
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${
options.template}`,
this
)
}
}
} else if (template.nodeType) {
//如果我们传入的template是一个节点对象,那么获取该节点对象中的innerHTML,然会的也是字符串形式
template = template.innerHTML
} else {
//不是以上两种格式,那么抛出错误
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
//如果template配置项不存在,那么获取el.outerHTML当作我们的template。返回的也是字符串类型
template = getOuterHTML(el)
}
if (template) {
//这是处理好的template。这种template的来源有两种,第一种是我们自己设置的,另一种就是el.outerHTML来充当template
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
//接下来的代码是进行编译,将我们的模板编译成以js描述的对象,即虚拟DOM,然后将虚拟DOM转化为render(渲染函数)。
const {
render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${
this._name} compile`, 'compile', 'compile end')
}
}
}
//如果我们没有render函数,其实通过对template进行编译,我们就会获得render函数,
//然后调用mount.call()函数。
//如果我们自己有render函数,那么我们就可以直接调用mount.call函数,不需要去进行编译。
return mount.call(this, el, hydrating)//this -> vm ; el -> 元素对象
}
该函数做的事情很多,我们来具体分析,首先是获取到我们的el
元素节点,然后判断我们的el
是否是body/html
。因为会发生覆盖问题,所以Vue
不允许我们的根节点是body/html
。然后获取vm
实例上的options.render
函数,因为我们没有定义render
函数,所以就会去判断是否有template
模板,我们也没有定义该模板,所以就会走else if
的分支:
//如果template配置项不存在,那么获取el.outerHTML当作我们的template。返回的也是字符串类型
template = getOuterHTML(el)
因为我们没有定义template
模板,所以Vue
就把我们的根节点作为了我们的template
模板(字符串形式)。此时vm
实例对象就有template
模板了,所以会执行:
const {
render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
//将生成的render函数挂载到options.render上
options.render = render
options.staticRenderFns = staticRenderFns
这段代码是干什么的呢?它的作用是将我们的tempalte
通过Vue
内置的模板编译器把它变为一个render
函数。然后将该函数挂载到vm.options.render
上面去。compileToFunctions
函数的内部比较复杂,有兴趣的话可以自己研究研究。这里提醒一下render
函数是一个函数,不是vdom
。
当这些执行完之后就会执行mount.call(this, el, hydrating)
函数,可能你会问,mount
函数是什么。这里简要说一下,Vue
官方给我们提供了两个版本,第一个版本是我们的runtime + complier
版本,另一个版本是runtimeOnly
版本,这两个版本有什么不同呢?前者是允许我们写template
的,或者说是允许我们添加el
属性的,因为该版本内置的有编译器,而后者是让我们写render
函数的,并不会进行模板编译处理,而mount
函数是后者的挂载函数,为什么前者引用了呢。原因是因为我们前者虽然没有render
函数,但是通过模板编译还是会生成,生成之后仍需要挂载,所以还是会用到mount
函数。好了我们来看该函数内部的代码实现:
// /web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined//这里之所以对再一次的对el进行判断,是因为这里的$mount是runtime-only版本的,所以对你传入的el进行判断。
return mountComponent(this, el, hydrating)
}
首先还是先获取el
元素节点,然后执行mountComponent()
函数。代码很简单,不过多介绍,我们接下来看mountComponent
函数。该函数的具体代码如下:
//./src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el//将el元素对象挂载到vm.$el上,也就是说,vm.$el是在执行$mount的时候挂载上去的。宏观的将,它是在created钩子函数之后,beforeMount钩子函数之前被挂载的。
if (!vm.$options.render) {
//这里的vm.$options.render是处理之后的render函数,也就是说,如果我们如果不传入render函数或者编译后的虚拟DOM无法生成render函数,那么vm.$options.render都为false
vm.$options.render = createEmptyVNode//如果在上述中为真,那么我们就给vm.$options.render赋值一个由空虚拟DOM组成的渲染函数。
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
//这里的警告就是你用了runtime-only,但是你写了template/el那么就会报错,它只能接收render函数。这是版本问题
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
//触发berforeMount钩子函数
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
//这和运行性能有关,暂时可以忽视
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
......
} else {
//最终定义updateComponent函数
updateComponent = () => {
//vm._update是在lifecycleMixin(Vue)中定义的
//vm._render是在renderMixin中定义的。
//hydrating:false
//该函数的执行其实是在new Watcher()中执行的,我们暂时只关注它的执行,不去关注在什么地方触发。
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
将el
节点挂载到vm
实例的$el
属性上,然后判断vm.$options.render
函数是否存在。因为vm
实例已经通过编译根节点获取到render
函数了,所以向下执行。在这里触发beforeMount
钩子函数。
接着就定义updateComponent
函数,其具体代码如下:
updateComponent = () =>