概念
真实 DOM
谈虚拟 DOM 之前,我们先了解什么是真实 DOM,就是 HTML 文件中真实的元素,提供了 API 让我们可以直接操作真实的 DOM 节点,比如修改 innerHTML,增加子节点...
image
html:
<div id="app">Hello World</div>
var dom = document.getElementById("app");
dom.innerHTML; // ""
虚拟 DOM
虚拟 DOM 就是能用抽象后对象来描述真实 DOM, 在实际操作下,描述真实 DOM 只是虚拟 DOM 需要满足的一个最主要的需求
下面就是满足上面 html 的虚拟 DOM
var vApp = {
// 标签名
tagName: "div",
// 属性名
attrs: {
id: "app",
},
// 子节点
children: [],
};
Vue 的虚拟节点接口定义
export interface VNode {
tag?: string;
data?: VNodeData;
children?: VNode[];
text?: string;
elm?: Node;
ns?: string;
context?: Vue;
key?: string | number;
componentOptions?: VNodeComponentOptions;
componentInstance?: Vue;
parent?: VNode;
raw?: boolean;
isStatic?: boolean;
isRootInsert: boolean;
isComment: boolean;
}
虚拟 DOM 的好处:
- 抽象:渲染过程抽象化后,增加代码可维护性;
- 跨平台:不同平台最终的渲染有不同的实现,比如服务端渲染,小程序,基于虚拟 DOM 兼容多个平台的渲染就容易多了。
注意:vDOM 性能不一定高,参考:尤雨溪关于虚拟 DOM 性能的回答
下面内容都是基于 Web 的处理
转换过程
从模板创建虚拟 DOM 有前置操作,我们写的 vue 和小程序,都是需要通过解析成 AST(抽象语法树)、AST 转换、生成可执行代码,才能生成我们需要的虚拟 DOM,有机会再展开;
render
生成真实 DOM,真实 DOM 节点有 8 种,我们只实现其中的元素节点和文字节点。
// render.js
function createElement(vNode) {
const { tagName, attrs = {}, children = [] } = vNode;
const $el = document.createElement(tagName);
// 处理属性
for (const [k, v] of Object.entries(attrs)) {
$el.setAttribute(k, v);
}
// 处理子元素
for (const child of children) {
$el.appendChild(render(child));
}
return $el;
}
const render = (vNode) => {
// 生成文字节点
if (typeof vNode === "string") {
return document.createTextNode(vNode);
}
// 生成元素节点
return createElement(vNode);
};
export default render;
import render from "./vdom/render";
var vApp = {
tagName: "div",
attrs: {
id: "app",
},
children: [
`hello world`
],
}
var $node = render(vApp);
console.log($node)
mount
挂载生成的 DOM
export default ($node, $target) => {
$target.replaceWith($node);
return $node;
};
main.js
// ...
var $rootEl = mount($node, document.getElementById('app'));
mount 之后更新
import render from "./vdom/render";
import mount from "./vdom/mount";
const createVApp = (count) => {
var vApp = {
tagName: "div",
attrs: {
id: "app",
},
children: [
`count is:`,
String(count),
{
tagName: "input",
attrs: { style: "display:block" },
children: [],
},
...Array.from({ length: count }, () => {
return {
tagName: "img",
attrs: {
src: "https://i.loli.net/2020/12/29/vxB5o4Cy1hk6YOV.png",
},
children: []
}
}),
],
};
return vApp;
};
var count = 0;
var vApp = createVApp(count);
var $node = render(vApp);
var $rootEl = mount($node, document.getElementById("app"));
setInterval(() => {
count++;
var newVApp = createVApp(count);
$rootEl = mount(render(newVApp), $rootEl);
}, 1000);
审查元素发现,输入框的数据每次更新都被清除,整个DOM都会被刷新,如何只更新修改的节点,就需要 diff 来判断修改的节点了
diff
我们先讨论节点更新简单的三种情况
- 新的节点树是 undefined, 那么直接销毁
- 新旧节点有节点是文本类型,比较替换
- 新旧节点 tagName 不一致,直接替换
if (newVTree === undefined) {
return ($node) => {
$node.remove();
return undefined;
};
}
if (typeof newVTree === "string" || typeof oldVTree === "string") {
if (newVTree !== oldVTree) {
return ($node) => {
const $newNode = render(newVTree);
$node.replaceWith($newNode);
return $node;
};
} else {
return ($node) => $node;
}
}
if (newVTree.tagName !== oldVTree.tagName) {
return ($node) => {
const $newNode = render(newVTree);
$node.replaceWith($newNode);
return $node;
};
}
接下来的就是相同 tagName 的元素,可能属性、子节点的 diff
- 属性
const diffAttrs = (oldAttrs, newAttrs) => {
const patches = [];
for (const [k, v] of Object.entries(newAttrs)) {
if (!(k in oldAttrs)) {
patches.push(($node) => {
$node.setAttribute(k, v);
return $node;
});
}
}
for (const k in oldAttrs) {
if (!(k in newAttrs)) {
patches.push(($node) => {
$node.removeAttribute(k);
return $node;
});
}
}
return ($node) => {
for (const patch of patches) {
patch($node);
}
return $node;
};
};
- 子节点根据新旧子节点长度,1. 递归 diff 旧子节点,2. 增加新节点(新子节点数多于旧子节点数)
// 用来生成新旧patch数组
const zip = (xs, ys) => {
const zipped = [];
for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
zipped.push([xs[i], ys[i]]);
}
return zipped;
};
// diffChildren
const diffChildren = (oldVChildren, newVChildren) => {
const childPatches = [];
// 旧节点diff递归
oldVChildren.forEach((oldVChild, i) => {
childPatches.push(diff(oldVChild, newVChildren[i]));
});
// 新增节点
const additionalPatches = [];
for (const additionalVChild of newVChildren.slice(oldVChildren.length)) {
additionalPatches.push(($node) => {
$node.appendChild(render(additionalVChild));
return $node;
});
}
// 更新节点
return ($parent) => {
for (const [patch, $child] of zip(childPatches, $parent.childNodes)) {
patch($child);
}
for (const patch of additionalPatches) {
patch($parent);
}
return $parent;
};
};
// diff
const diff = (oldVTree, newVTree) => {
// ......
const patchAttrs = diffAttrs(oldVTree.attrs, newVTree.attrs);
const patchChildren = diffChildren(oldVTree.children, newVTree.children);
return ($node) => {
patchAttrs($node);
patchChildren($node);
return $node;
};
};
完整的diff方法, 传入新旧虚拟DOM, 生成patch回调(传入真实DOM后返回更新后的真实DOM)
import render from "./render";
const zip = (xs, ys) => {
const zipped = [];
for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
zipped.push([xs[i], ys[i]]);
}
return zipped;
};
const diffAttrs = (oldAttrs, newAttrs) => {
const patches = [];
for (const [k, v] of Object.entries(newAttrs)) {
if (!(k in oldAttrs)) {
patches.push(($node) => {
$node.setAttribute(k, v);
return $node;
});
}
}
for (const k in oldAttrs) {
if (!(k in newAttrs)) {
patches.push(($node) => {
$node.removeAttribute(k);
return $node;
});
}
}
return ($node) => {
for (const patch of patches) {
patch($node);
}
return $node;
};
};
const diffChildren = (oldVChildren, newVChildren) => {
const childPatches = [];
// 旧节点diff
oldVChildren.forEach((oldVChild, i) => {
childPatches.push(diff(oldVChild, newVChildren[i]));
});
// 新增节点
const additionalPatches = [];
for (const additionalVChild of newVChildren.slice(oldVChildren.length)) {
additionalPatches.push($node => {
$node.appendChild(render(additionalVChild));
return $node;
});
}
// 更新节点
return $parent => {
for (const [patch, $child] of zip(childPatches, $parent.childNodes)) {
patch($child);
}
for (const patch of additionalPatches) {
patch($parent);
}
return $parent;
};
};
const diff = (oldVTree, newVTree) => {
if (newVTree === undefined) {
return ($node) => {
$node.remove();
return undefined;
};
}
if (typeof newVTree === "string" || typeof oldVTree === "string") {
if (newVTree !== oldVTree) {
return ($node) => {
const $newNode = render(newVTree);
$node.replaceWith($newNode);
return $node;
};
} else {
return ($node) => $node;
}
}
if (newVTree.tagName !== oldVTree.tagName) {
return ($node) => {
const $newNode = render(newVTree);
$node.replaceWith($newNode);
return $node;
};
}
const patchAttrs = diffAttrs(oldVTree.attrs, newVTree.attrs);
const patchChildren = diffChildren(oldVTree.children, newVTree.children);
return ($node) => {
patchAttrs($node);
patchChildren($node);
return $node;
};
};
export default diff;
// main.js
import diff from "./vdom/diff";
// ....
setInterval(() => {
count = Math.floor(Math.random() * 5);
const vNewApp = createVApp(count);
const patch = diff(vApp, vNewApp);
$rootEl = patch($rootEl);
vApp = vNewApp;
}, 1000);
审查元素可发现,只有发生变化的元素DOM发生了更新,input数据不再丢失。