Part3-1-4 Vue.js Virtual DOM 的实现原理
Posted 沿着路走到底
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Part3-1-4 Vue.js Virtual DOM 的实现原理相关的知识,希望对你有一定的参考价值。
Virtual DOM
Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象
使用 Virtual DOM 来描述 真实DOM
创建 虚拟DOM 比创建 真实DOM 的开销小很多
虚拟 DOM 的作用
维护视图和状态的关系
• 复杂视图情况下提升渲染性能
• 跨平台
• 浏览器平台渲染DOM
• 服务端渲染 SSR(Nuxt.js/Next.js)
• 原生应用(Weex/React Native)
• 小程序(mpvue/uni-app)等
虚拟 DOM 库 Snabbdom
• Vue.js 2.x 内部使用的虚拟 DOM 就是改造的 Snabbdom
• 大约 200 SLOC (single line of code)
• 通过模块可扩展
• 源码使用 TypeScript 开发
• 最快的 Virtual DOM 之一
Snabbdom 基本使用
创建项目
• 安装 parcel
• 配置 scripts
• 目录结构
安装 parcel
npm install parcel-bundler -D
配置 scripts
"scripts": {
"dev": "parcel index.html --open",
"build": "parcel build index.html"
},
Snabbdom 文档
看文档的意义
• 学习任何一个库都要先看文档
• 通过文档了解库的作用
• 看文档中提供的示例,自己快速实现一个 demo
• 通过文档查看 API 的使用
Snabbdom 文档
• https://github.com/snabbdom/snabbdom
• 当前版本 v2.1.0
安装 Snabbdom
npm intall snabbdom@2.1.0
导入 Snabbdom
Snabbdom 的两个核心函数 init 和 h()
init() 是一个高阶函数,返回 patch()
h() 返回虚拟节点 VNode,这个函数我们在使用 Vue.js 的时候见过
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
const patch = init([])
node 12,webpack5 支持在 package.json 中 简化路径
Snabbdom 基本使用
文本
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
const patch = init([])
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串就是标签中的文本内容
let vnode = h('div#container.cls',{
hook: {
init (vnode) {
console.log(vnode.elm)
},
create (emptyNode, vnode) {
console.log(vnode.elm)
}
}
}, 'Hello World')
let app = document.querySelector('#app')
// 第一个参数:旧的 VNode,可以是 DOM 元素
// 第二个参数:新的 VNode
// 返回新的 VNode
let oldVnode = patch(app, vnode)
vnode = h('div#container.xxx', 'Hello Snabbdom')
patch(oldVnode, vnode)
包含子元素
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
const patch = init([])
let vnode = h('div#container', [
h('h1', 'Hello Snabbdom'),
h('p', '这是一个p')
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)
setTimeout(() => {
// vnode = h('div#container', [
// h('h1', 'Hello World'),
// h('p', 'Hello P')
// ])
// patch(oldVnode, vnode)
// 清除div中的内容
patch(oldVnode, h('!'))
}, 2000);
模块的作用
• Snabbdom 的核心库并不能处理 DOM 元素的属性/样式/事件等, 可以通过注册 Snabbdom 默认提供的模块来实现
• Snabbdom 中的模块可以用来扩展 Snabbdom的功能
• Snabbdom 中的模块的实现是通过注册全局的钩子函数来实现的
官方提供的模块
• attributes
• props
• dataset
• class
• style
• eventlisteners
模块使用步骤
• 导入需要的模块
• init() 中注册模块
• h() 函数的第二个参数处使用模块
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
// 1. 导入模块
import { styleModule } from 'snabbdom/build/package/modules/style'
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
// 2. 注册模块
const patch = init([
styleModule,
eventListenersModule
])
// 3. 使用h() 函数的第二个参数传入模块中使用的数据(对象)
let vnode = h('div', [
h('h1', { style: { backgroundColor: 'red' } }, 'Hello World'),
h('p', { on: { click: eventHandler } }, 'Hello P')
])
function eventHandler () {
console.log('别点我,疼')
}
let app = document.querySelector('#app')
patch(app, vnode)
如何学习源码
• 宏观了解
• 带着目标看源码
• 看源码的过程要不求甚解
• 调试
• 参考资料
Snabbdom 的核心
• init() 设置模块,创建 patch() 函数
• 使用 h() 函数创建 javascript 对象(VNode)描述真实 DOM
• patch() 比较新旧两个 Vnode
• 把变化的内容更新到真实 DOM 树
Snabbdom 源码
源码地址
• https://github.com/snabbdom/snabbdom
• 当前版本:v2.1.0
克隆代码
• git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git
Snabbdom 源码解析
h 函数介绍
• 作用:创建 VNode 对象
• Vue 中的 h 函数
new Vue({
router,
render: h => h(App)
}).$mount('#app')
• h 函数最早见于 hyperscript,使用 JavaScript 创建超文本
函数重载
• 参数个数或参数类型不同的函数
• JavaScript 中没有重载的概念
• TypeScript 中有重载,不过重载的实现还是通过代码调整参数
• 重载和参数相关,和返回值无关
函数重载-参数个数
函数重载-参数类型
snabbdom/src/h.ts
import { vnode, VNode, VNodeData } from "./vnode";
import * as is from "./is";
export type VNodes = VNode[];
export type VNodeChildElement = VNode | string | number | undefined | null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>;
function addNS(
data: any,
children: VNodes | undefined,
sel: string | undefined
): void {
data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0; i < children.length; ++i) {
const childData = children[i].data;
if (childData !== undefined) {
addNS(childData, children[i].children as VNodes, children[i].sel);
}
}
}
}
// h 函数的重载
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(
sel: string,
data: VNodeData | null,
children: VNodeChildren
): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
let data: VNodeData = {};
let children: any;
let text: any;
let i: number;
// 处理参数,实现重载的机制
if (c !== undefined) {
// 处理三个参数的情况
// sel、data、children/text
if (b !== null) {
data = b;
}
if (is.array(c)) {
children = c;
} else if (is.primitive(c)) { // 如果 c 是字符串或者数字
text = c;
} else if (c && c.sel) { // 如果 c 是 vNode
children = [c];
}
} else if (b !== undefined && b !== null) {
// 处理两个参数的情况
if (is.array(b)) {
children = b;
} else if (is.primitive(b)) { 如果 b 是字符串或者数字
text = b;
} else if (b && b.sel) { // 如果 b 是 vNode
children = [b];
} else {
data = b;
}
}
if (children !== undefined) {
// 处理 children 中的原始值 (string/number)
for (i = 0; i < children.length; ++i) {
// 如果 child 是 string/number, 创建文本节点
if (is.primitive(children[i]))
children[i] = vnode(
undefined,
undefined,
undefined,
children[i],
undefined
);
}
}
if (
sel[0] === "s" &&
sel[1] === "v" &&
sel[2] === "g" &&
(sel.length === 3 || sel[3] === "." || sel[3] === "#")
) {
// 如果是 svg,添加命名空间
addNS(data, children, sel);
}
// 返回 VNode
return vnode(sel, data, children, text, undefined);
}
VNode
snabbdom/src/vnode.ts
import { Hooks } from "./hooks";
import { AttachData } from "./helpers/attachto";
import { VNodeStyle } from "./modules/style";
import { On } from "./modules/eventlisteners";
import { Attrs } from "./modules/attributes";
import { Classes } from "./modules/class";
import { Props } from "./modules/props";
import { Dataset } from "./modules/dataset";
export type Key = string | number | symbol;
// children 与 text 互斥
export interface VNode {
sel: string | undefined;
data: VNodeData | undefined;
children: Array<VNode | string> | undefined; // 记录子节点
elm: Node | undefined;
text: string | undefined; // 记录对应文本节点中的文本内容
key: Key | undefined;
}
export interface VNodeData {
props?: Props;
attrs?: Attrs;
class?: Classes;
style?: VNodeStyle;
dataset?: Dataset;
on?: On;
attachData?: AttachData;
hook?: Hooks;
key?: Key;
ns?: string; // for SVGs
fn?: () => VNode; // for thunks
args?: any[]; // for thunks
is?: string; // for custom elements v1
[key: string]: any; // for any other 3rd party module
}
export function vnode(
sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined
): VNode {
const key = data === undefined ? undefined : data.key;
return { sel, data, children, text, elm, key };
}
patch 整体过程分析
• patch(oldVnode, newVnode)
• 把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
• 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
• 如果不是相同节点,删除之前的内容,重新渲染
• 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
• 如果新的 VNode 有 children,判断子节点是否有变化,
snabbdom/src/init.ts
import { Module } from "./modules/module";
import { vnode, VNode } from "./vnode";
import * as is from "./is";
import { htmlDomApi, DOMAPI } from "./htmldomapi";
type NonUndefined<T> = T extends undefined ? never : T;
function isUndef(s: any): boolean {
return s === undefined;
}
function isDef<A>(s: A): s is NonUndefined<A> {
return s !== undefined;
}
type VNodeQueue = VNode[];
const emptyNode = vnode("", {}, [], undefined, undefined);
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
const isSameKey = vnode1.key === vnode2.key;
const isSameIs = vnode1.data?.is === vnode2.data?.is;
const isSameSel = vnode1.sel === vnode2.sel;
return isSameSel && isSameKey && isSameIs;
}
function isVnode(vnode: any): vnode is VNode {
return vnode.sel !== undefined;
}
type KeyToIndexMap = { [key: string]: number };
type ArraysOf<T> = {
[K in keyof T]: Array<T[K]>;
};
type ModuleHooks = ArraysOf<Required<Module>>;
function createKeyToOldIdx(
children: VNode[],
beginIdx: number,
endIdx: number
): KeyToIndexMap {
const map: KeyToIndexMap = {};
for (let i = beginIdx; i <= endIdx; ++i) {
const key = children[i]?.key;
if (key !== undefined) {
map[key as string] = i;
}
}
return map;
}
const hooks: Array<keyof Module> = [
"create",
"update",
"remove",
"destroy",
"pre",
"post",
];
// domApi 把 VNode 对象转换为其他平台下对应的元素,默认为 DOM 操作的 API
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number;
let j: number;
const cbs: ModuleHooks = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: [],
};
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
(cbs[hooks[i]] as any[]).push(hook);
}
}
}
function emptyNodeAt(elm: Element) {
const id = elm.id ? "#" + elm.id : "";
// elm.className doesn't return a string when elm is an SVG element inside a shadowRoot.
// https://stackoverflow.com/questions/29454340/detecting-classname-of-svganimatedstring
const classes = elm.getAttribute("class");
const c = classes ? "." + classes.split(" ").join(".") : "";
return vnode(
api.tagName(elm).toLowerCase() + id + c,
{},
[],
undefined,
elm
);
}
function createRmCb(childElm: Node, listeners: number) {
return function rmCb() {
if (--listeners === 0) {
const parent = api.parentNode(childElm) as Node;
api.removeChild(parent, childElm);
}
};
}
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
// 执行用户设置的 init 钩子函数
let i: any;
let data = vnode.data;
if (data !== undefined) {
const init = data.hook?.init;
if (isDef(init)) {
init(vnode);
data = vnode.data;
}
}
const children = vnode.children;
const sel = vnode.sel;
// 把 vnode 转换成真实 dom 对象(没有渲染到页面)
if (sel === "!") {
// 如果选择器是 ! ,创建注释节点
if (isUndef(vnode.text)) {
vnode.text = "";
}
vnode.elm = api.createComment(vnode.text!);
} else if (sel !== undefined) {
// Parse selector
// 如果选择器不为空,解析选择器
const hashIdx = sel.indexOf("#");
const dotIdx = sel.indexOf(".", hashIdx);
const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length;
const tag =
hashIdx !== -1 || dotIdx !== -1
? sel.slice(0, Math.min(hash, dot))
: sel;
const elm = (vnode.elm =
isDef(data) && isDef((i = data.ns))
? api.createElementNS(i, tag, data)
: api.createElement(tag, data));
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
if (dotIdx > 0)
elm.setAttribute("class", sel.slice(dot + 1).replace(/\\./g, " "));
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
// 如果 vnode 中有子节点,创建子 vnode 对应的 DOM 元素并追加到 DOM 树上
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
}
}
} else if (is.primitive(vnode.text)) {
api.appendChild(elm, api.createTextNode(vnode.text));
}
const hook = vnode.data!.hook;
if (isDef(hook)) {
hook.create?.(emptyNode, vnode);
if (hook.insert) {
insertedVnodeQueue.push(vnode);
}
}
} else {
// 如果选择器为空,创建文本节点
vnode.elm = api.createTextNode(vnode.text!);
}
// 返回创建的 dom
return vnode.elm;
}
function addVnodes(
parentElm: Node,
before: Node | null,
vnodes: VNode[],
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue
) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
}
}
}
function invokeDestroyHook(vnode: VNode) {
const data = vnode.data;
if (data !== undefined) {
data?.hook?.destroy?.(vnode);
for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
if (vnode.children !== undefined) {
for (let j = 0; j < vnode.children.length; ++j) {
const child = vnode.children[j];
if (child != null && typeof child !== "string") {
invokeDestroyHook(child);
}
}
}
}
}
function removeVnodes(
parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number
): void {
for (; startIdx <= endIdx; ++startIdx) {
let listeners: number;
let rm: () => void;
const ch = vnodes[startIdx];
if (ch != null) {
if (isDef(ch.sel)) {
invokeDestroyHook(ch);
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm!, listeners);
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
const removeHook = ch?.data?.hook?.remove;
if (isDef(removeHook)) {
removeHook(ch, rm);
} else {
rm();
}
} else {
// Text node
api.removeChild(parentElm, ch.elm!);
}
}
}
}
function updateChildren(
parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue
) {
let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = oldKeyToIdx[newStartVnode.key as string];
if (isUndef(idxInOld)) {
// New element
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
} else {
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
// 第一个过程:触发 prepatch 和 update 钩子函数
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);
const elm = (vnode.elm = oldVnode.elm)!;
const oldCh = oldVnode.children as VNode[];
const ch = vnode.children as VNode[];
if (oldVnode === vnode) return;
if (vnode.data !== undefined) {
for (let i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
vnode.data.hook?.update?.(oldVnode, vnode);
}
// 第二个过程:真正对比新旧 vnode 差异的地方
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, "");
}
} else if (oldVnode.text !== vnode.text) {
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
api.setTextContent(elm, vnode.text!);
}
// 第三个过程:触发 postpatch 钩子函数
hook?.postpatch?.(oldVnode, vnode);
}
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
elm = oldVnode.elm!;
parent = api.parentNode(elm) as Node;
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
};
}
export interface DOMAPI {
createElement: (
tagName: any,
options?: ElementCreationOptions
) => HTMLElement;
createElementNS: (
namespaceURI: string,
qualifiedName: string,
options?: ElementCreationOptions
) => Element;
createTextNode: (text: string) => Text;
createComment: (text: string) => Comment;
insertBefore: (
parentNode: Node,
newNode: Node,
referenceNode: Node | null
) => void;
removeChild: (node: Node, child: Node) => void;
appendChild: (node: Node, child: Node) => void;
parentNode: (node: Node) => Node | null;
nextSibling: (node: Node) => Node | null;
tagName: (elm: Element) => string;
setTextContent: (node: Node, text: string | null) => void;
getTextContent: (node: Node) => string | null;
isElement: (node: Node) => node is Element;
isText: (node: Node) => node is Text;
isComment: (node: Node) => node is Comment;
}
function createElement(
tagName: any,
options?: ElementCreationOptions
): HTMLElement {
return document.createElement(tagName, options);
}
function createElementNS(
namespaceURI: string,
qualifiedName: string,
options?: ElementCreationOptions
): Element {
return document.createElementNS(namespaceURI, qualifiedName, options);
}
function createTextNode(text: string): Text {
return document.createTextNode(text);
}
function createComment(text: string): Comment {
return document.createComment(text);
}
function insertBefore(
parentNode: Node,
newNode: Node,
referenceNode: Node | null
): void {
parentNode.insertBefore(newNode, referenceNode);
}
function removeChild(node: Node, child: Node): void {
node.removeChild(child);
}
function appendChild(node: Node, child: Node): void {
node.appendChild(child);
}
function parentNode(node: Node): Node | null {
return node.parentNode;
}
function nextSibling(node: Node): Node | null {
return node.nextSibling;
}
function tagName(elm: Element): string {
return elm.tagName;
}
function setTextContent(node: Node, text: string | null): void {
node.textContent = text;
}
function getTextContent(node: Node): string | null {
return node.textContent;
}
function isElement(node: Node): node is Element {
return node.nodeType === 1;
}
function isText(node: Node): node is Text {
return node.nodeType === 3;
}
function isComment(node: Node): node is Comment {
return node.nodeType === 8;
}
export const htmlDomApi: DOMAPI = {
createElement,
createElementNS,
createTextNode,
createComment,
insertBefore,
removeChild,
appendChild,
parentNode,
nextSibling,
tagName,
setTextContent,
getTextContent,
isElement,
isText,
isComment,
};
Diff 算法
• DOM 操作时候很少会跨级别操作节点
• 只比较同级别的节点
执行过程
在对开始和结束节点比较的时候,总共有四种情况
• oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
• oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
• oldStartVnode / newEndVnode (旧开始节点 / 新结束节点)
• oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)
旧开始节点 和 新开始节点之间的比较
首先将新开始节点与旧开始节点 调用 sameVnode 函数进行比较,如果是相同节点(key 和 sel 相同),调用 patchVnode 函数比较这2个节点内部的差异,然后更新到真实 DOM。
比较完成后,将索引移动到下一个节点的位置(oldStartIdx++ / newStartIdx++),再去比较第二个节点,把第二个节点作为开始节点进行比较。
旧结束节点 和 新结束节点 之间的比较
如果开始节点不是相同节点,会从后往前比较,比较旧的结束节点和新的结束节点是否是相同节点,如果是相同节点,调用 patchVnode 函数比较这2个节点内部的差异,然后更新到真实 DOM。
比较完成后,将索引移动到倒数第二个节点的位置(oldEndIdx-- / newEndIdx--),再去比较倒数第二个节点,把倒数第二个节点作为结束节点进行比较。
旧开始节点 和 新结束节点 之间的比较
然后移动索引,旧开始节点的索引移动到下一个位置,新结束索引的位置移动到前一个位置。
旧结束节点 和 新开始节点 之间的比较
如果以上四种情况都不满足时,说明开始和结束节点都不相同
首先遍历新的开始节点,在旧节点数组中,查找是否有相同节点,如果没有找到,说明此时的新开始节点是一个新的节点,此时需要创建一个新的 DOM 元素,插入到旧节点最前面的位置。
如果新开始节点在旧节点数组中找到了相同 key 值的节点,先判断 sel 是否相同,
如果 sel 不同,说明不是相同的节点,需要创建新的 DOM 元素,并插入到旧节点最前面的位置。如果 sel 相同,说明是相同节点,找到的这个旧节点与新开始节点通过patchVnode 函数进行比较,并更新到真实 DOM,然后将这个旧节点移动到最开始的位置。
当循环结束之后
当老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,将这些剩余的节点创建对应的 DOM 元素,并插入到 老节点的末尾。
如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,将老节点剩余的节点批量删除。
总结
如果是相同节点的话,会重用之前旧节点对应的 DOM 元素,在 patchVnode 函数中会对比新旧节点之间的差异,然后把差异更新到重用的 DOM 元素上。这个差异可能是文本内容不同,也可能是子元素不同,而当前 DOM 是不需要重新创建的,如果文本内容或子元素也都相同的话,是不会进行 DOM 操作的。虚拟 DOM 是通过这种方式提高性能的。
图
1
以上是关于Part3-1-4 Vue.js Virtual DOM 的实现原理的主要内容,如果未能解决你的问题,请参考以下文章