先从初始渲染看起
当创建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过程结束后,才能访问到,所以要收集起来统一执行。
大体流程图如下























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)