写点什么

你不知道的 virtual DOM(二):Virtual Dom 的更新

  • 2020-03-08
  • 本文字数:3830 字

    阅读完需:约 13 分钟

你不知道的virtual DOM(二):Virtual Dom的更新

一、前言

目前最流行的两大前端框架,React 和 Vue,都不约而同的借助 Virtual DOM 技术提高页面的渲染效率。那么,什么是 Virtual DOM ?它是通过什么方式去提升页面渲染效率的呢?本系列文章会详细讲解 Virtual DOM 的创建过程,并实现一个简单的 Diff 算法来更新页面。本文的内容脱离于任何的前端框架,只讲最纯粹的 Virtual DOM 。敲单词太累了,下文 Virtual DOM 一律用 VD 表示。


这是 VD 系列文章的第二篇,本文将会实现一个简单的 VD Diff 算法,计算出差异并反映到真实的 DOM 上去。

二、思路

使用 VD 的框架,一般的设计思路都是页面等于页面状态的映射,即 UI=render(state)。当需要更新页面的时候,无需关心 DOM 具体的变换方式,只需要改变 state即可,剩下的事情( render)将由框架代劳。我们考虑最简单的情况,当 state 发生变化时,我们重新生成整个 VD ,触发比较的操作。上述过程分为以下四步:


  • state 变化,生成新的 VD

  • 比较 VD 与之前 VD 的异同

  • 生成差异对象( patch

  • 遍历差异对象并更新 DOM 差异对象的数据结构是下面这个样子,与每一个 VDOM 元素一一对应:


{  type,  vdom,  props: [{        type,        key,        value       }]  children}
复制代码


最外层的 type 对应的是 DOM 元素的变化类型,有 4 种:新建、删除、替换和更新。props 变化的 type 只有 2 种:更新和删除。枚举值如下:


const nodePatchTypes = {  CREATE: 'create node',  REMOVE: 'remove node',  REPLACE: 'replace node',  UPDATE: 'update node'}
const propPatchTypes = { REMOVE: 'remove prop', UPDATE: 'update prop'}
复制代码

三、代码实现

我们做一个定时器,500 毫秒运行一次,每次对 state 加 1。页面的 li元素的数量随着 state 而变。


let state = { num: 5 };let timer;let preVDom;
function render(element) { // 初始化的 VD const vdom = view(); preVDom = vdom;
const dom = createElement(vdom); element.appendChild(dom);

timer = setInterval(() => { state.num += 1; tick(element); }, 500);}
function tick(element) { if (state.num > 20) { clearTimeout(timer); return; }
const newVDom = view();}
function view() { return ( <div> Hello World <ul> { // 生成元素为0到n-1的数组 [...Array(state.num).keys()] .map( i => ( <li id={i} class={`li-${i}`}> 第{i * state.num} </li> )) } </ul> </div> );}
复制代码


接下来,通过对比 2 个 VD,生成差异对象。


function tick(element) {  if (state.num > 20) {    clearTimeout(timer);    return;  }
const newVDom = view();
// 生成差异对象 const patchObj = diff(preVDom, newVDom);}
function diff(oldVDom, newVDom) { // 新建 node if (oldVDom == undefined) { return { type: nodePatchTypes.CREATE, vdom: newVDom } }
// 删除 node if (newVDom == undefined) { return { type: nodePatchTypes.REMOVE } }
// 替换 node if ( typeof oldVDom !== typeof newVDom || ((typeof oldVDom === 'string' || typeof oldVDom === 'number') && oldVDom !== newVDom) || oldVDom.tag !== newVDom.tag ) { return { type: nodePatchTypes.REPLACE, vdom: newVDom } }
// 更新 node if (oldVDom.tag) { // 比较 props 的变化 const propsDiff = diffProps(oldVDom, newVDom);
// 比较 children 的变化 const childrenDiff = diffChildren(oldVDom, newVDom);
// 如果 props 或者 children 有变化,才需要更新 if (propsDiff.length > 0 || childrenDiff.some( patchObj => (patchObj !== undefined) )) { return { type: nodePatchTypes.UPDATE, props: propsDiff, children: childrenDiff } }
}}
// 比较 props 的变化function diffProps(oldVDom, newVDom) { const patches = [];
const allProps = {...oldVDom.props, ...newVDom.props};
// 获取新旧所有属性名后,再逐一判断新旧属性值 Object.keys(allProps).forEach((key) => { const oldValue = oldVDom.props[key]; const newValue = newVDom.props[key];
// 删除属性 if (newValue == undefined) { patches.push({ type: propPatchTypes.REMOVE, key }); } // 更新属性 else if (oldValue == undefined || oldValue !== newValue) { patches.push({ type: propPatchTypes.UPDATE, key, value: newValue }); } } )
return patches;}
// 比较 children 的变化function diffChildren(oldVDom, newVDom) { const patches = [];
// 获取子元素最大长度 const childLength = Math.max(oldVDom.children.length, newVDom.children.length);
// 遍历并diff子元素 for (let i = 0; i < childLength; i++) { patches.push(diff(oldVDom.children[i], newVDom.children[i])); }
return patches;}
复制代码


计算得出的差异对象是这个样子的:


{  type: "update node",  props: [],  children: [    null,     {      type: "update node",      props: [],      children: [        null,         {          type: "update node",          props: [],          children: [            null,             {              type: "replace node",              vdom: 6            }          ]        }      ]    },    {      type: "create node",      vdom: {        tag: "li",        props: {          id: 5,          class: "li-5"        },        children: ["第", 30]      }    }  ]}
复制代码


下一步就是遍历差异对象并更新 DOM 了:


function tick(element) {  if (state.num > 20) {    clearTimeout(timer);    return;  }
const newVDom = view();
// 生成差异对象 const patchObj = diff(preVDom, newVDom);
preVDom = newVDom;
// 给 DOM 打个补丁 patch(element, patchObj);}
// 给 DOM 打个补丁function patch(parent, patchObj, index=0) { if (!patchObj) { return; }
// 新建元素 if (patchObj.type === nodePatchTypes.CREATE) { return parent.appendChild(createElement(patchObj.vdom)); }
const element = parent.childNodes[index];
// 删除元素 if (patchObj.type === nodePatchTypes.REMOVE) { return parent.removeChild(element); }
// 替换元素 if (patchObj.type === nodePatchTypes.REPLACE) { return parent.replaceChild(createElement(patchObj.vdom), element); }
// 更新元素 if (patchObj.type === nodePatchTypes.UPDATE) { const {props, children} = patchObj;
// 更新属性 patchProps(element, props);
// 更新子元素 children.forEach( (patchObj, i) => { // 更新子元素时,需要将子元素的序号传入 patch(element, patchObj, i) }); }}
// 更新属性function patchProps(element, props) { if (!props) { return; }
props.forEach( patchObj => { // 删除属性 if (patchObj.type === propPatchTypes.REMOVE) { element.removeAttribute(patchObj.key); } // 更新或新建属性 else if (patchObj.type === propPatchTypes.UPDATE) { element.setAttribute(patchObj.key, patchObj.value); } })}
复制代码


到此为止,整个更新的流程就执行完了。可以看到页面跟我们预期的一样,每 500 毫秒刷新一次,构造渲染树和绘制页面花的时间也非常少。



作为对比,如果我们在生成新的 VD 后,不经过比较,而是直接重新渲染整个 DOM 的时候,会怎样呢?我们修改一下代码:


function tick(element) {  if (state.num > 20) {    clearTimeout(timer);    return;  }
const newVDom = view(); newDom = createElement(newVDom);
element.replaceChild(newDom, dom);
dom = newDom;
/* // 生成差异对象 const patchObj = diff(preVDom, newVDom);
preVDom = newVDom;
// 给 DOM 打个补丁 patch(element, patchObj); */}
复制代码


效果如下:



可以看到,构造渲染树( Rendering)和绘制页面( Painting)的时间要多一些。但另一方面花在 JS 计算( Scripting)的时间要少一些,因为不需要比较节点的变化。如果算总时间的话,重新渲染整个 DOM 花费的时间反而更少,这是为什么呢?


其实原因很简单,因为我们的 DOM 树太简单了!节点很少,使用到的 css 也很少,所以构造渲染树和绘制页面就花不了多少时间。VD 真正的效果还是要在真实的项目中才体现得出来。

四、总结

本文详细介绍如何实现一个简单的 VD Diff 算法,再根据计算出的差异去更新真实的 DOM 。然后对性能做了一个简单的分析,得出使用 VD 在减少渲染时间的同时增加了 JS 计算时间的结论。基于当前这个版本的代码还能做怎样的优化呢,请期待下一篇的内容:《你不知道的 Virtual DOM(三):Virtual DOM 更新优化》


P.S: 想看完整代码见这里,如果有必要建一个仓库的话请留言给我:代码(https://gist.github.com/dickenslian/a0a8d41a88d566d86271de16cd7738f0


2020-03-08 19:241544

评论

发布
暂无评论
发现更多内容

2B营销路径: 9大步骤自我拆解

boshi

营销数字化 七日更

论文免费开源:NB-IoT智慧路灯监控系统

不脱发的程序猿

28天写作 论文 3月日更 NB-IoT智慧路灯 大学生毕业

「面试高频」秒杀架构的设计套路,你值得拥有

我爱娃哈哈😍

架构设计 架构设计实战 秒杀架构

Java面试必看!阿里(嵩山版)分布式核心原理笔记来了

Java架构追梦

Java 阿里巴巴 架构 面试 架构分布式

GO训练营第10周——日志&指标&链路追踪

Glowry

冲击大厂!阿里P9纯手打Java面试小抄(21版)在GitHub上已获80万star

Java 程序员 面试

收藏!Linux常用命令合集

roseduan

Linux

跟我学ModelArts丨探索ModelArts平台个性化联邦学习API

华为云开发者联盟

AI 联邦学习 API 华为云 modelarts

区块链电子发票平台,区块链电子发票优势

13530558032

【前端面试题】关于一些js的一些面试题(金融行业),我和面试官扯了三个小时

孙叫兽

JavaScript 大前端 金融 笔试题

Python 生成 QR 二维码

HoneyMoose

在线数据迁移,数字化时代的必修课 —— 京东云数据迁移实践

京东科技开发者

数据库 数据迁移

LeetCode题解:213. 打家劫舍 II,动态规划(不缓存偷盗状态),JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

使用“零信任”,不惧“内部威胁”!

龙归科技

管理 数据完整性 零信任 内部威胁

MySQL的锁

一个大红包

3月日更

一个合格的CloudNative应用:程序当开源软件编写,应用配置外置

华为云开发者联盟

云原生 华为云 Cloud Native CCE CSE

安卓嵌入式底层开发!整理出这份8万字Android性能优化实战解析,已开源

欢喜学安卓

android 程序员 面试 移动开发

2021最新总结网易/腾讯/CVTE/字节面经分享(附答案解析)

比伯

Java 编程 架构 面试 程序人生

【LeetCode】设计停车系统Java题解

Albert

算法 LeetCode 28天写作 3月日更

Java 和 Python 关于 % 的那些坑

与你一起学算法

Java Python

如何批量下载YouTube视频到本地

科技猫

软件 音视频 经验分享 资源分享 工具分享

EGG Network公链技术创新,EFTalk打造高效全能公链

币圈那点事

区块链

2021 年要了解的 34 种 JavaScript 简写优化技术

LeanCloud

JavaScript 面试 大前端

寻找被遗忘的勇气(十九)

Changing Lin

3月日更

一文搞懂三级管和场效应管驱动电路设计及使用

不脱发的程序猿

28天写作 电路设计 三极管 3月日更 场效应管

智慧党建信息管理平台系统建设

13530558032

拍乐云推出“实时标注”,助力少儿编程、在线金融等行业场景实现高效互动

拍乐云Pano

音视频 在线教育 互动白板 实时标注 在线金融

干货 | 万字详解整个数据仓库设计体系

五分钟学大数据

大数据 数据仓库 28天写作 3月日更

源中瑞智慧社区解决方案,社区服务平台

13530558032

快点来学吧!Android性能优化面试题集锦,深度解析,值得收藏

欢喜学安卓

android 程序员 面试 移动开发

电子门锁没电的解决办法

孙叫兽

生活 程序人生 电子锁

你不知道的virtual DOM(二):Virtual Dom的更新_文化 & 方法_大白_InfoQ精选文章