vue2.x的patch方法和客户端混合
之前确实没自己看过 vue2.x 的 _update 这一块,导致今天被面试官问到了,现在回头补一下这方面的知识。
怎么创建 DOM 元素
我们首先来看怎么创建 DOM 元素,下面是 createElm 的源码:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// 如果这个 vnode 之前用到过,则从缓存里面克隆(注意,这里不是直接用以前的 vnode,因为所有的 vnode 都不能相同)
if (isDef(vnode.elm) && isDef(ownerArray)) {
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
// 如果是组件vnode(占位vnode),则使用 createComponent 创建这个组件
return
}
// 如果不是组件 vnode,那证明是 div、span 这种 vnode,那么直接用相应平台的方法进行创建
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__) {
// 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 之前用到过,则从缓存里面克隆(注意,这里不是直接用以前的 vnode,因为所有的 vnode 都不能相同)
- 如果是组件vnode(占位vnode),则使用 createComponent 创建这个组件
- 如果不是组件 vnode,那证明是 div、span 这种 vnode,那么直接用相应平台的方法进行创建
需要说明的是(源码就不贴出来了):
- 为了区分不同的平台(web或者weex),统一使用 modules 和 nodeOps 进行封装平台方法提供统一的接口。其中 modules 主要负责操作 dom 上的属性(class、style、events等),nodeOps 主要负责 DOM 的各种操作(创建、插入、寻找节点等)。
- insert 方法就是把创建的 DOM 元素插入父节点里面去。
createComponent
我们来看一下对于组件 vnode,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)) {
// 执行组件 vnode 的 init 钩子,
// 这个钩子会生成一个组件并赋值给 vnode 的 componentInstance 属性,
// 然后对这个组件执行 $mount 方法进行挂载
i(vnode, false /* hydrating */)
}
// 由于我们在上面生成了 componentInstance 属性,所以我们在这里把组件生成的 DOM 插入到父节点。
// initComponent 主要处理了 ref 的情况,并对插入时机做了一些调度。
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
// 这里是处理 keep-alive 组件的情况
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
这里首先执行组件 vnode 的 init 钩子,这个钩子会生成一个组件并赋值给 vnode 的 componentInstance 属性,然后对这个组件执行$mount
方法进行挂载。挂载完成后就插入父节点,此时组件 vnode 就创建了 DOM 元素。
需要说明的是:
- 由于在这里会执行组件的
$mount
方法,所以这里会执行组件的 beforeCreate、created、beforeMount、mounted 生命周期。这就是为什么子组件的这些生命周期会出现在父组件 mounted 生命周期之前的原因。 - 这里组件在执行
$mount
方法的时候又会对自己进行 patch,从而一直 patch 到叶子节点。
patchVnode 是怎么进行的
我们再来看看相同的 vnode 是怎么 patchVnode 的,下面是 patchVnode 的源码:
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 新老 vnode 完全一样,则直接返回
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// 缓存 vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
// 执行 prepatch 钩子
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
// 执行 update 钩子
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 更新文本
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 使用 diff 更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 旧的 vnode 的子节点不存在,直接增加新的 vnode 的子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 新的 vnode 的子节点不存在,直接删除旧的 vnode 的子节点
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
// 执行 postpatch 钩子
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
简单来说,patchVnode 首先会执行 prepatch 和 update 钩子(用来更新 props、listeners 等),然后更新两个节点的文本,然后对各自的子节点进行更新,如果新旧节点的子节点都存在,则使用diff 算法进行更新,最后执行 postpatch 钩子。
这里需要注意的是:在对子节点进行 patchVnode 的时候,又会对更深的子节点进行 patchVnode 或创建 DOM 元素,进行循环更新,直到叶子节点。
怎么进行客户端激活的
vue 首先会用 hydrate 判断可不可以进行客户端激活,源码如下:
// Note: this is a browser-only function so we can assume elms are DOM nodes.
function hydrate (elm, vnode, insertedVnodeQueue, inVPre) {
let i
const { tag, data, children } = vnode
inVPre = inVPre || (data && data.pre)
vnode.elm = elm
// elm 是服务器发回来的DOM,vnode 是客户端创建的 vnode
// 如果 vnode 是注释节点或静态节点,则直接返回 true
if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) {
vnode.isAsyncPlaceholder = true
return true
}
// 比较节点的 tag 是否一样
if (process.env.NODE_ENV !== 'production') {
if (!assertNodeMatch(elm, vnode, inVPre)) {
return false
}
}
// 如果 vnode 是组件节点,则使用 init 钩子生成组件节点
// 在生成组件节点的时候,组件节点自己会调用 patch 方法和 hydrate 方法判断能否激活
// 所以,只要组件节点生成成功,那么就不需要往下判断了
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode, true /* hydrating */)
if (isDef(i = vnode.componentInstance)) {
// child component. it should have hydrated its own tree.
initComponent(vnode, insertedVnodeQueue)
return true
}
}
// 如果 vnode 是非组件节点,那么递归比较子节点
if (isDef(tag)) {
if (isDef(children)) {
// elm 没有子节点但是 vnode 有子节点,则直接生成子节点插入到 elm 里面
if (!elm.hasChildNodes()) {
createChildren(vnode, children, insertedVnodeQueue)
} else {
// 比较 vnode 的 v-html 内容是否和 elm 相同
if (isDef(i = data) && isDef(i = i.domProps) && isDef(i = i.innerHTML)) {
if (i !== elm.innerHTML) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' &&
typeof console !== 'undefined' &&
!hydrationBailed
) {
hydrationBailed = true
console.warn('Parent: ', elm)
console.warn('server innerHTML: ', i)
console.warn('client innerHTML: ', elm.innerHTML)
}
return false
}
} else {
// iterate and compare children lists
let childrenMatch = true
let childNode = elm.firstChild
for (let i = 0; i < children.length; i++) {
if (!childNode || !hydrate(childNode, children[i], insertedVnodeQueue, inVPre)) {
childrenMatch = false
break
}
childNode = childNode.nextSibling
}
// if childNode is not null, it means the actual childNodes list is
// longer than the virtual children list.
if (!childrenMatch || childNode) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' &&
typeof console !== 'undefined' &&
!hydrationBailed
) {
hydrationBailed = true
console.warn('Parent: ', elm)
console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children)
}
return false
}
}
}
}
// 比较 elm 和 vnode 的 data
if (isDef(data)) {
let fullInvoke = false
for (const key in data) {
if (!isRenderedModule(key)) {
fullInvoke = true
invokeCreateHooks(vnode, insertedVnodeQueue)
break
}
}
if (!fullInvoke && data['class']) {
// ensure collecting deps for deep class bindings for future updates
traverse(data['class'])
}
}
} else if (elm.data !== vnode.text) {
// 比较 elm 和 vnode 的文本(如果不一样则直接使用vnode的文本)
elm.data = vnode.text
}
return true
}
这里主要做了下面几件事:
- 把 elm 赋值给
vnode.elm
储存起来。 - 比较 elm 和 vnode 的 tag 是否一样。(生产环境会跳过)
- 如果 vnode 是组件节点,则使用 init 钩子生成组件节点。(在生成组件节点的时候,组件节点自己会调用 patch 方法和 hydrate 方法判断能否激活,所以,只要组件节点生成成功,那么就不需要往下判断了)
- 如果 vnode 是非组件节点,那么递归比较子节点。
- 比较 elm 和 vnode 的 data
- 比较 elm 和 vnode 的文本(如果不一样则直接使用vnode的文本)
需要说明的是:
- 生产环境会忽略掉大部分错误,直接使用 vnode 对 elm 进行矫正。
- 为什么要进行客户端激活?因为如果直接把 vnode 生成 DOM 并挂载是非常耗时间和内存的,所以这里激活的过程就相当于给 elm 使用 vnode 进行 patch 的过程。
- 执行 hydrate 方法之后,其实就已经激活完成了,后面会直接使用
invokeInsertHook(vnode, insertedVnodeQueue, true)
进行插入了。 invokeInsertHook
方法会把createElm
里面的insert
方法缓存起来,最后在整个 root vnode 更新结束后,再一起执行insert
方法。
通俗的认识 vnode
vnode 其实并不神秘,它其实就是一堆数据和它的皮(elm)组成,它的皮(elm)是指挂载的 DOM,所以每次 patch 的时候会深度遍历比较它的数据,然后更新它的皮(elm);而在客户端激活的时候,只是 vnode 为了尽可能复用服务器发回来的 html(因为DOM操作很昂贵),而在服务器发回来的 html 上修修补补形成一个新的皮而已!!!