前言
各位道友大家好,我是LSF,在上一篇博文 中,分析了Vue初始化的整体流程,最后到了 update 动态创建 DOM 阶段。接下来这篇博文,会对这个流程进行分析,重点需要掌握 createElm 函数的执行逻辑。
一、_update 如何判断是初始化还是更新操作?
_update 是在Vue实例化之前,通过prototype混入的一个实例方法。主要目的是将vnode转化成真实DOM,它定义在 core/instance/lifecycle.js 文件中。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this // vm -> this
const prevEl = vm.$el
// 保存上一个vnode。
const prevVnode = vm._vnode
// 设置 activeInstance 当前活动的vm,返回方法。
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode // 赋值 _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 */)
} else {
// updates 更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
// activeInstance 恢复到当前的vm
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 是否为 null 来判断的是否是初始化 patch。由于是初始化操作,开始的时候 vm._vnode 没有被赋值成 vnode,从而 vm._vnode 为 null。所以代码的执行逻辑会走到初始化 patch。
二、patch
2.1 patch 定义
web端的 Vue.prototype.__patch__
方法,它定义的入口在 src/platforms/web/runtime/index.js 文件中。
import { patch } from './patch'
...
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// install platform patch function
// 安装web端的 patch 方法。
Vue.prototype.__patch__ = inBrowser ? patch : noop
...
如果是浏览器环境下,被赋值为 patch 方法,该方法定义在 src/platforms/web/runtime/patch.js中。如果是非浏览器环境,patch 被赋值成一个空函数。
/* @flow */
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
// 调用 createPatchFunction 函数,返回 patch。
export const patch: Function = createPatchFunction({ nodeOps, modules })
通过代码可以看到,最终 vue 是调用了 createPatchFunction 函数,它定义在 src/core/vdom/patch.js 中。createPatchFunction 函数内部定义了如 emptyNodeAt、removeNode、createElement、createChildren 等一系列的辅助函数,通过这些辅助函数,完成了对 patch 函数的代码逻辑的封装。
2.2 初始化的 patch
创建Vue实例,或者组件实例的,patch 都会被执行。
-
如果是创建vue实例执行 patch
isRealElement:判断是否是真实的DOM节点。
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly):负责DOM的更新。
oldVnode = emptyNodeAt(oldVnode):对容器DOM进行vnode的转化。
createElm():创建新节点,初始化创建需要重点关注的函数。
-
如果是创建组件实例执行的 patch
isInitialPatch:用户判断子组件否初次执行 patch,进行创建。
insertedVnodeQueue:新创建子组件节点,组件 vnode 会被push到这个队列中。
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)。
具体代码注释如下
export function createPatchFunction (backend) {
...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 如果新的 vnode 为空,调用 destory 钩子,销毁oldVnode
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
// 用户判断子组件否初次执行 patch,进行创建。
let isInitialPatch = false
// 新创建子组件节点,组件 vnode 会被push到这个队列中
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
// 空挂载(可能作为组件),创建新的根元素
isInitialPatch = true
// 创建组件节点的子元素
createElm(vnode, insertedVnodeQueue)
} else {
// 1.作为判断是否是真实的DOM节点条件
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
// 更新操作
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
...
// either not server-rendered, or hydration failed.
// create an empty node and replace it
// 2. 传入的容器DOM(如 el: "#app"),会在这里被转化成 vnode。
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
// 3. 创建新节点
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)) {...
}
// destroy old node
// 销毁旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
// 调用 insertedVnodeQueue 队列中所有子组件的 insert 钩子。
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
三、createElm 动态创建DOM
createElm
函数是动态创建 DOM 的核心,作用是通过 vnode 创建真实的 DOM,并插入到它的父 DOM 节点中。它定义在 src/core/vdom/patch.js
的 createPatchFunction 方法中。createElm 内部创建 DOM 的主要判断逻辑,可以概括为下面几种情况。
1、如果创建组件节点
如果碰到子组件标签,走创建组件节点逻辑。
创建完成,插入到父亲元素中。
2、如果创建标签元素节点
如果 vnode.tag 不为空,先创建标签元素, 赋值 vnode.elm 进行占位。
调用
createChildren
创建子节点,最终这些子节点会 append 到 vnode.elm 标签元素中。将 vnode.elm 标签元素插入到父亲元素中。
3、如果创建注释节点
如果 vnode.isComment 不为空,创建注释节点,赋值 vnode.elm。
将注释节点插入到父亲元素中。
4、如果创建文本节点
上面三种情况都不是,则创建文本节点,赋值 vnode.elm。
将文本节点插入到父亲元素中。
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
// 1、如果碰到子组件标签,走创建组件节点逻辑,插入父亲节点。
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
// 2、如果是标签标记,先创建标签元素进行占位。
// 调用 createChildren 创建子节点(递归调用createElm)。
// 将标签元素,插入父亲元素。
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
)
}
}
// 通过上面的tag,创建标签元素,赋值给 vnode.elm 进行占位
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__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)) {
// 3、创建注释节点,插入到父亲元素
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
// 4、创建文本节点,插入到父亲元素
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
下面对动态创建的几种情况分别进行说明。
3.1 创建组件节点
创建组件节点和 vue 的组件系统息息相关,这里先不具体展开,之后的博文中单独分析 vue 组件系统。只需要记住 vue 模板里的子组件初始化创建,是在这一步进行即可。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
// 创建组件节点
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
...
}
createComponent 这个方法也定义在 src/core/vdom/patch.js 的 createPatchFunction 的方法中,这里先简单的介绍一下这个方法的内部逻辑。
通过 vnode.data 中是否包含组件相关的 hook,来判断当前 vnode 是否是子组件 vnode(组件的 vnode,会包含 init 等钩子方法)。
调用 init,执行子组件的初始化流程,创建子组件实例,进行子组件挂载。
将生成的子组件 DOM 赋值给 vnode.elm。
通过 vnode.elm 将创建的子组件节点,插入到父亲元素中。
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 调用组件 init 钩子后,会执行子组件的**初始化流程**
// 创建子组件实例,进行子组件挂载。
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// 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.
// 如果是组件实例,将创建 vnode.elm 占位符
// 将生成的组件节点,插入到父亲元素中
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
如果是创建组件节点,并且成功,createComponent 函数返回 true。createElm 函数执行到 return。
如果是其他类型的节点,createComponent 函数返回 undefined,createElm 函数,会向下执行创建其他类型节点(标签元素、注释、文本)的代码逻辑。
综上所述,createElm 函数执行,只要碰到组件标签,会递归的去初始化创建子组件,简图如下所示(绿色线路部分)。
再调用 insert(parentElm, vnode.elm, refElm),将生成的组件节点插入到父亲元素中(遵从先子后父)。
3.2 创建标签元素节点
createElm
判断如果 vnode 不是组件的 vnode,它会判断是否是标签元素,从而进行创建标签元素节点的代码逻辑, 主要逻辑分析如下。
vnode.tag 标签属性存在,通过 tag 创建对应的标签元素,赋值给 vnode.elm 进行占位。
调用 createChildren 创建子节点(遍历子vnode,递归调用 createElm 函数)。
将创建的标签元素节点,插入父亲元素。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
// 2、如果是标签标记,先创建标签元素进行占位。
// 调用 createChildren 创建子节点(遍历子vnode,递归调用 createElm 函数)。
// 将标签元素,插入父亲元素。
// 如果标签属性不为空
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
)
}
}
// 通过上面的tag,创建标签元素,赋值给 vnode.elm 进行占位
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)) {
// vnode.data 不为空,调用所有create的钩子。
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 将创建的标签元素节点,插入父亲元素
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
...
}
createChildren
函数主要逻辑如下
如果 vnode.children 是子 vnode 数组,遍历 vnode.children 中的每个子 vnode,递归的调用了 createElm 函数,创建对应的子节点,并插入到父亲元素中(此时的父亲元素 parentElm 为 vnode.elm)。
如果 vnode.text 为空字符串。就创建一个空文本节点,插入到 vnode.elm 元素中。
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(children)
}
// 遍历子vnode数组,递归调用 createElm
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
// 创建空文本节点,appendChildren 到 vnode.elm 中
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
上面已经创建完成子标签节点,invokeCreateHooks 调用执行所有子组件相关的 create 钩子。这个方法createElm、
initComponent 中都会被调用。如果在 initComponent 中调用,说明创建的子节点中有组件节点,还会将组件 vnode 添加到 insertedVnodeQueue 队列中。
// createElm 中
if (isDef(data)) {
// vnode.data 不为空,调用所有create的钩子。
invokeCreateHooks(vnode, insertedVnodeQueue)
}
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
// 所有组件相关的create钩子都调用
// initComponent调用的话,还会将各个子组件的 vnode 添加到 insertedVnodeQueue 队列中。
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
综上所述,createElm 创建标签节点内部通过 createChildren 实现了对 createElm 的遍历递归调用,实现了深度优先遍历,简图如下所示(蓝色线路部分)。
再调用 insert(parentElm, vnode.elm, refElm),将生成的元素节点插入到父亲元素中(遵从先子后父)。
3.3 创建注释节点
如果不是创建组件节点和元素节点,vue 就通过 vnode.isComment 属性判断,是否创建注释节点。创建完成之后,插入到父亲元素中(遵从先子后父)
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
3.3 创建文本节点
如果不是创建组件节点、元素节点、注释节点,vue 就创建文本节点,创建完成之后,插入到父亲元素中(遵从先子后父)。
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
四、销毁旧节点
通过前面章节的分析,知道了 patch 函数,主要通过 createElm 动态的创建好了 DOM,并且已经成功添加到了旧DOM的后面,所以下一步操作,就只需要将旧 DOM 进行删除即可。
// destroy old node
// 销毁旧的节点(如 el: "app" 这个DOM)
// 创建完成的整个dom会append到 el: "app", 的父亲元素(如 parentElm 为 body)上
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
五、总结
vue 通过调用 patch 函数进行初始化 DOM 的创建。
patch 的关键是理解内部 createElm 这个函数,它会判断组件、元素、注释、文本这些类型的节点,来创建相应的DOM,完成之后添加到父元素。
vue 的组件系统实现,关键在于动态创建组件节点的逻辑当中。
新 DOM 创建添加过程是从子到父的,而组件的实例化是从父到子的。