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 = () => {
vm._update(vm._render(), hydrating)
}
这段代码是非常重要的,所以后面我们会经常讲他。
接下来的代码是定义一个watcher实例对象,具体代码如下:
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
为什么要定义watcher呢?每一个组件都会有一个自己的独一无二的watcher实例,watcher用要标识我们的组件,什么意思呢?如果我们的组件引用了一个属性,那么这个属性的deps中就会添加这个组件的watcher,表示该组件引用了该属性。就是追踪关系。现在我们进入Watcher构造函数的内部,来解析一下它的具体代码:
//D:\vue-src-jx\vue-dev\vue-dev\src\core\observer\watcher.js
constructor (
vm: Component,
expOrFn: string | Function,//updateComponent
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.before = options.before
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
这里我们只放出了Watcher构造函数的constructor函数,因为其他的函数时辅助函数,没有必要一次性放出来。首先将vm实例对象挂载到watcher.vm上,然后判断是否是渲染watcher。在Vue中的watcher分为两种:renderWatcher/userWatcher。我的理解是renderWatcher是:属性的deps中的watcher是渲染watcher。因为在生成vdom的时候组件会去引用一些属性,而属性就会把这些组件的watcher放到它们的deps当中,也就是在渲染生成vdom的时候添加的watcher叫做渲染watcher。而userWatcher指的是当我们在vm.$watch函数中引入的属性,此时也会把组件的watcher放入到属性的deps中,只不过这种不是在渲染的时候添加的watcher是userWatcher。回到我们的代码当中,因为在执行new Watcher的时候传入的isRenderWatcher === true。所以是渲染watcher,这样就会将该watcher添加到vm._watcher上。后续的代码就是为实例的watcher添加各种属性。其中有几行需要我们注意:
this.before = options.before
这个是用来添加下面的函数的:
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
然后为每一个watcher都添加一个uid:
this.id = ++uid
虽然感觉这行代码很不起眼,但是将会在我们的组件更新上面起到很大的作用,那就是组件的更新顺序。
然后为watcher实例添加下面的属性:
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
这个是用来依赖收集的,这里我们不过多展开。然后判断expOrFn是否是一个函数,这个变量就是传入的updateComponent函数,然后就执行:
this.value = this.lazy
? undefined
: this.get()
我们来看看watcher.get()函数做了什么:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value)
}
popTarget()//
this.cleanupDeps()
}
return value
}
首先就是执行:pushTarget(this),这里的this指向的是watcher实例对象,而pushTarget函数是干什么的呢?
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
这个函数的作用是将我们的watcher实例对象放入到targetStack栈中,然后Dep.target = target。为什么这样做呢?这是很有讲究的,首先,Vue是不是正在创建根组件实例对象,后面会有生成vnode环节,那么当我们访问到一个属性的时候,是不是一定会触发它们的get函数,那么此时它就知道有组件引用了它。但是,没错我要说但是了,它不知道谁引用的,就像你蒙着眼被班里的同学亲了一下,你知道你被亲了,但是你不知道是谁亲的。那么这个属性如何判断是谁亲(呸,是谁引用)的呢?答案很简单,那就是当一个组件在渲染的时候先把这个组件的watcher给放到全局的变量Dep.target上,当有组件引用的时候,属性就看Dep.target上存的是谁的watcher,然后就将该watcher放到自己的deps口袋里,当该属性的值改变的时候就会循环遍历通知每一个watcher。此时可能还有疑问,你是怎么确定当有属性被引用的时候就一定是Dep.target上的wathcer对应的组件引用的呢?嗯,这个问题问的不错,这是一个哲学问题......,哈哈开玩笑的,原因很简单,当我们的组件在将要渲染的时候会把它对应的watcher放到Dep.target上去,那么此时是不是该组件正在占用Dep.target。好了问题就来了,现在我们的组件要渲染生成vnode了,生成的过程中一定会引用属性,你觉得哪些属性会被触发get函数呢?当然watcher对应的组件所引用的属性,该属性倒是想让其他的组件引用它呢,但是其他的组件没有渲染,怎么会引用它的,只有当前的根组件引用了它,当然是根组件引用它了。这就好比你在班里被蒙着眼,此时班里只有一个人,你觉得是谁亲的呢,答案很明显了。所以希望大家都能找到自己的另一半。好了,关于pushTarget就暂时到这里了。让我们回到watcher.get()函数当中,获取watcher所对应的组件实例,然后执行:
this.getter.call(vm, vm)
其实等价于执行:
updateComponent.call(vm,vm)
好了,到了我们的重头戏了,我们先来回顾一下该函数的定义:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
进入到该函数中,首先我们执行的是vm._render()函数,我先剧透一下,它的主要目的是生成组件对应的vnode。那它具体是怎么生成的呢?我们来看vm._render的具体代码:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options//从vm.$options中拿到render函数。
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
//vm._renderProxy在生产环境下其实就是vm。通过调用render函数来生产vnode。
//
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
我们先看最后一行代码:
return vnode
懂我意思吧[挑眉]。首先从vm.$options中解构出render/_parentVnode两个值,然后判断,因为vm实例没有_parentVnode所以不会执行该分支代码。然后执行:
vnode = render.call(vm._renderProxy, vm.$createElement)
因为vm实例对象的render函数不是我们传入的,而是通过编译模板得到的,所以render函数算是Vue内部的函数,所以不能够直接的得到,但是这并不能够影响我们接下来的讲解。
我们可以通过代码调试的方式获取到了它的内部代码:
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('span',[_v(_s(message))])])}
})
我们发现它最后调用了_c方法,那么我们就来看vm._c方法是何方神圣。
//D:\vue-src-jx\vue-dev\vue-dev\src\core\instance\render.js
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
我们从上面的代码可以看到编译后的render的模样,我们来分别讲一下a/b/c/d代表什么,按照顺序分别是:标签名,data,childrend即该节点的直属子节点,normalizationType。这里的data很有意思,为什么这么说呢,有没有发现在内部代码的时候Vue一共调用了两次_c函数,但是参数却不同,第一个传入了三个参数,而第二个传入了两个参数。所以data代表什么就不确定了,可能是该节点的属性对象,也可能是该节点的子节点数组。无论是哪种,都会执行这个操作:
//这是 createElement 函数中的代码
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
然后就执行了:
return _createElement(context, tag, data, children, normalizationType)
在向下讲之前,我们回头来看一下上面的一段代码:
_c('div',{attrs:{"id":"app"}},[_c('span',[_v(_s(message))])])
从上面的代码中我们可以看到Vue一共调用了两次_c函数,其实每调用一次会有很复杂的操作,但是为了让大家能够明白里面发生了什么,所以我两个函数的调用都会讲。根据调用顺序,首先调用的是第二个_c函数(其实本质上会先调用_s/_v这两个函数,但是这并不是我们讲的重点),我们把传入的参数一一对应:
a:'span'
b:[VNode]//这个vnode其实就是一个生成好的文本节点,因为如果再往下讲文本节点有点麻烦,所以就省略掉了
c:undefined
d:undefined
然后我们进行到_c函数的内部,其实它的内部就是调用了createElement(vm, a, b, c, d, false)。我们来看看该函数内部的具体的代码:
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
)
这里我们先展示它传入的参数,内部代码后续再展示,我们把参数一一对应一下:
context:vm
tag:'span'
data:[VNode]
children:undefined
normalizationType:undefined
alwaysNormalize:false
然后我们逐行讲解该函数内部的代码,首先是:
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
这样一通操作后,那么此时的变量就变为了:
context:vm
tag:'span'
data:undefined
children:[VNode]
normalizationType:undefined
alwaysNormalize:false
这相当于参数值后移?对的,后移。然后执行:
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
很明显该分支走不到,然后就执行:
return _createElement(context, tag, data, children, normalizationType)
我们来看看_createElement()函数,和上面一样,我们先展示参数:
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
)
然后我们将参数一一对应:
context:vm
tag:'span'
data:undefined
children:[VNode]
normalizationType:undefined
alwaysNormalize:false
然后我们来逐行的解读代码,首先执行的是:
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
因为我们的data不存在,所以走不到这个分支,然后:
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
也走不到,我们继续看:
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
走不到,我们继续看:
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
走不到,我们继续看:
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
走不到,我们继续看:
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
好了,这段代码也走不到,我们接着看:
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
这段代码也走不到,其实这段代码是将vm实例的children数组扁平化。然后继续执行:
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
debugger
vnode = createComponent(Ctor, data, context, children, tag)
console.log(vnode)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
vnode = createComponent(tag, data, context, children)
}
首先定义两个变量vnode/ns,然后判断我们的tag是否是string类型的,很明显,是的,然后执行:
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
因为我们暂时还没有生成vnode。所以是没有$vnode这个属性的,那么就会调用config.getTagNamespace。这个函数的作用很简单,它的代码如下:
function getTagNamespace (tag) {
if (isSVG(tag)) {
return 'svg'
}
// basic support for MathML
// note it doesn't support other MathML elements being component roots
if (tag === 'math') {
return 'math'
}
}
所以ns的结果是undefined。然后执行:
config.isReservedTag(tag)
这个函数判断tag是否是保留标记,就是判断该tag是否是html标签,仅此而已,我们继续看代码:
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
这个分支是走不到的,所以会执行下面的代码:
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
好了,要开始正是的创建一个vnode了。
在创建vnode之前,我们需要搞明白,我们到底创建的是谁的vnode。vue执行的是第二个_c()函数,说明创建的是子元素节点span的vnode。VNode构造函数没有什么特别的操作,其主要的目的就是创建一个对象,而这个对象里面有一些属性,那么我们来具体的看看都有哪些属性:
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
/*当前节点的标签名*/
this.tag = tag
/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.data = data
/*当前节点的子节点,是一个数组*/
this.children = children
/*当前节点的文本*/
this.text = text
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm
/*当前节点的名字空间*/
this.ns = undefined
/*编译作用域*/
this.context = context
/*函数化组件作用域*/
this.functionalContext = undefined
/*节点的key属性,被当作节点的标志,用以优化*/
this.key = data && data.key
/*组件的option选项*/
this.componentOptions = componentOptions
/*当前节点对应的组件的实例*/
this.componentInstance = undefined
/*当前节点的父节点*/
this.parent = undefined
/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false
/*静态节点标志*/
this.isStatic = false
/*是否作为跟节点插入*/
this.isRootInsert = true
/*是否为注释节点*/
this.isComment = false
/*是否为克隆节点*/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
这里只展示了一部分的代码,主要是来初始化我们的vnode实例的。我们来看看创建完成的vnode是个什么样子的:
//span 的 vnode
{
asyncFactory: undefined
asyncMeta: undefined
children: [VNode]
componentInstance: undefined
componentOptions: undefined
context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
data: undefined
elm: undefined
fnContext: undefined
fnOptions: undefined
fnScopeId: undefined
isAsyncPlaceholder: false
isCloned: false
isComment: false
isOnce: false
isRootInsert: true
isStatic: false
key: undefined
ns: undefined
parent: undefined
raw: false
tag: "span"
text: undefined
child: undefined
__proto__: Object
}
这是它展开后的,为了方便起见,我们看它没有展开的样子:
VNode {tag: "span", data: undefined, children: Array(1), text: undefined, elm: undefined, …}
我们从没有展开的vnode可以看出:首先,vnode只是一个对象,而这个对象是用来描述的span节点的。vnode就是我们span节点的对象描述形式。
好了,当vm实例生成了vnode之后,代码进行回退。回到_craeteElement函数当中来。接下来执行:
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
我们会发现,其实这段代码只会执行else if分支,但是这个分支里面的其他分支是不执行的,也就是说,这段代码并没对vnode做了什么,然后直接返回。然后回退到createElement函数,因为是最后一行代码会接着回退。然后它回退到什么时候停止呢?答案是回退到这段代码后停止:
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('span',[_v(_s(message))])])}
})
第二个_c()函数已经执行完毕了,现在开始执行第一个_c()函数了。好,我们接下来分析第一个_c()的执行过程,因为了第二个的铺垫,所以第一个函数的执行我们会进行的稍微快一些。首先依旧是进入到_c()函数的内部,然后执行如下代码:
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
在进入到createElement函数内部之前,我们先来进行参数的一一对应:
context:vm
tag:'div'
data:{attrs:{"id":"app"}}
children: VNode {tag: "span", data: undefined, children: Array(1), text: undefined, ...}
normalizationType:undefined
alwaysNormalize:false
然后走下面的代码:
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
其实上面的代码是不会执行的,第一vm的data不是数组,然后进入到isPrimitive函数中。我们来看看这个函数是干什么的:
function isPrimitive (value) {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
就是判断传入的data是否和这几个值相等,很显然不相等,所以不会执行。
然后执行:
_createElement(context, tag, data, children, normalizationType)
我们依旧来看一下参数的一一对应:
context:vm
tag:'div'
data:{attrs:{"id":"app"}}
children:VNode {tag: "span", data: undefined, children: Array(1), text: undefined, ...}
normalizationType:undefined
现在我们进入到_createElement函数中去。下面这些代码都不会执行:
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
然后开始判断tag的类型,然后就是一通判断,和我们创建span的vnode一样,他最后会走这段代码:
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
好,我们来进行参数的一一对应:
tag:'div'
data:{attrs:{"id":"app"}}
children:children:VNode {tag: "span", data: undefined, children: Array(1), text: undefined, ...}
text:undefined
elm:undefined
context:vm
接着我们就进入到VNode构造函数中了。执行的过程和span标签一样,我们这里看他最后的生成结果:
//div 的 vnode
VNode {tag: "div", data: {…}, children: Array(1), text: undefined, elm: undefined, …}
展开后:
{
asyncFactory: undefined
asyncMeta: undefined
children: [VNode]
componentInstance: undefined
componentOptions: undefined
context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
data: {attrs: {…}}
elm: undefined
fnContext: undefined
fnOptions: undefined
fnScopeId: undefined
isAsyncPlaceholder: false
isCloned: false
isComment: false
isOnce: false
isRootInsert: true
isStatic: false
key: undefined
ns: undefined
parent: undefined
raw: false
tag: "div"
text: undefined
child: (...)
__proto__: Object
}
然后回退到_createElement函数中去,在该函数中继续执行下面的代码:
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
这次和span的vnode有所不同,它会执行这段代码:
if (isDef(data)) registerDeepBindings(data)
我们进入到registerDeepBindings函数中。具体代码如下:
function registerDeepBindings (data) {
if (isObject(data.style)) {
traverse(data.style)
}
if (isObject(data.class)) {
traverse(data.class)
}
}
这个函数主要是对style/class进行了处理,所以对id这个属性是忽略的。所以继续回退。一直回退到Vue._render函数当中,因为我们在该函数中执行了:
vnode = render.call(vm._renderProxy, vm.$createElement)
所以会一直回退到这个函数当中来。
好回到vm._render函数中来,我们继续讲解后面执行的代码,当执行完:
vnode = render.call(vm._renderProxy, vm.$createElement)
后会执行:
currentRenderingInstance = null
然后执行后续代码,其实后续的代码大部分都走不到,如:
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
然后它会执行:
vnode.parent = _parentVnode;
将_parentVnode挂载到vnode.parnet。因为这个根节点的vnode不存在父节点,所以为undefined。执行完后将vnode返回。当返回之后就会回到一个函数当中,这个函数的代码如下:
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
刚在返回vnode这一操作是在vm._render()函数中执行的,这也验证了我们之前说的那句话,vm._render()函数是用来创建vnode的。接下来就是vm._update函数的执行了。我们来看内部的具体代码:
//D:\vue-src-jx\vue-dev\vue-dev\src\core\instance\lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this//这里是vm实例
const prevEl = vm.$el
const prevVnode = vm._vnode//undefined
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
//首次渲染地时候vm.$el是真实地DOM节点对象 vnode是渲染后生成地虚拟DOM
} else {
// updates 数据更新渲染
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
该函数的前半部分是对一些变量的赋值,因为我们是第一次渲染,所以prevVnode为undefined。所以会走这个分支:
if (!prevVnode) {
// initial render 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
//首次渲染地时候vm.$el是真实地DOM节点对象 vnode是渲染后生成地虚拟DOM
}
这个分支是针对首次渲染的实例对象。首次渲染会执行:
vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
我们来该函数内部具体的实现,,因为在本质上vm.__patch__的调用其实就是调用的是patch。所以我们来看patch函数的内部实现:
//D:\vue-src-jx\vue-dev\vue-dev\src\core\vdom\patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {//删除逻辑
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)//isRealElement: true
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {//不是服务端渲染,所以走不到该逻辑
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {//false
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
//该代码的意思是将我们的真实的DOM转化为VDOM.
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm //这个是真实地dom,虽然我们把真实地dom转为了虚拟dom,但是还是保留了原来地dom
const parentElm = nodeOps.parentNode(oldElm)//这是真实地父节点。
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
在解析函数代码之前,我们来对参数进行简单的讲解。vm.$el是根组件的根元素节点,也就是div。它是一个真实的节点,并不是一个vnode。第二个参数就是根组件的vnode。接下来我们进入函数内部,来看看它的具体执行。
if (isUndef(vnode)) {//删除逻辑
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
首先判断vnode是否存在,如果不存在Vue就判定你想要删除这个节点,所以这个分支我们走不到。然后向下执行:
if (isUndef(oldVnode)) {
......
}
这段代码是走不到的,因为vm实例的oldVnode是存在的。然后继续向下执行:
var isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
}
因为oldVnode是真实的节点,所以isRealElement为真,所以直接走else分支:
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {//不是服务端渲染,所以走不到该逻辑
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {//false
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
//该代码的意思是将我们的真实的DOM转化为VDOM.
oldVnode = emptyNodeAt(oldVnode)
}
上面的代码是else分支的前半段,Vue首先判断isRealElement。很明显为真,所以就向下执行,下面的这些代码都不会执行:
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {//不是服务端渲染,所以走不到该逻辑
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {//false
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
然后就执行这段代码:
oldVnode = emptyNodeAt(oldVnode)
那么此时你可能会有疑惑,if(isRealElement)到底是用来干什么的?其实这就要涉及到patch的渲染原理了。我们可以想一下,patch在什么情况下会执行。答案有两个:第一,当进行组件的首次渲染。第二,当数据更新是触发的再次渲染。也就是说有两次。在进行patch渲染的时候Vue一定会做一件事,那就是新旧vdom的比对。也就是说会将新的和旧的进行对比,然后找出渲染的最优解。但是组件在首次渲染的时候是没有旧vnode。既然没有,那么就创建一个,因为首次渲染新旧dom是一样的,为了使函数运行兼容,所以就会创建一个旧vnode出来,然后进行patch对比。
我们讲完了旧节点的创建原因,接下来继续向下执行:
const oldElm = oldVnode.elm //这个是真实地dom,虽然我们把真实地dom转为了虚拟dom,但是还是保留了原来的dom
const parentElm = nodeOps.parentNode(oldElm)//这是真实地父节点。
下面的代码执行就很关键了。因为组件将要进入到另一个阶段:生成真实节点挂载阶段。
生成真实节点挂载阶段
在生成真实节点并挂载的阶段,Vue主要执行的是createElm函数。具体代码如下:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
//nodeOps是一个原生DOM API的一个封装对象,nodeOps.createElement(tag)其实就是document.createElement(tag)。
//vnode.elm此时就是通过nodeOps.createElement()函数创建的真实节点对象。
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
//WEEX端的渲染
// in Weex, the default insertion order is parent-first.
// List items can be optimized to use children-first insertion
// with append="tree".
const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
if (!appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
createChildren(vnode, children, insertedVnodeQueue)
if (appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
} else {
//
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
这个函数主要是通过vnode来创建一个对应的真实节点。而创建一个真实节点主要经历了四个部分:
第一部分:判断该节点是否是组件节点
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
第二部分:创建vnode对应真实节点
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
第三部分:创建真实子节点
createChildren(vnode, children, insertedVnodeQueue)
在createChildren函数中又会执行createElm:
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
当调用createElm函数的时候,会把当前节点的vnode.elm作为父节点传入到createElm函数中,当创建子节点的时候,该子节点的parentElm就是我们传入的vnode.elm元素。
第四部分:将该真实节点插入到父节点中。
insert(parentElm, vnode.elm, refElm)
我们对每一个部分都进行详细的解释,然后再整体进行分析一波。第一部分是通过调用createComponent函数来判断我们传入的vnode是不是一个组件vnode。如果是一个组件vnode。那么就回去创建一个组件实例,然后执行对应的组件创建代码。很显然,在这里并不是一个组件vnode。第二部分是创建我们当前vnode所对应的节点,当创建完成后就挂载到当前节点的vnode.elm上。这个挂载是很有用的,后面insert的时候就体现出来了。也就是说在第二部的时候,我们的vnode所对应的节点就已经被创建出来了。而第三步开始创建子节点,其实子节点是是一个独立的节点,它也有自己的vnode。所以在createChildren函数中还是调用了createElm函数。第四步是进行插入操作,将我们创建好的当前节点插入到对一个的父节点当中。在插入操作的时候我们必须清楚两个基本点,那么就是我们打算插谁,把它插在哪里。首先我们来解决第一个基本点,那就是插谁,很简单插当前vnode所创建的真实节点。第二个基本点,插在哪里,插在父节点中,那么我们怎么获取到父节点呢?其实很简单。我举个例子,我们有一个模板:
<div>
<span></span>
</div>
如果我们想获取到这两个节点,那么就需要调用两次createElm函数,第一次调用时创建父节点div。第二次调用时创建子节点span。当父节点创建完之后,我们开始创建子节点了,那么当子节点也创建完之后我们如何将子节点插入到父节点div当中呢,Vue的做法就是在第二个节点创建之前,向createElm函数出入第三个参数,这个参数就是父节点的vnode.elm即刚创建的div元素节点。将这个节点作为子节点的父节点传入,当子节点创建完成后就进行插入操作,因为div节点没有父元素,所以在第一次调用createElm函数的时候就没有传入第三个参数,所以默认为undefined。
好了,上面的四个部分都讲完了,我们现在了重新梳理一遍,我们就以上面的模板为实例。那么生成的vnode如下:
//简易版本
{
tag:'div',
data:undefined,
children:[{
tag:'span',
data:undefined,
children:undefined,
......
}],
......
}
我们通过上面的那个模板将我们上面的四步走一遍。
首先是if判断,因为我们提供的是一个正常的元素节点,所以不会去创建一个组件实例,然后通过nodeOps.createElement(tag, vnode)代码,创建了一个真实的div元素节点,并把这个节点挂载到了这个div的vnode.elm上,好了我们第一个div元素创建完成。接着就开始执行createChildren(vnode, children, insertedVnodeQueue)。当执行该函数的时候,其实就是循环执行createElm这个函数。
好我们第二次进入这个函数当中。在第二次调用的时候,也就是生成span节点的时候,我们把父元素的vnode.elm作为第三个参数传入到了createElm函数中,目的是当span节点创建完成好以这个节点为父节点来进行插入操作。
首先还是if判断,依然不成立,然后执行:
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
此时的tag已经是span。所以创建的是span的真实节点。然后将创建的span节点挂载到span的vnode的elm属性上,然后就看是执行createChildren。因为我们的span节点是没有子节点的,所以执行insert操作。因为我们已经将div的vnode.elm作为parentElm传入,所以新创建的span节点就会插入到div节点当中。好了,插入完之后进行回退,回退到createChildren函数当中,因为没有其他子节点了,所以会继续回退。然后回退到第一次调用createElm函数中,然后进行div元素的insert操作,而parentElm就是div元素所对应的父元素节点。
好了,对于节点的生成和挂载的框架我们已经搭建好了,接下来我们回到我们最初的实例当中,来一一走一遍createElm函数。
我们先来回顾一下我们的实例是什么样子的,模板如下:
<div id="app">
<span>{{message}}</span>
</div>
渲染函数如下:
_c('div',{attrs:{"id":"app"}},[_c('span',[_v(_s(message))])])
现在知道了我们的模板和渲染函数了,那么我们就来走一下createElm函数。
首先依旧实判断是否该节点是一个组件节点,很显然不是组件节点。然后就定义一些变量并赋值,用于条件的判断,然后判断我们的tag是否存在,很显然是存在的,然后调用:
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
我们的第一个元素节点div就这样诞生了,然后将其赋值给div所对应的vnode的elm属性,然后执行:
createChildren(vnode, children, insertedVnodeQueue)
在createChildren函数中会循环创建子节点:
for (let i = 0; i < children.length; ++i) {
//这个就是深度遍历并创建节点。
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
在执行createElm函数去创建span节点的时候,把div真实节点作为parentElm传给该参数。同样的,这个函数执行的时候首先就会去创建span节点,然后调用createChildern函数,在调用这个函数的时候依旧会执行createElm函数去创建文本节点,此时我们会把span节点作为parentElm传入到createElm函数中,在createElm函数中,首先会创建文本节点,因为文本节点没有子节点,所以执行insert操作,因为此时的parentElm是span节点,所以就把文本插入到该节点当中,然后进行回退,回退到createChildren函数中,因为span节点没有其他的子节点,所以执行insert操作,因为此时span的父节点是我们传入的div节点,所以就把该节点插入到div节点当中。然后回退到div的createChildren函数当中,因为div节点没有其他子节点,所以执行insert操作,此时div的parentElm是body节点,所以将该节点插入到body当中,一旦插入到body。此时就说明我们更改了文档的DOM结构,会引发浏览器的重新渲染。到此,我们的生成真实节点挂载阶段就结束了,我们会发现:创建节点是由父到子,挂载节点是由子到父。
节点的创建和挂载是完成了,但是这还是有一个问题的,此时调试代码的时候你会发现页面的元素里有两个:
<div id="app">
<span>{{message}}</span>
</div>
<div id="app">
<span>123</span>
</div>
也就是说Vue完成了挂载,但是它并没有将页面的原有节点删除,所以此时Vue要做的事情就是将页面上原有的节点删除,所以会调用:
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
}
removeVnodes函数会将原有的节点删除。当执行完该函数后会执行:
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
该函数的具体代码如下:
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
因为我们这测试的案例并不会触发这个函数,到后面我们讲解嵌套组件的时候会具体讲解。
然后回退到patch函数中,然后执行:
return vnode.elm
执行完之后就会回退到Vue._update()函数当中去,后面的代码就是进行一些列的挂载,挂载完之后进行回退,一直回退到watcher.get()函数当中,因为我们是在这个函数中调用的updateComponent函数的。然后执行watcher.get中的代码,首先是判断this.deep。这个在一开始的时候是false。然后调用popTarget表示我们当前的组件已经完成了,然后将该组件的watcher出栈,一旦有属性在被访问,那么就是下一个watcher干的事情了。然后执行watcher.cleanupDeps()。为什么执行这个函数,我们会面再讲。此处先省略。接着就会继续回退,一直回退到mountComponent函数当中,然后执行:
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
因为此时的vm.$vnode还不存在,所以就会执行mounted钩子函数,在执行之前将该组件实例标记为已挂载状态。当把上面的代码执行完之后,就算是真正的执行完毕,接着就会一直回退到<script>标签当中的代码。
至此,我们的这一个简单的小案例就已经讲完了,你是到君是否能明白。
我们前面讲到了我们为什么要执行cleanDeps,该函数的具体代码如下:
cleanupDeps () {
let i = this.deps.length//获取watcher中存储的deps个数
while (i--) {
const dep = this.deps[i]//获取到对应的deps
if (!this.newDepIds.has(dep.id)) {//判断新deps中是否存在旧的deps。因为在触发重新渲染的时候会有一份新的deps。所以将新的和旧的进行比较
dep.removeSub(this)//如果新的deps中有同样的dep,说明该组件还在依赖这个deps,所以保留,如果没有,那么说明该组件已经不依赖这个deps了,所以两个解除互相绑定的关系,即删除,那么此时的this.deps中存的就是新deps用的到的dep,deps中的dep新的deps都有,但是,新的deps中的dep,deps就不一定有了。
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
//看下面的部分就可以了
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
为什么要删除旧的deps呢?因为当我们更新数据的时候会引发vue重新渲染vdom。而再渲染的过程中会再一次的访问属性,而访问属性的过程中会再一次的触发属性的get函数,进而又一次的收集依赖,但是我们以前有了旧依赖怎么办,那么我们就必须删除旧依赖然后保存新依赖。然后把新依赖放入到旧依赖中,等待下一次的更新。
在实例一中,因为我们没有进行数据的更新,所以执行该操作基本没有什么影响。