虚拟DOM

作者: 5d18ee6b5b1c | 来源:发表于2018-06-24 13:50 被阅读0次

    随处可见的VDOM

    VDOM,也叫虚拟DOM,并不是什么高大上的新事物,它是仅存于内存中的DOM,因为还未展示到页面中,所以称为VDOM。

    var a = document.createElement("div");
    

    如上所示,大家对此应该不陌生吧?没错,这就是VDOM。

    问题来了,如果让VDOM变成真实的DOM呢?

    其实很简单……只需将节点append到页面中

    var a = document.createElement("div");
    document.body.append(a);
    

    所以,请大家不要把VDOM想得太复杂!它随处可见~

    React中的VDOM

    常见的DOM操作

    在讲React中的VDOM前,有必要说下,我们日常中常见的DOM操作有哪些?

    事实上,就三类:增、删、改。对应的DOM操作如下:

    1. 增加一个节点 => appendChild
    2. 删除一个节点 => removeChild
    3. 更改一个节点 => replaceChild

    现实中,很多前端小伙伴在处理前端模板变动时,是简单粗暴的,不管是哪种情况,都会直接使用类似jQuery的html方法整块替换~(全局搜下你代码,是不是有不少$(...).html())

    这样做有什么问题呢?——性能问题。如果页面比较小,问题还不是很大,如果页面庞大,这样做势必会出现卡顿,用户体验绝对是不好的。

    如何解决呢?——这就引入了差量更新!

    差量更新

    什么是差量更新?就是只对局面的HTML片段进行更新。比如你加了一个节点,那么我就只更新这个节点,我无需整个模板替换。

    这样一来,效率就提高了。

    可问题来了,我怎么知道哪个节点更新了,哪个节点删除了,哪个节点替换了呢?——我们需要对DOM建模!

    VDOM建模

    说是建模,简单点说就是用一个JS对象来表示VDOM。

    如果我们可以用一个JS对象来表示VDOM,那么这个对象上多一个属性(增加节点),少一个属性(删除节点),或者属性值变了(更改节点),就一目了然了!

    那如何建模呢?

    这个麽!我们就要化繁为简了。思考下,DOM也叫DOM树,是一个树形结构,DOM树上有很多元素节点。

    我们要对VDOM进行建模,本质上就是对一个个元素节点进行建模,然后再把节点放回DOM树的指定位置,这样不就完成对DOM树的建模了么?

    别把建模想得太复杂,无非就是用JS对象的形式来展示罢了。

    如何对元素节点进行建模呢?

    这个简单,我们不难发现,每个节点无非都是由以下三部分组成:

    1. type : 元素类型
    2. props : 元素属性
    3. children : 子元素集合

    比如<div id="main">test</div>,type是div,props是id="main",children是“test”。

    我们希望的结果是:

    {type:"div",props:{"id":"main"},children:[
           test
    ]}
    

    如果是更复杂的结构,比如div中有一个图片,我们可以写成

    {type:"div",props:{"style":""},children:[
            {type:"img",props:{"src":"..."}}
        ]}
    

    以上也是React对VDOM建模的结果。是不是很简单呢?

    如何快捷建模?

    如何把真实的DOM,转化成建模后的VDOM呢?

    这个简单,transform-react-jsx已经帮我们实现了,使用webpack或者rollup的朋友可以直接使用这个插件。

    以下附加rollup的配置文件,供参考:

    import babel from 'rollup-plugin-babel';
    
    export default {
        input : 'src/main.js',
        output : {
            file : 'dist/main.js',
            format : 'cjs'
        },
        banner : "/* fed123.com */",
        plugins : [
            babel({
                'presets' : [[
                    'env',
                    {
                        modules : false
                    }
                ]],
                "plugins" : [
                    ["transform-react-jsx" , {
                        "pragma" : "vnode"
                    }]
                ]
            })
        ]
    }
    

    可能你还是一知半解,下面给出一个例子:

    // React 常见的DOM写法
    const vdom = (
        <div id="_Q5" style="border:1px solid red">
            <div style="text-align:center;">
                <img src="https://m.baidu.com/static/index/plus/plus_logo.png" height="56"/>
            </div>
            Hello
        </div>
    );
    
    // 转义后的
    var vdom = vnode(
        "div",
        { id: "_Q5", style: "border:1px solid red" },
        vnode(
            "div",
            { style: "text-align:center;" },
            vnode("img", { src: "https://m.baidu.com/static/index/plus/plus_logo.png", height: "56", onClick: function onClick() {
                    alert(1);
                } })
        ),
        "Hello"
    );
    
    如何将VDOM变成真实DOM呢?

    我们知道,将DOM变成VDOM,是为了差量更新,最终我们还是要把VDOM还原成DOM的!VDOM只是个桥梁,如果不能还原成DOM,VDOM就没意义了!

    怎么做呢?

    可以参考如下代码:

     // 把vdom挂载到页面上
     function createElement(node) {
        if (typeof node === 'string') {
            return document.createTextNode(node);
        }
        const $el = document.createElement(node.type);
        let appendChild = $el.appendChild.bind($el);
        node.children
            .map(createElement)
            .map(appendChild);
        return $el;
    }
    

    我们判断,如果是子节点是个字符串节点,直接插入页面即可,如果子节点是个DOM节点,那么就递归调用~

    通过这个思路,我们就可以将VDOM还原成DOM了。

    DIFF Virtual DOM & Update

    以上,是VDOM的准备工作,主要包括两个步骤:

    1. 对VDOM进行建模,方便后续的差量更新
    2. 将VDOM转成真实的DOM

    接下来才是主菜。

    我们先看思考下,如何判断DOM发生了变化,并找到这个变化?

    DIFF算法

    DIFF算法是React框架采用的方法。也就是判断DOM是否发生了变化、然后找到这个变化,这样我们才能实现差量更新。

    DOM的变化主要有三种:appendChild、replaceChild、removeChild.

    还记得我们对VDOM的建模么?

    {type:"div",props:{"style":""},children:[
            {type:"img",props:{"src":"..."}}
        ]}
    

    每个节点都包含一个children,DIFF的过程,其实也是diff children的过程。通过递归children的方式,就可以判断不同的children并对其操作。有以下几种情况:

    1. 没有旧的节点,则创建新的节点,并插入父节点。
    2. 如果没有新的节点,则摧毁旧的节点。
    3. 如果节点发生了变化,则用replaceChild改变节点信息
    4. 如果节点没有变化,则对比该节点的子节点进行判断,使用递归调用
    
    function updateElement($parent, newNode, oldNode, index = 0) {
        if(!oldNode) {
            $parent.appendChild(
                createElement(newNode)
            );
        } else if (!newNode) {
            $parent.removeChild(
                $parent.childNodes[index]
            );
        } else if (changed(newNode, oldNode)) {
            $parent.replaceChild(
                createElement(newNode),
                $parent.childNodes[index]
            );
        } else if(newNode.type) {
            const newLength = newNode.children.length;
            const oldLength = oldNode.children.length;
            for(let i = 0; i < newLength || i < oldLength; i++) {
                updateElement(
                    $parent.childNodes[index],
                    newNode.children[i],
                    oldNode.children[i],
                    i
                );
            }
        }
    }
    

    为什么要DIFF children呢?因为我们必须DOM树是由一个个元素节点组成的,DOM树变化的最小单位也是元素节点。

    通过递归的方式,我们就可以从最底层的children开始,层层遍历,找到变化的节点,然后对这些节点差量更新了。

    而所谓的差量更新,就是上述提到的三种操作:appendChild、replaceChild、removeChild。这在上面的代码都有体现到。

    Handle Props & Event

    通过上述的步骤,我们就可以把DOM树进行差量更新并呈现到页面上,但我们知道,DOM树可不只有节点,还有参数跟事件,所以我们需要把参数跟事件加上。

    再看一眼我们对VDOM的建模!

    {type:"div",props:{"style":""},children:[
            {type:"img",props:{"src":"..."}}
        ]}
    

    我们要做的,就是把props加载到对应的元素节点上,这个步骤简称:DIFF props。

    DIFF props,同DIFF VDOM,找到props的不同,然后setAttribute跟removeAttribute。

    这里直接上代码:

    function updateProps ($target, newProps, oldProps = {}){
        const props = Object.assign({},oldProps, newProps);
        Object.keys(props).forEach(name => {
            updateProp($target, name, newProps[name], oldProps[name]);
        })
    }
    function updateProp ($target, name, newVal, oldVal) {
        if (!newVal) {
            removeProp($target, name, oldVal);
        } else if (!oldVal || newVal !== oldVal) {
            setProp($target, name, newVal);
        }
    }
    function setProp ($target, name, value) {
        if (typeof value === "boolean") {
            handleBooleanProp($target, name, value);
        }
        $target.setAttribute(name, value);
    }
    
    function setBooleanProp($target, name, value) {
        if (!!value) {
            $target.setAttribute(name, value);
            $target[name] = true;
        } else {
            $target[name] = false;
        }
    }
    
    function removeProp($target, name, value) {
        if (typeof value === 'boolean') {
            $target[name] = false;
        } 
        $target.removeAttribute(name);
    }
    

    项目源码地址

    相关文章

      网友评论

          本文标题:虚拟DOM

          本文链接:https://www.haomeiwen.com/subject/hyejyftx.html