当通过 crateComponent 创建了组件 VNode,接下来会走到 vm._update,执行 vm._update,执行 vm.path 去把 VNode 转换成真正的 DOM 节点。但这些只是针对于一个普通的 VNode 节点,现在来看一下组件的 VNode 会有哪些不一样的地方。
-
patch 过程会调用 createElm 创建元素节点,看一下 createElm 的实现,定义在 src/core/vdom/patch.js 中:
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 } ... }
createComponent
-
这里有个关键的逻辑,这里会判断 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值,如果为 true 则直接结束,接下来就先看一下 createComponent 方法:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */, parentElm, refElm) } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
-
createComponent 函数中,首先对 vnode.data 做一些判断操作:
let i = vnode.data if (isDef(i)) { // ... if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */, parentElm, refElm) } }
-
如果 VNode 是一个组件 VNode,那么这个条件会被满足,并且得到 i 就是 init 钩子函数,之前提到过,在创建组件 VNode 的时候合并钩子函数就是包含 init 钩子函数,它定义在 src/core/vdom/create-component.js
init ( vnode: VNodeWithData, hydrating: boolean, parentElm: ?Node, refElm: ?Node ): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance, parentElm, refElm ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) }
-
init 钩子函数执行也比较简单,首先不考虑 keepAlive 的情况,它是通过 createComponentInstanceForVnode 创建一个 Vue 的实例,然后调用 $mount 方法挂载子组件,看一下 createComponentInstanceForVnode 的实现:
export function createComponentInstanceForVnode ( vnode: any, // we know it's MountedComponentVNode but flow doesn't parent: any, // activeInstance in lifecycle state parentElm?: ?Node, refElm?: ?Node ): Component { const options: InternalComponentOptions = { _isComponent: true, parent, _parentVnode: vnode, _parentElm: parentElm || null, _refElm: refElm || null } // check inline-template render functions const inlineTemplate = vnode.data.inlineTemplate if (isDef(inlineTemplate)) { options.render = inlineTemplate.render options.staticRenderFns = inlineTemplate.staticRenderFns } return new vnode.componentOptions.Ctor(options) }
createComponentInstanceForVnode 函数构造的一个内容组件的参数,然后执行 new vnode.componentOptions.Ctor(options)。其中这里的 vnode.componentOptions.Ctor 对应的就是子组件的构造函数,前面的小章节中分析了它实际上就是继承于 Vue 的一个构造器 Sub, 相当于 new Sub(options) 这里的参数需要注意一下: _ isComponent: true 表示它是一个组件,parent 表示当前激活的组件实例。
-
所以子组件的实例化实际上就是在这个时机执行的,并且它会执行实例的 _init 方法,代码在 src/core/instance/init.js 中
Vue.prototype._init = function (options?: Object) { const vm: Component = this // a 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) } // a flag to avoid this being observed vm._isVue = true // merge options 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 { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // ... if (vm.$options.el) { vm.$mount(vm.$options.el) } }
-
首先是合并 options 的过程变化,_isComponent 为 true,然后会走到 initInternalComponent 过程,先看一下这个函数:
export function initInternalComponent (vm: Component, options: InternalComponentOptions) { const opts = vm.$options = Object.create(vm.constructor.options) // doing this because it's faster than dynamic enumeration. const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode opts._parentElm = options._parentElm opts._refElm = options._refElm const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } }
-
在这个过程中,要记住几点: opts.parent = options.parent、opts._parentVnode = parentVnode, opts._parentElm = options._parentElm, opts._refElm = options._refElm 它们就是之前通过 createComponentInstanceForVnode 函数传入的几个参数合并到了内容选项 $optons 中了。
在看一下 _init 函数最后的执行的代码:if (vm.$options.el) { vm.$mount(vm.$options.el) }
-
由于组件初始化的时候是不传 el 的,因此组件是自己接管了 、$mount 的过程,这个过程的主要流程在 $mount 章节中已经介绍过了,现在看组件 init 的过程,componentVnodeHooks (代码在: src/core/vdom/create-component.js) 的 init 钩子函数,在完成实例化的 init 后,接着会执行 child.$mount(hydrating ? vnode.elm : undefined, hydrating)。这里 hydrating 为 true 一般是服务器渲染的情况,现在只考虑客户端渲染,所以这里 $mount 相当于执行了 child.$mount(undefined, false), 它最终会调用 mountComponent 方法,从而执行了 vm._render() 方法:
Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options ... // 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 { 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') { if (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 } } else { vnode = vm._vnode } } ... // set parent vnode.parent = _parentVnode return vnode }
上面这里只保留了一些部分关键的代码, _parentVnode 就是当前组件的父 VNode,而 render 函数生成的 vnode 当前组件渲染 vnode,vnode 的 parent 指向了 _parentVnode,其实也就是 vm.$vnode,他们是一种父子的关系。
-
在执行完 vm._render 生成 VNode 之后,接下来会执行 vm._update 去渲染 VNode,看一下 _update 的定义,在 src/core/instance/lifecycle.js 中:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this if (vm._isMounted) { callHook(vm, 'beforeUpdate') } const prevEl = vm.$el const prevVnode = vm._vnode const prevActiveInstance = activeInstance activeInstance = 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.$options._parentElm, vm.$options._refElm ) // no need for the ref nodes after initial patch // this prevents keeping a detached DOM tree in memory (#5851) vm.$options._parentElm = vm.$options._refElm = null } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // 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. }
_update 过程中有几个关键的地方(代码),首先是 vm._vnode = vnode 的逻辑,这个 vnode 是通过 vm._render() 返回的组件渲染 VNode,vm._vnode 和 vm.$vnode 的关系就是父子关系,用代码就是 vm.vnode.parent === vm.$vnode。
-
这段代码也是很有意思:
export let activeInstance: any = null const prevActiveInstance = activeInstance activeInstance = 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.$options._parentElm, vm.$options._refElm ) // no need for the ref nodes after initial patch // this prevents keeping a detached DOM tree in memory (#5851) vm.$options._parentElm = vm.$options._refElm = null } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance
这个 activeInstance 作用就是保持当前上下文的 Vue 实例,它是在 lifecycle 模块的全局变量,定义 export let activeInstance: any = null,并且在之前调用 createComponentInstanceForVnode 方法的时候从 lifecycle 模块获取,并且作为参数传入的。
因为 JavaScript 是一个单线程,Vue 整个初始化时一个深度便遍历的过程,咋实例化子组件的过程中,它需要知道当前上下文的 Vue 实例是什么,并把它作为子组件的父 Vue 实例。之前有提到过对组组件的实例过程会先调用 initInternalComponent(vm, options) 合并 options,把 parent 存储到 vm.$options 中,在$mount 之前会调用 initLifecycle(vm) 方法:
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
...
}
上面可以看到 vm.$parent 就是用来保留当前 vm 的父实例,并且通过 parent.$children.push(vm) 来把当前的 vm 存储到父实例的 $children 中。
在 vm._update 的过程中,把当前的 vm 赋值给 activeInstance,同时通过 const prevActiveInstance = activeInstance 用 prevActiveInstance 保留上一次的 activeInstance。实际上,prevActiveInstance 和当前的 vm 是一个父子关系,当一个 vm 实例完成它的所有子树的 patch 或者 update 过程后,activeInstance 会回到它的父实例,这个就美的保证了 crateComponentInstanceForVnode 整个深度遍历过程中,在实例化子组件的时候能传入当前子组件的父 Vue 实例,并在 _init 的过程中,通过 vm.$parent 把父子关系保留。
回到 _update,最后就是调用 patch 渲染 VNode 。
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
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, parentElm, refElm)
} else {
...
}
...
}
- 这里就又回到了本文开始的地方,之前分析过负责渲染 DOM 的函数是 createElm,注意这里只传了两个参数,所以对应的 parentElm 是 undefined。再看一下它的定义:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
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
)
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
...
} 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 是组件渲染的 vnode,也就是之前说的 vm._vnode,如果组件的根节点是普通元素,那么 vm._vnode 也是普通的 vnode,这里 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值就是 false,接下来的过程就和 我之前写过的 createComponent(Vue 创建组件) 是一样的。先创建一个父节点占位符,然后在遍历所有的子 VNode 递推调用 crateElm,在遍历过程中,如果遇到子 VNode 是一个组件的 VNode,则重复本文开始的过程,通过一个递归的方式就可以完成整个组件数的构建。
-
由于这个时候传入的 parentElm 是空,所以对组件的插入,在 createComponent 有这个一段逻辑在 patch.js 文件中:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */, parentElm, refElm) } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
在完成组件整个 parch 过程后,最后执行 nsert(parentElm, vnode.elm, refElm) 完成组件的 DOM 插入,如果组件 patch 过程中又创建了子组件,那么 DOM 的插入顺序是
先子后父
。