阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

你不知道的 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:241404

评论

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

WebGPU小白入门(一): 零基础创建第一个WebGPU项目

Orillusion

开源 WebGL 元宇宙 Metaverse webgpu

「前端CI/CD系列」第三篇:如何用建木CI构建前端项目并部署到CDN

Jianmu

开源 前端 CDN 七牛云 建木CI

基于冬奥示范效应,数字孪生将助力建筑运维和集会安全运营

易观分析

数字孪生

GDP Streaming RPC 设计

百度Geek说

后端 RPC Go 语言

眼影、口红、香水…特别的日子献给所有的她 | InfoQ 会员周女神节特别限定活动

InfoQ写作社区官方

热门活动 InfoQ会员周 38妇女节

电商秒杀系统

tony

「架构实战营」

【案例】替代进口数仓,星环科技助力北京银行建设新一代大数据平台

星环科技

数据库

开源云 IDE 产品新宠儿,如何使用 Gitpod 开发 APISIX?

API7.ai 技术团队

ide 开发工具 Apache APISIX

Flink 流处理在中信建投证券的实践与应用

Apache Flink

大数据 flink 开源 编程 实时计算

DPDK uio 分析 丨DPDK的优势及学习总结

Linux服务器开发

Linux服务器开发 DPDK Linux后台开发 高性能网络 网络虚拟化

堪比JMeter的.Net压测工具 - Crank 入门篇

MASA技术团队

C# .net 微软 测试 压测

医疗数字化,星环科技ArgoDB+KunDB统一分布式数据库解决方案来了

星环科技

数据库 医疗安全

有Python基础后,3天就拿Flask开发项目系列博客之一

梦想橡皮擦

3月月更

在华外企高管谈政府工作报告:共享发展成就 未来机遇可期

科技新消息

专注自主研发,加速大数据基础软件国产化进程

星环科技

数据库 大数据 基础软件

两天两夜,1M图片优化到100kb!

沉默王二

Java

【51单片机】keil5如何创建工程

謓泽

单片机 3月月更 keil5

3个案例,详解如何选择合适的研发模式 | 研发效能提升36计

阿里云云效

阿里云 云原生 研发团队 研发 研发提效

金融数据查询增速三倍,服务器成本减半,海尔云链的 OLAP 引擎选型之路

StarRocks

数据库 数据分析 OLAP StarRocks

对容器在野安全问题的观测和分析

腾讯安全云鼎实验室

网络安全 容器安全 在野攻击

「国产替代」,真的是中国SaaS的发展路径吗?

ToB行业头条

毕业总结

施正威

适用于企业的销售自动化CRM系统

低代码小观

销售管理 CRM CRM系统 客户关系管理系统 企业管理软件

【愚公系列】2022年03月 Docker容器 Windows11安装Docker Desktop

愚公搬代码

3月月更

技术平台&应用开发专题月 | 如何打造强大的K8S集群

用友BIP

用友 用友iuap

直播预告 | PolarDB-X 动手实践系列——如何在 PolarDB-X 中优化慢 SQL

阿里云数据库开源

数据库 大数据 阿里云 开源 polarDB

技术分享会回顾|Rust在量化领域如何应用?

非凸科技

以开发之名|致敬女性开发者 用“代码”创造无限可能

最新动态

一日为期,极行千里 ——「企业级零代码黑客马拉松」正式启动报名

明道云

低代码 零代码 企业 黑客马拉松

重学设计模式——你真的面向对象了吗?

黄林晴

设计模式

基于 Apache ShardingSphere 构建高可用分布式数据库

SphereEx

Apache 开源 分布式 ShardingSphere SphereEx

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