本文为系列文章:
手写虚拟DOM(一)—— VirtualDOM介绍
手写虚拟DOM(二)—— VirtualDOM Diff
手写虚拟DOM(三)—— Diff算法优化
手写虚拟DOM(四)—— 进一步提升Diff效率之关键字Key
手写虚拟DOM(五)—— 自定义组件
手写虚拟DOM(六)—— 事件处理
手写虚拟DOM(七)—— 异步更新
一、前言
本文继续上一节的Virtual DOM内容,来聊一聊,当内容即dom tree发生变化时,
如何基于Virtual DOM来进行局部刷新渲染,而不是整棵树都重新渲染。
二、思路
使用 Virtual DOM 的框架,一般的设计思路都是页面等于页面状态的映射,即 UI = render(state)。当需要更新页面的时候,无需关心 DOM 具体的变换方式,只需要改变state即可,剩下的事情(render)将由框架代劳。我们考虑最简单的情况,当 state 发生变化时,我们重新生成整个 Virtual DOM ,触发比较的操作。
上述过程分为以下四步:
- state 变化,生成新的 Virtual DOM;
- 比较新 Virtual DOM 与之前 Virtual DOM 的差异;
- 生成差异对象(patch);
- 遍历差异对象并更新 DOM;
差异对象的数据结构是下面这个样子,与每一个 VDOM 元素一一对应:
{
type,
vdom,
props: [{
type,
key,
value
}],
children
}
DOM 元素的 type 对应的变化类型,有 4 种:新建、删除、替换和更新。
props 变化的 type 只有2种:更新和删除。
枚举值如下:
const TYPES = {
CREATE: 'create',
DELETE: 'delete',
UPDATE: 'update',
REPLACE: 'replace'
};
三、代码实现
此例通过每3秒,新增一个 div 块元素,并修改根元素的属性(data-number)
3.1、改造原有代码
// 新增状态对象
const state = {
number: 0
};
// 新增上一次的Virtual DOM内容
let vdomPre;
// 根据 state.number 来计算有多少个 div
function view() {
return (
<div data-number={state.number}>
Hello World!
{
[...Array(state.number).keys()].map(idx => (
<div id={'div' + idx} data-idx={idx}>-> {idx}</div>
))
}
</div>
);
}
/*************************************************
* 在 render 中:
* 1. 首次创建 dom;
* 2. 非首次则比较前后两次 Virtual DOM 差异,并更新
* 3. 记录本次的 Virtual DOM
*************************************************/
function render(container) {
const vdom = view();
if (!vdomPre) {
container.appendChild(createElement(vdom));
} else {
patch(diff(vdomPre, vdom), container);
}
vdomPre = vdom;
setTimeout(() => {
state.number += 1;
render(container);
}, 3000);
}
// 执行render
render(document.getElementById("app"));
3.2、diff的算法实现(简单版)
/***************************************
* 这里的diff,实际上就是一个递归的过程:
*
* 1. diff根元素;
* 2. diff根元素的props;
* 3. diff根元素的children;
* 4. 每个child的diff又是调用diff方法
***************************************/
function diff(pre, post) {
// 原vdom没有,新vdom有,则表明是新增节点
if (pre === undefined) {
return {
type: TYPES.CREATE,
vdom: post
};
}
// 原vdom有,新vdom没有,则表明是移除节点
if (post === undefined) {
return {
type: TYPES.DELETE
};
}
// 新老节点(类型不同 or tag不同 or 内容不同),则表明是替换节点
if (typeof pre !== typeof post || pre.tag !== post.tag ||
(typeof pre === 'string' || typeof pre === 'number') && pre !== post) {
return {
type: TYPES.REPLACE,
vdom: post
};
}
// 至此,只有可能是当前vdom的自身props变化 or 其children发生变化
if (pre.tag) {
const props = diffProps(pre.props, post.props);
const children = diffChildren(pre, post);
if (props.length > 0 || children.length > 0) {
return {
type: TYPES.UPDATE,
props: props,
children: children
};
}
}
}
function diffProps(preProps, postProps) {
const patches = [];
// 合并所有props的键值(后者替换前者)
const all = {...preProps, ...postProps};
// 遍历props的所有键值
Object.keys(all).forEach(key => {
const ov = preProps[key];
const nv = postProps[key];
// 新vdom没有该属性
if (nv === undefined) {
patches.push({
pType: TYPES.DELETE,
key
});
}
// 老vdom没有该属性,or 该属性值与新vdom的属性值不一致
if (ov === undefined || ov !== nv) {
patches.push({
pType: TYPES.UPDATE,
key,
value: nv
});
}
});
return patches;
}
function diffChildren(pre, post) {
const patches = [];
// 子元素最大长度
const len = Math.max(pre.children.length, post.children.length);
// 依次遍历并diff子元素
for (let i = 0; i < len; i ++) {
const param = diff(pre.children[i], post.children[i]);
if (param) {
param['idx'] = i;
patches.push(param);
}
}
return patches;
}
3.3、差异(patch)更新
/***************************************************************
* Virtual DOM 增量更新
***************************************************************/
function patch(dif, parent, cid = 0) {
const len = parent.childNodes.length;
const child = cid >= len ? null : parent.childNodes[cid];
switch (dif.type) {
case TYPES.CREATE:
parent.appendChild(createElement(dif.vdom));
return;
case TYPES.DELETE:
parent.removeChild(child);
return;
case TYPES.REPLACE:
parent.replaceChild(createElement(dif.vdom), child);
return;
case TYPES.UPDATE:
patchProps(child, dif.props);
patchChildren(child, dif.children);
break;
}
}
function patchProps(element, props = []) {
if (!props || props.length === 0) {
return;
}
props.forEach(p => {
if (p.pType === TYPES.DELETE) {
element.removeAttribute(p.key);
} else if (p.pType === TYPES.UPDATE) {
element.setAttribute(p.key, p.value);
}
});
}
function patchChildren(element, children = []) {
if (!children || children.length === 0) {
return;
}
children.forEach(child => {
patch(child, element, child.idx);
});
}
3.4、NodeJS环境
本次的代码实现,用到了ES6,所以,需要添加 @babel/core 和 @babel/preset-env;
同时,需要移除原 babel-cli,换成 @babel/cli
// package.json 的修改
"dependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"babel-plugin-transform-react-jsx": "^6.24.1"
}
// .babelrc 的修改
{
"plugins": [
[
"transform-react-jsx",
{
"pragma": "v"
}
]
],
"presets": ["@babel/preset-env"]
}
四、总结
本文详细介绍如何实现一个简单的 Virtual DOM Diff 算法,再根据计算出的差异去更新真实的 DOM 。然后对性能做了一个简单的分析,得出使用 Virtual DOM 在减少渲染时间的同时增加了 JS 计算时间的结论。
项目源码:
https://github.com/qingye/VirtualDOM-Study/tree/master/VirtualDOM-Study-02
网友评论