持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 18 天,点击查看活动详情
14. 虚拟DOM-4.patch
start
- 上一节提到了
__patch__
,这一节继续梳理相关的逻辑
__patch__
// `src/platforms/web/runtime/index.js`
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
// noop 其实就是一个空函数
export function noop(a?: any, b?: any, c?: any) {}
__patch__
赋值的时候做了区分,如果是浏览器端的,才需要修改真实 DOM,所以赋值为 patch
函数。其他情况是不需要修改 DOM 的,直接赋值一个 noop
;
noop 看到源码,其实也就是一个空函数
patch
// src\platforms\web\runtime\patch.js
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)
// 是一个函数
export const patch: Function = createPatchFunction({ nodeOps, modules })
patch
是什么?看到最后一行的代码,patch
其实就是 createPatchFunction
函数执行后的返回值(是一个函数类型的)。
解释一下传入的参数
nodeOps
封装了一系列 DOM 操作的方法;
modules
定义了一些模块的钩子函数的实现;
createPatchFunction
// src\core\vdom\patch.js
// 创建 patch 的函数
export function createPatchFunction(backend) {
/* 省略了函数的声明 */
// 主干逻辑 patch ,滑动到底部,createPatchFunction 函数归根到底返回的是 patch这个函数
// 四个参数, 旧节点;_render返回的新节点;是否是服务端渲染;removeOnly 是给 transition-group 用的,之后会介绍。
return function patch(oldVnode, vnode, hydrating, removeOnly) {
// 1. 新节点 是否是undeifned 或者 null
if (isUndef(vnode)) {
// 旧节点存在 调用销毁钩子
if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
return;
}
let isInitialPatch = false;
const insertedVnodeQueue = [];
// 2.如果旧节点为空,新节点存在
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
// 空挂载(可能作为组件),创建新的根元素
isInitialPatch = true;
// 依据 新节点vnode 直接创建 真实dom
createElm(vnode, insertedVnodeQueue);
} else {
// oldVnode 是不是真实的 DOM 元素
const isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
// 给现有根节点打补丁
// 3. 是相似的 vnode,开始深入对比
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
/* 服务端渲染的逻辑 */
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
if (isTrue(hydrating)) {
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."
);
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
/* 如果不是服务端,返回一个空的 vnode */
oldVnode = emptyNodeAt(oldVnode);
}
// replacing existing element
// 4. 替换现有的元素
const oldElm = oldVnode.elm;
const parentElm = nodeOps.parentNode(oldElm);
// create new node
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)) {
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);
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert;
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]();
}
}
} else {
registerRef(ancestor);
}
ancestor = ancestor.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;
};
}
createPatchFunction
这个方法代码行数比较多,接近 1000 行左右的 js。
直接到最后一行,可以看到它本质上是返回的一个名为 patch
的函数。
createPatchFunction
函数开头的部分代码我做了省略,着重研究一下这个返回的patch
函数
patch
1.patch的四个参数
- 旧节点;
- _render 返回的新节点;
- 是否是服务端渲染;
removeOnly
是给 transition-group 用的,之后会介绍。
2.认识一下patch
中有关的函数
2.1 isUndef
export function isUndef(v: any): boolean %checks {
return v === undefined || v === null
}
// 判断 传入的参数 是否 为 undefined 或者为 null
2.2 isDef
export function isDef(v: any): boolean %checks {
return v !== undefined && v !== null
}
//判断 传入的参数 是否 不是 undefined 也不是 null
2.3 sameVnode
// 是不是相似的 虚拟节点
function sameVnode(a, b) {
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
)
}
// 参数:`a,b`分别为旧节点,新节点。
// 作用:判断新旧 vnode 是否有所不同。
// 核心逻辑: 可以看到,判断是否是相似节点,判断了 key,tag,asyncFactory,isComment
这里就可以看到, key 对于 vnode对比 的重要性了。
2.4 isRealElement
const isRealElement = isDef(oldVnode.nodeType);
// 什么情况 isRealElement 会为 true。
// 组件初始化的时候,会传入一个真实DOM,oldVnode.nodeType就会存在。
3. patch主干逻辑
3.1 vnode 不存在,oldVnode 存在,调用 invokeDestroyHook 方法销毁 oldVnode;
旧的存在,新的不存在,说明旧的那些不需要了,所以直接销毁即可;
3.2 vnode 存在,oldVnode 不存在,说明是最初挂载,将 isInitialPatch 标记置为 true,调用了 createElm(vnode, insertedVnodeQueue) 方法创建一个新的根元素;
旧的不存在,新的存在,直接创建一个新的根元素
createElm
创建真实的dom
oldVnode 、vnode 都存在:
3.3 是相同节点,这里有个判断是同一个节点的方法 sameVnode
,调用 patchVnode
方法对节点进行比对;
相同节点,就需要
patchVnode
去 diff
3.4 如果 vnode 和 oldVnode 不是同一个节点,那么根据 vnode 创建新的元素并挂载至 oldVnode 父元素下。
直接拿到 之前节点的父元素,然后将最新的元素更新到 oldVnode 父元素下,删除旧的元素即可。
4. 小节
- 整体看下来,从
__patch__
到createPatchFunction
函数执行后的返回的patch
函数。 - 从
createPatchFunction
函数执行后的返回的patch
函数 到 创建真实的dom。
end
到目前为止可以梳理到这么一个流程: