博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
你不知道的Virtual DOM(三):Virtual Dom更新优化
阅读量:6701 次
发布时间:2019-06-25

本文共 6757 字,大约阅读时间需要 22 分钟。

欢迎关注我的公众号睿Talk,获取我最新的文章:

clipboard.png

一、前言

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

这是VD系列文章的第三篇,以下是本系列其它文章的传送门:

本文基于本系列文章的第二篇,对VD的比较过程进行优化。

二、优化一:省略patch对象,直接更新dom

在上一个版本的代码里,我们是通过在diff过程中生成patch对象,然后在利用这个对象更新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);}

实际上这步是多余的。既然在diff的时候就已经知道要如何操作dom了,那为什么不直接在diff里面更新呢?先来回顾下之前的diff代码:

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            }           }            }}

diff最终返回的对象是这个数据结构:

{    type,    vdom,    props: [{               type,               key,               value             }]    children}

现在,我们把生成对象的步骤省略掉,直接操作dom。这时候我们需要将父元素,还有子元素的索引传进来(原patch的逻辑):

function diff(oldVDom, newVDom, parent, index=0) {    // 新建node    if (oldVDom == undefined) {        parent.appendChild(createElement(newVDom));    }    const element = parent.childNodes[index];    // 删除node    if (newVDom == undefined) {        parent.removeChild(element);    }    // 替换node    if (        typeof oldVDom !== typeof newVDom ||        ((typeof oldVDom === 'string' || typeof oldVDom === 'number') && oldVDom !== newVDom) ||        oldVDom.tag !== newVDom.tag    ) {        parent.replaceChild(createElement(newVDom), element);    }    // 更新node    if (oldVDom.tag) {        // 比较props的变化        diffProps(oldVDom, newVDom, element);        // 比较children的变化        diffChildren(oldVDom, newVDom, element);    }}function diffProps(oldVDom, newVDom) {    const allProps = {...oldVDom.props, ...newVDom.props};    // 获取新旧所有属性名后,再逐一判断新旧属性值    Object.keys(allProps).forEach((key) => {            const oldValue = oldVDom.props[key];            const newValue = newVDom.props[key];            // 删除属性            if (newValue == undefined) {                element.removeAttribute(key);            }             // 更新属性            else if (oldValue == undefined || oldValue !== newValue) {                element.setAttribute(key, newValue);            }        }    )}function diffChildren(oldVDom, newVDom, parent) {    // 获取子元素最大长度    const childLength = Math.max(oldVDom.children.length, newVDom.children.length);    // 遍历并diff子元素    for (let i = 0; i < childLength; i++) {        diff(oldVDom.children[i], newVDom.children[i], parent, i);    }}

本质上来说,这次的优化是将patch的逻辑整合进diff的过程中了。经过这次优化,JS计算的时间快了那么几毫秒。虽然性能的提升不大,但代码比原来的少了80多行,降低了逻辑复杂度,优化的效果还是不错的。

clipboard.png

三、优化二:VD与真实dom融合

在之前的版本里面,diff操作针对的是新旧2个VD。既然真实的dom已经根据之前的VD渲染出来了,有没办法用当前的dom跟新的VD做比较呢?

答案是肯定的,只需要按需获取dom中不同的属性就可以了。比如,当比较tag的时候,使用的是nodeType和tagName,比较文本的时候用的是nodeValue。

function tick(element) {    if (state.num > 20) {        clearTimeout(timer);        return;    }    const newVDom = view();    // 比较并更新节点    diff(newVDom, element);    // diff(preVDom, newVDom, element);    // preVDom = newVDom;}function diff(newVDom, parent, index=0) {        const element = parent.childNodes[index];    // 新建node    if (element == undefined) {        parent.appendChild(createElement(newVDom));        return;    }    // 删除node    if (newVDom == undefined) {        parent.removeChild(element);        return;    }    // 替换node    if (!isSameType(element, newVDom)) {        parent.replaceChild(createElement(newVDom), element);        return;    }    // 更新node    if (element.nodeType === Node.ELEMENT_NODE) {        // 比较props的变化        diffProps(newVDom, element);        // 比较children的变化        diffChildren(newVDom, element);    }}// 比较元素类型是否相同function isSameType(element, newVDom) {    const elmType = element.nodeType;    const vdomType = typeof newVDom;    // 当dom元素是文本节点的情况    if (elmType === Node.TEXT_NODE &&         (vdomType === 'string' || vdomType === 'number') &&        element.nodeValue == newVDom    ) {       return true;     }    // 当dom元素是普通节点的情况    if (elmType === Node.ELEMENT_NODE && element.tagName.toLowerCase() == newVDom.tag) {        return true;    }    return false;}

为了方便属性的比较,提高效率,我们将VD的props存在dom元素的__preprops_字段中:

const ATTR_KEY = '__preprops_';// 创建dom元素function createElement(vdom) {    // 如果vdom是字符串或者数字类型,则创建文本节点,比如“Hello World”    if (typeof vdom === 'string' || typeof vdom === 'number') {        return doc.createTextNode(vdom);    }    const {tag, props, children} = vdom;    // 1. 创建元素    const element = doc.createElement(tag);    // 2. 属性赋值    setProps(element, props);    // 3. 创建子元素    children.map(createElement)            .forEach(element.appendChild.bind(element));    return element;}// 属性赋值function setProps(element, props) {     // 属性赋值    element[ATTR_KEY] = props;    for (let key in props) {        element.setAttribute(key, props[key]);    }}

进行属性比较的时候再取出来:

// 比较props的变化function diffProps(newVDom, element) {    let newProps = {...element[ATTR_KEY]};    const allProps = {...newProps, ...newVDom.props};    // 获取新旧所有属性名后,再逐一判断新旧属性值    Object.keys(allProps).forEach((key) => {            const oldValue = newProps[key];            const newValue = newVDom.props[key];            // 删除属性            if (newValue == undefined) {                element.removeAttribute(key);                delete newProps[key];            }             // 更新属性            else if (oldValue == undefined || oldValue !== newValue) {                element.setAttribute(key, newValue);                newProps[key] = newValue;            }        }    )    // 属性重新赋值    element[ATTR_KEY] = newProps;}

通过这种方式,我们不再需要用变量preVDom将上一次生成的VD存下来,而是直接跟真实的dom进行比较,灵活性更强。

四、总结

本文基于上一个版本的代码,简化了页面渲染的过程(省略patch对象),同时提供了更灵活的VD比较方法(直接跟dom比较),可用性越来越强了。基于当前这个版本的代码还能做怎样的优化呢,请看下一篇的内容:。

P.S.: 想看完整代码见这里,如果有必要建一个仓库的话请留言给我:

转载地址:http://wswlo.baihongyu.com/

你可能感兴趣的文章
Android 初始化Menu item的值(ActionBar篇)
查看>>
Facebook和Google服务全球大瘫痪,网友:全世界只剩下推特 ...
查看>>
不让「数据孤岛」成为 AI 发展的绊脚石,「联邦学习」将成突破口? ...
查看>>
阿里云大数据ACP认证知识点梳理9——产品特点(DATA WORKS) ...
查看>>
首发 | 完成近亿美元B轮融资,黑芝麻如何成为自动驾驶芯片破局者?
查看>>
AI通过儿童眼球运动,筛查胎儿酒精谱系障碍
查看>>
【资料下载】Python第四讲——使用IPython/Jupyter Notebook与日志服务玩转超大规模数据分析与可视化...
查看>>
用不到 30 行 Python 代码实现 YOLO
查看>>
云HBase备份恢复,为云HBase数据安全保驾护航
查看>>
Ubuntu16.04上通过kubeadm安装指定版本Kubernetes1.9.0
查看>>
Intellij IDEA Jrebel Plugin 激活服务
查看>>
用Mockplus教你使用属性面板的设置交互状态
查看>>
让旧手机运行 Android O? 看看 Android Go 是如何做到的
查看>>
Zabbix SNMP监控安装、配置与服务器实例(学习笔记六)
查看>>
CocoaPods 1.7.0 Beta 发布,Xcode 依赖库管理
查看>>
学习讲述
查看>>
一个优秀的CQRS框架Reveno
查看>>
GoLang并发控制(下)
查看>>
Ansible window自动化运维手册*
查看>>
安装Linux vCenter步骤
查看>>