vue2.x的patch方法和客户端混合
之前确实没自己看过 vue2.x 的 _update 这一块,导致今天被面试官问到了,现在回头补一下这方面的知识。
从初始化 watcher 说起
我们知道,在声明了响应式数据之后,我们再实例化一个 watcher并调用响应式数据,才能把 watcher 添加到响应式数据的依赖里面获得更新。而 vue 也是这么做的,它对 vm 来实例化一个 watcher,这个 watcher 在声明的时候会立即对回调函数 updateComponent 求值,从而执行 _update 和 _render,从而把这个 watcher 添加到相应的响应式数据里面。源码如下:
// lifecycle.js
callHook(vm, 'beforeMount');
// ...
updateComponent = () => {
vm._update(vm._render(), hydrating)
};
// ...
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
这里有一个注意的点,就是这里的 _render 函数其实是调用的 render 函数,而解析 template 并进行静态节点优化以及生成render函数是在 $mount 方法里面进行的,也就是说 template 转化为 render 方法是在 beforeMount 生命周期里面进行的。(如果使用了 vue-loader 的话,那么 vue-loader 里面会直接把 template 打包成 render 方法。)
_render方法干了些什么呢
_render 方法生成了 vnode,代码如下:
// render.js
Vue.prototype._render = function (): VNode {
vnode = render.call(vm._renderProxy, vm.$createElement)
return vnode
}
_update方法干了些什么呢
_update 方法其实就是调用__patch__
方法,代码如下:
// lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
这里如果是初次渲染的话,prevNode 是没有的,所以会拿vm.$el
这个 DOM 元素去和新的 vnode 进行 patch;如果不是初次渲染的话,就拿旧的 vnode 和 新的 vnode 进行 patch。
__patch__方法干了些什么呢
__patch__
方法的源码和解释如下:
function patch (oldVnode, vnode, hydrating, removeOnly) {
// 如果没有新的 vnode,但有旧的 vnode,就销毁旧的 vnode
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// 如果旧的 vnode 没有,就表示是第一次渲染,则直接创建
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 如果旧的 vnode 不是 DOM node的形式,并且新旧节点一样,则使用 patchVnode 进行更新
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// 如果旧的 vnode 是元素节点,并且有服务端渲染标签的话,就移除标签并且进行服务端渲染
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
// 先判断能不能进行混合,能的话,调用 invokeInsertHook 方法进行混合
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// 如果混合失败,则使用空的节点
oldVnode = emptyNodeAt(oldVnode)
}
// 如果新旧节点不相同,则使用新的 vnode 进行渲染
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
const insert = ancestor.data.hook.insert
if (insert.merged) {
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// 移除老的 vnode
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
这里简单来说分为下面几种情况:
- 如果没有新的 vnode 只有旧的 vnode,就直接摧毁掉旧的 vnode;
- 如果没有旧的 vnode,就直接用新的 vnode 创建 DOM 元素;
- 对比新旧 vnode,如果是 sameVnode,就开始进行 patchVnode;
- 如果旧的 vnode 是 DOM,并且有 ssr 标记,则清除 ssr 标记并进行客户端激活。(如果激活失败则创建一个空的节点);
- 如果旧的 vnode 是 DOM,并且没有 ssr 标记,就用新的 vnode 创建 DOM 元素,并且挂载到新的 vnode 的父节点上面。
上面过程中主要有三点需要注意:
- 怎么用 vnode 创建 DOM 元素的?
- patchVnode 是怎么进行的?
- 怎么进行客户端激活?