先从初始渲染看起
当创建Render Watcher
时会执行updateComponent
函数
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
复制代码
updateComponent
执行vm._render
函数,获取 VNode,然后执行vm._update
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el // dom 节点
// 获取老的 VNode
const prevVnode = vm._vnode
// 设置 activeInstance 并返回一个匿名函数,匿名函数返回值是上一个 activeInstance 的值
const restoreActiveInstance = setActiveInstance(vm)
// 当前 vue实例的 render 函数创建的 vnode
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// 设置 vm.$el
// 首次渲染时走这里
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 更新页面时走这里
vm.$el = vm.__patch__(prevVnode, vnode)
}
// 将 activeInstance 的值设置成上一个 vm 实例
restoreActiveInstance()
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
复制代码
vm._update
函数先将变量activeInstance
设置成当前的组件实例,然后执行vm.__patch__
函数;
vm.__patch__
函数的作用是创建、渲染、返回节点,并将vm.__patch__
的返回值赋值给vm.$el
。vm.__patch__
执行完成后将变量activeInstance
设置成上一个组件实例,如果此时是根组件则为 null
接下来就重点介绍vm.__patch__
,函数定义在/src/platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
复制代码
而patch
函数定义在src/platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
复制代码
运用函数柯里化的方式,将平台特有API在这里区分,而不是在调用时通过if...else
判断。
对于modules
其实就是平台特有的一些操作,比如:attr
、class
、style
、event
等,还有核心的 directive
和 ref
,它们会向外暴露一些特有的方法
比如 directive
,向外抛出了create
、update
、destroy
export default {
create: updateDirectives,
update: updateDirectives,
: function unbindDirectives (vnode: VNodeWithData) {
updateDirectives(vnode, emptyNode)
}
}
复制代码
createPatchFunction
接下来会执行createPatchFunction
函数,函数定义在src/core/vdom/patch.js
中,createPatchFunction
函数内部定义了很多辅助方法,这些方法就暂时不一一列举了,等后面边用边看,先看下主要逻辑
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
// 将 modules 中导出的值都放到 cbs 中
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
// ...
return function patch (oldVnode, vnode, hydrating, removeOnly) {}
}
复制代码
createPatchFunction
函数最终的返回值是patch
方法,也就是说vm.__patch__
实际执行的是这里返回的patch
方法,而在这之前会从 modules
中找到相应的方法,添加到cbs
,最后cbs
的数据结构如下
cbs = {
create: [fn1, fn2, ...],
update: [fn1, fn2, ...],
...
}
复制代码
cbs
中的这些方法在 patch 不同阶段调用,做相应的操作,比如创建指令等;cbs
初始化完成之后会调用patch
方法
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 新节点不存在,老节点存在,销毁老节点
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
// VNode是组件的渲染VNode,并且是第一次渲染
if (isUndef(oldVnode)) {
// 新节点存在,老节点不存在(首次渲染组件时会出现这种情况)
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 判断 oldVnode 是否为真实节点
const isRealElement = isDef(oldVnode.nodeType)
// oldVnode 不是真实元素并且 oldVnode 和 VNode 是同一个节点,则执行 patchVnode
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// oldVnode 是真实节点
if (isRealElement) {
// 根据 oldVnode(此时 oldVnode 是真实节点) 创建一个 VNode
oldVnode = emptyNodeAt(oldVnode)
}
// 获取节点的 真实元素
const oldElm = oldVnode.elm
// 获取 oldVnode 的 父节点
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 更新 组件vnode 的 elm 并重新执行父组件的 cbs.create 和 insert hooks(不包含 mounted 钩子)
if (isDef(vnode.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
}
复制代码
首先 patch
函数会判断新节点是否存在,如果新节点不存在,老节点存在,调用 destroy
,销毁老节点;反之,判断是否存在老节点:
- 如果不存在则调用
createElm
方法去创建并插入节点,比如组件的 渲染VNode 初次渲染时,会走这个逻辑。 - 如果存在,判断老节点是否为真实节点;
- 如果不是真实节点并且新旧节点相同,说明是更新过程,调用
patchVnode
函数; - 如果上述判断没有成立,继续判断老节点是不是真实节点,如果是,根据
oldVnode
创建一个VNode对象并赋值给oldVnode
。然后获取oldVnode
的真实节点和父节点,如果oldVnode
为null
,则获取的真实节点和父节点都为null
;接下来会调用createElm
方法去创建并插入节点,这里的createElm
方法会传入父节点和oldVnode
的相邻节点;比如根实例设置了el
属性、或者更新过程中新旧节点不同都会走这个逻辑。等节点创建插入完成之后 删除老节点
- 如果不是真实节点并且新旧节点相同,说明是更新过程,调用
createElm
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// 获取 data 对象
const data = vnode.data
// 获取 children
const children = vnode.children
// 节点标签
const tag = vnode.tag
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
// ...
}
// 创建 dom 节点
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
if (__WEEX__) {
// ...
} else {
// 递归创建所有子节点,生成整颗 dom 树
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
} 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)
}
}
复制代码
createElm
基于 VNode 创建整棵 DOM 树,并插入到父节点上。
createElm
中会先调用createComponent
方法,判断传入VNode的类型,如果传入的是组件占位符VNode,则执行创建组件的逻辑,返回true
;如果传入的是普通VNode,返回false
,继续执行createElm
这里分别说一下普通标签和组件标签的 patch 过程
普通标签
首先 createElm
会调用createComponent
方法,因为是一个普通VNode,返回false
。接下来获取data
、tag
、children
;然后判断tag
是否为空:
- 如果
tag
为空,并且vnode.isComment
为true
,说明是一个注释VNode,创建注释节点,插入到父节点中 - 如果
tag
为空,并且vnode.isComment
为false
,说明是一个文本VNode,创建文本节点,插入到父节点中 - 如果
tag
不为空,调用nodeOps.createElement
创建节点,并赋值给vnode.elm
,然后调用createChildren
创建子节点,将子节点插入到vnode.elm
;子节点插入完成后将vnode.elm
插入到父节点
createChildren
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
// 检测这组节点的 key 是否重复
checkDuplicateKeys(children)
}
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
// 说明是文本节点,创建文本节点,并插入父节点
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
复制代码
如果children
是一个数组, 遍历children
,调用createElm
依次创建这些节点并插入父节点中。如果不是一个数组,并且是一个文本VNode的话,创建一个文本节点并插入到父节点
组件标签创建过程
如果当前的VNode是组件占位符VNode,在 createElm
中调用createComponent
方法,会执行创建组件的过程,返回true
先看下createComponent
方法
createComponent
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// 判断组件实例是否已经存在, 并且组件被 keep-alive 包裹
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 执行 组件的 init 钩子函数
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
// 将 渲染vnode 的 $el 属性赋值给 组件vnode 的 elm 属性
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
复制代码
在创建组件占位符VNode时,给vnode.data
添加了几个钩子函数,其中就包含init
钩子,所以createComponent
会先判断vnode.data
上有没有init
钩子函数如果有则执行这个钩子函数,看下init
的代码
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// ...
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},
insert (vnode: MountedComponentVNode) {},
destroy (vnode: MountedComponentVNode) {}
}
复制代码
init
方法的if
是针对于keep-alive
的,这个后期会说,现在就看else
的逻辑就行,else
里面会执行createComponentInstanceForVnode
方法,并传入vnode
、activeInstance
(当前vm实例)。
export function createComponentInstanceForVnode (
vnode: any,
parent: any, // activeInstance in lifecycle state
): Component {
// 给组件vue实例的 options 添加 _isComponent、_parentVnode、parent 属性
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
复制代码
createComponentInstanceForVnode
方法首先会定义一个options
对象,并添加属性
_isComponent
:是否为组件_parentVnode
:组件的占位符VNodeparent
:父组件实例
然后执行return new vnode.componentOptions.Ctor(options)
,在创建组件占位符VNode时,通过Vue.extend
方法创建了一个子组件的构造函数,并将他存到了vnode.componentOptions.Ctor
里面,所以这里会为子组件创建Vue
实例,并返回这个实例。也就是说他会执行一系列的初始化操作等
回到init
方法,将createComponentInstanceForVnode
方法返回的Vue
实例赋值给vnode.componentInstance
,也就是说 组件占位符VNode 的componentInstance
属性指向子组件实例,创建了子组件实例后会执行child.$mount(hydrating ? vnode.elm : undefined, hydrating)
,调用 $mount
方法挂载子组件,在这个过程中会创建子组件的Render Watcher
然后执行子组件的render
函数创建组件的渲染VNode,并做依赖收集,接着执行_update
函数、__patch__
函数,就和上面的流程一样了。
当子组件的挂载过程执行完成后,子组件Vue
实例的$el
属性就是子组件的DOM树;回到createComponent
方法,执行initComponent
函数,initComponent
函数内会执行vnode.elm = vnode.componentInstance.$el
,将子组件的DOM树赋值给组件占位符VNode 的elm
属性,然后执行insert(parentElm, vnode.elm, refElm)
,将DOM树插入父元素中,并返回true
,接着回到createElm
中,因为createComponent
返回true
,所以不会继续向下执行。initComponent
方法不是只有这一点逻辑,还会执行cbs
中的钩子函数,这些逻辑在其他章节会说。
总结
当组件创建Render Watcher
时,执行render
函数获取组件的渲染VNode;然后执行_update
函数,_update
函数内会执行patch
函数创建节点并插入到DOM中;如果组件中有子组件,会调用组件占位符VNode的init
钩子函数,给子组件创建Vue
实例,并执行子组件实例的$mount
方法,会对子组件执行上述流程;等子组件执行完成之后将子组件的DOM树挂载到组件占位符VNode的elm
上,并将其插入到父元素中
mounted生命周期是如何执行的
假设有2个组件分别是,根组件、A
组件、B
组件,他们的关系是 根组件是A
的父组件,A
是B
的父组件
在执行根组件的 patch 过程中,会执行A
组件的$mount
方法,进而根组件patch过程停滞,先执行A
的patch过程,B
组件也是这个流程,A
组件的patch过程停滞,执行B
的patch过程,执行patch
函数中有这样一段逻辑
return function patch (oldVnode, vnode, hydrating, removeOnly) {
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
复制代码
会创建一个isInitialPatch
变量和一个insertedVnodeQueue
数组,根据组件的渲染VNode创建DOM树时,传入的oldVnode
为null
,会将isInitialPatch
设置成true
,渲染VNode的DOM树创建完成后,会执行invokeInsertHook
函数
function invokeInsertHook (vnode, queue, initial) {
if (isTrue(initial) && isDef(vnode.parent)) {
// 每个组件的渲染 vnode 才会执行,vnode.parent 指向 组件vnode
vnode.parent.data.pendingInsert = queue
} else {
// 执行 insertedVnodeQueue 中所有 vnode 的 insert hook
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
复制代码
invokeInsertHook
中会将insertedVnodeQueue
添加到vnode.parent.data.pendingInsert
里面,也就是组件占位符VNode的data.pendingInsert
中。此时这个insertedVnodeQueue
为空数组,因为B
组件中没有子组件;到此B
组件的DOM树已经创建完成,回到createComponent
方法,继续执行A
组件的patch过程。
createComponent
函数会执行initComponent
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
// 将 渲染vnode 的 $el 属性赋值给 组件vnode 的 elm 属性
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
registerRef(vnode)
insertedVnodeQueue.push(vnode)
}
}
复制代码
此时的vnode
参数是B
组件的占位符VNode,将vnode.data.pendingInsert
添加到insertedVnodeQueue
里面;然后就是给B
组件的占位符VNode设置elm
属性;判断B
组件是不是一个空组件,不管是不是都会执行insertedVnodeQueue.push(vnode)
;也就是说将B
组件的占位符VNode添加到insertedVnodeQueue
里面。执行到patch
函数时,这里和B
组件一样isInitialPatch
也是true
,所以执行invokeInsertHook
时,会将insertedVnodeQueue
赋值给A
组件占位符VNode的data.pendingInsert
。
A
组件的patch过程结束,回到根组件的patch过程中,继续执行createComponent
,createComponent
内执行initComponent
;将A
组件占位符VNode的data.pendingInsert
添加到insertedVnodeQueue
里面;此时insertedVnodeQueue
有两个元素分别是B
组件的占位符VNode和A
组件的占位符VNode;当根组件的patch
函数执行invokeInsertHook
时,就会走else
逻辑,因为根组件的patch
函数传入的oldVnode
是有值的,所以isInitialPatch
为false
,此时invokeInsertHook
会执行insertedVnodeQueue
中所有VNode的insert
钩子函数;
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {}
},
destroy (vnode: MountedComponentVNode) {}
}
复制代码
在创建组件的占位符VNode时,会挂载一个insert
钩子函数。这个钩子函数会给组件实例设置_isMounted
为true
,代表已经挂载完成,然后执行组件的mounted
生命周期
当根组件的patch过程结束后,Render Watcher
也创建完成了,会回到mountComponent
方法,mountComponent
方法中有这样的逻辑
if (vm.$vnode == null) {
// 表示此组件已经挂载完成
vm._isMounted = true
// 根组件的 mounted 函数 先子后父
callHook(vm, 'mounted')
}
复制代码
根组件实例的$vnode
指向null
,会执行根组件的mounted
生命周期函数,并将根组件实例的_isMounted
属性设置为true
也就是说在整个patch过程中,会将组件占位符VNode收集起来,等patch过程结束时,执行所有VNode的insert
钩子函数,顺序是先子后父,其实这里不光会收集组件占位符VNode,还会收集有指令的VNode,前提是指令绑定了insert
钩子。
如果每次在组件patch结束后执行当前组件的insert
钩子,由于还没有将组件的DOM树渲染到页面上,所以访问不到DOM节点;只有整个patch过程结束后,才能访问到,所以要收集起来统一执行。
大体流程图如下