js虚拟dom实现思路及案例

发布者: xiaozhimn

现在react,vue都采用虚拟dom的设计思路来进行真实dom的局部的刷新,看似这样的实现效果非常创新并且的确降低了操作dom的成本开销,那么今天我们使用一个简单的案例来模拟下实现虚拟dom的思路。

第一步: 模仿react的api定义一个构造虚拟dom的Element方法

function Element({tagName, props, children}){
    if(!(this instanceof Element)){
        return new Element({tagName, props, children})
    }
    this.tagName = tagName;
    this.props = props || {};
    this.children = children || [];
}

第二步: 定义一个用于渲染真实dom的render方法

Element.prototype.render = function(){
    var el = document.createElement(this.tagName),
        props = this.props,
        propName,
        propValue;
    for(propName in props){
        propValue = props[propName];
        el.setAttribute(propName, propValue);
    }
    this.children.forEach(function(child){
        var childEl = null;
        if(child instanceof Element){
            childEl = child.render();
        }else{
            childEl = document.createTextNode(child);
        }
        el.appendChild(childEl);
    });
    return el;
};

第三步: 呈现虚拟dom到真实dom中进行渲染

var elem = Element({
         tagName: 'ul',
         props: {'class': 'list'},
         children: [
                Element({tagName: 'li', children: ['item1']}),
                Element({tagName: 'li', children: ['item2']})
         ]
});
document.querySelector('body').appendChild(elem.render());

第四步:处理dom的diff

DOM更新,无外乎四种情况,如下:
1. 新增节点;
2. 删除节点;
3. 替换节点;
4. 父节点相同,对比子节点.

我们使用深度优先遍历算法来进行虚拟dom的比较代码如下:
function changed(elem1, elem2) {
    return (typeof elem1 !== typeof elem2) || (typeof elem1 === 'string' && elem1 !== elem2) ||     
    (elem1.tagName !== elem2.tagName);//这里的比较没有涉及到props的比较主要用于演示,详细的大家可以自己思考
}
function updateElement($root, newElem, oldElem, index = 0) {
    if (!oldElem){
        $root.appendChild(newElem.render());
    } else if (!newElem) {
        $root.removeChild($root.childNodes[index]);
    } else if (changed(newElem, oldElem)) {
        if (typeof newElem === 'string') {
            $root.childNodes[index].textContent = newElem;
        } else {
            $root.replaceChild(newElem.render(), $root.childNodes[index]);
        }
    } else if (newElem.tagName) {
        let newLen = newElem.children.length;
        let oldLen = oldElem.children.length;
        for (let i = 0; i < newLen || i < oldLen; i++) {
            updateElement($root.childNodes[index], newElem.children[i], oldElem.children[i], i)
        }
    }
}

第五步: 演示一个简单的例子:

    <button id="update">更新元素</button>
    <div id="view"></div>
    function Element({tagName, props, children}){
        if(!(this instanceof Element)){
            return new Element({tagName, props, children})
        }
        this.tagName = tagName;
        this.props = props || {};
        this.children = children || [];
   }
   Element.prototype.render = function(){
        var el = document.createElement(this.tagName),
            props = this.props,
            propName,
            propValue;
        for(propName in props){
            propValue = props[propName];
            el.setAttribute(propName, propValue);
        }
        this.children.forEach(function(child){
            var childEl = null;
            if(child instanceof Element){
                childEl = child.render();
            }else{
                childEl = document.createTextNode(child);
            }
            el.appendChild(childEl);
        });
        return el;
  };
  function changed(elem1, elem2) {
     return (typeof elem1 !== typeof elem2) || (typeof elem1 === 'string' && elem1 !== elem2) ||     
     (elem1.tagName !== elem2.tagName);//这里的比较没有涉及到props的比较主要用于演示,详细的大家可以自己思考
  }
  function updateElement($root, newElem, oldElem, index = 0) {
    if (!oldElem){
        $root.appendChild(newElem.render());
    } else if (!newElem) {
        $root.removeChild($root.childNodes[index]);
    } else if (changed(newElem, oldElem)) {
        if (typeof newElem === 'string') {
            $root.childNodes[index].textContent = newElem;
        } else {
            $root.replaceChild(newElem.render(), $root.childNodes[index]);
        }
    } else if (newElem.tagName) {
        let newLen = newElem.children.length;
        let oldLen = oldElem.children.length;
        for (let i = 0; i < newLen || i < oldLen; i++) {
            updateElement($root.childNodes[index], newElem.children[i], oldElem.children[i], i)
        }
    }
  }    
   var elem = Element({
       tagName: 'ul',
       props: {'class': 'list'},
       children: [
                    Element({tagName: 'li', children: ['item1']}),
                    Element({tagName: 'li', children: ['item2']})
                ]
   });
   var newElem =  Element({
       tagName: 'ul',
       props: {'class': 'list'},
       children: [
                    Element({tagName: 'li', children: ['item1']}),
                    Element({tagName: 'li', children: ['hahaha']})
       ]
  });    
  var $root = document.querySelector('#view');
  var $update= document.querySelector('#update');
  updateElement($root, elem);
  $update.addEventListener('click', () => {
    updateElement($root, newElem, elem);
  });

0赞