美文网首页
如何自己写简单的virtual dom(读博客笔记)

如何自己写简单的virtual dom(读博客笔记)

作者: 小王啊_ | 来源:发表于2017-07-24 14:40 被阅读0次

    原文

    正常的dom

    <ul class=”list”>
      <li>item 1</li>
      <li>item 2</li>
    </ul>
    

    用js的object来代表dom

    {
        type: 'ul', props: {'class': 'list'}, children: [
            {type: 'li', props: {}, children: ['item 1']},
            {type: 'li', props: {}, children: ['item 2']}
        ]
    }
    

    写个帮助方法创建js的dom

    function helper(type, props, ...children) {
        return {type, props, children};
    }
    
    

    现在就可以这样写:

    helper('ul', {'class': 'list'}, 
        helper('li', {}, 'item 1'),
        helper('li', {}, 'item 2')
    )
    

    可以通过babel 来转换jsx

    实现从我们的js的object到真实dom

    function createElement(node) {
        if (typeof node == 'string') {
            return document.createTextNode(node);
        }
        $el = document.createElement(node.type);
        node.children
            .map(createElement)
            .forEach($el.appendChild.bind($el));
        return $el; 
    }
    

    接下来处理diff

    有四种情况

    • 新增
    // old
    <ul>
        <li>item 1</li>
    </ul>
    // new 
    <ul>
        <li>item 1</li>
        <li>item 2</li>
    </ul>
    
    • 删除
    // old
    <ul>
        <li>item 1</li>
        <li>item 2</li>
    </ul>
    // new 
    <ul>
        <li>item 1</li>
    </ul>
    
    • 替换
    // old
    <div>
        <p>item 1</p>
        <button>cpck it</button>
    </div>
    // new 
    <div>
        <p>item 1</p>
        <p>hello</p>
    </div>
    
    • 节点一致,子节点不一致
    // old
    <ul>
        <li>item 1</li>
        <li>
            <span>hello</span>
            <div>hi!</div>
        </li>
    </ul>
    // new 
    <ul>
        <li>item 1</li>
        <li>
            <span>hello</span>
            <span>hi!</span>
        </li>
    </ul>
    

    所以我们可以写一个更新函数,接收三个参数,$parent、newNode、oldNode, 其中$parent是真实dom元素,并且是虚拟节点的父节点。(暂时不考虑props)

    当无新节点或者旧节点时

    function updateElement($parent, newNode, oldNode,  index = 0) {
        // 无旧节点
        if (!oldNode) {
            $parent.appendChild(newNode);
        // 无新节点
        } else if (!newNode) {
            $parent.removeChild(
                $parent.childNodes[index];
            );
        }
    }
    

    有新节点和旧节点时,需要判断节点是否改变,所以我们可以先写一个判断节点是否改变的函数。

    function changed(node1, node2) {
                // 基础数据类型判断
        return typeof node1 !== typeof node2 ||
                // 文本节点时是否一致
               typeof node1 == 'string' && node1 !== node2 ||
               // 元素节点时类型是否一致
               node1.type !== node2.type;
    }
    

    那么现在我们就可以完善一下 updateElement 函数:

    function updateElement($parent, newNode, oldNode,  index = 0) {
        // 无旧节点
        if (!oldNode) {
            $parent.appendChild(newNode);
        // 无新节点
        } else if (!newNode) {
            $parent.removeChild(
                $parent.childNodes[index];
            );
            // 新旧节点发生变化时
        } else if (changed(newNode, oldNode)) {
            $parent.replaceChild(
                createElement(newNode), 
                $parent.childNodes[index];
            )
        }
    }
    

    最后不过也非常重要的事情

    我们在对比节点时,需要保证它们的子节点也需要对比,才能判断他们的差异。在写代码之前我们需要考虑以下几个问题:

    1. 我们只需要对比元素节点而不用对比文本节点(文本节点无子节点);
    2. 我们把现在这个节点当做父节点;
    3. 我们需要一个一个节点对比,甚至是undefined,我们函数中需要有能应对undefined这种情况的能力;
    4. index只是子节点的索引。

    考虑到以上,我们可以继续完善 updateElement 函数:

    function updateElement($parent, newNode, oldNode,  index = 0) {
        // 无旧节点
        if (!oldNode) {
            $parent.appendChild(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 len = newNode.children.length > oldNode.children.length ? newNode.children.length : oldNode.Children.length;
            for (var i = 0; i<len; i++) {
                updateElement(
                    $parent.childNodes[index],
                    newNode.childNodes[i],
                    oldNode.childNodes[i],
                    i
                );
            }
        }
    }
    

    现在我们从整体来看

    // index.html
    <button id="reload">RELOAD</button>
    <div id="root"></div>
    

    js(babel+jsx)

    function createElement(node) {
        if (typeof node == 'string') {
            return document.createTextNode(node);
        }
        $el = document.createElement(node.type);
        node.children
            .map(createElement)
            .forEach($el.appendChild.bind($el));
        return $el; 
    }
    
    function changed(node1, node2) {
                // 基础数据类型判断
        return typeof node1 !== typeof node2 ||
                // 文本节点时是否一致
               typeof node1 == 'string' && node1 !== node2 ||
               // 元素节点时类型是否一致
               node1.type !== node2.type;
    }
    
    function updateElement($parent, newNode, oldNode,  index = 0) {
        // 无旧节点
        if (!oldNode) {
            $parent.appendChild(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 len = newNode.children.length > oldNode.children.length ? newNode.children.length : oldNode.Children.length;
            for (var i = 0; i<len; i++) {
                updateElement(
                    $parent.childNodes[index],
                    newNode.childNodes[i],
                    oldNode.childNodes[i],
                    i
                );
            }
        }
    }
    
    const a = (
      <ul>
        <li>item 1</li>
        <li>item 2</li>
      </ul>
    );
    
    const b = (
      <ul>
        <li>item 1</li>
        <li>hello!</li>
      </ul>
    );
    
    const $root = document.getElementById('root');
    const $reload = document.getElementById('reload');
    
    updateElement($root, a);
    
    $reload.addEventListener('click', () => {
        updateElement($root, a, b);
    })
    
    

    总结

    我们到现在已经基本完成了 Virtual Dom 的简单实现,通过这我们应该能够了解 Virtual Dom 的基本原理,和了解 React 内部基本原理。

    在这篇文章中我们还有一些我们没完成的东西,如下:

    • 设置节点的属性,并且计算差别和更新它们;
    • 节点的事件监听;
    • 让我们的 Virtual Dom 和组件工作,比如 React;
    • 拿到真实的Dom的引用;
    • 支持其它库直接操作真实DOM;
    • 其它...

    相关文章

      网友评论

          本文标题:如何自己写简单的virtual dom(读博客笔记)

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