从零写代码实现最简单的虚拟DOM

前言

虚拟DOM是如今MVVM框架必须具备的技术特性,我们今天写一个简单的虚拟DOM实现,来学习它的原理。注意,下方的任何代码逻辑都是简化过的不严谨的,只粗放的表达虚拟DOM的原理。

写一个简单的虚拟DOM对象

虚拟DOM对象的本质是一个JS对象,我先简单写一个:

var vdom = Element({
    tagName: 'ul',
    props: {'class': 'list'},
    children: [
        Element({tagName: 'li', children: ['item1']}),
        Element({tagName: 'li', children: ['item2']})
    ]
});

代码很好理解,有个ul,它class是list,它有2个子元素,都是li,li的子元素都是文本节点,内容一个叫'item1',另一个叫'item2'

但是,这不是HTML代码,HTML代码应该是:

  • item1
  • item2

所以现在的问题是,怎么让虚拟DOM对象转换为DOM。关键点就是这个Element构造函数。

写一个简单的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 || [];
}

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

原理很简单,Element构造函数负责将虚拟DOM对象一层一层的递归解析,每一层都要做这么几个操作:

  1. document.createElement()创建一个节点
  2. setAttribute()设置属性
  3. 遍历children,如果children的某一项也是Element实例,则对这项再来一遍1和2步骤。如果这项是文本节点,则document.createTextNode()

怎么用

先解析,然后插入root元素。

document.querySelector('#root').appendChild(elem.render());

怎么更新虚拟DOM

更新虚拟DOM,也就是用户对虚拟DOM做了操作,操作是有这几种:

  1. 原本空,现在新增节点
  2. 原本有,现在删除节点
  3. 原本有,现在替换节点
  4. 当前节点相同,对比子节点。

注意,vue.js对虚拟DOM的修改的理解要比这个复杂,并不是这么粗放的归为4类,这里只是简化介绍。

写一个更新节点的方法:

$root是根元素,比如body

function isChanged(elem1, elem2) {
    return (typeof elem1 !== typeof elem2) ||
           (typeof elem1 === 'string' && elem1 !== elem2) ||
           (elem1.type !== elem2.type);
}

function updateElement($root, newElem, oldElem, index = 0) {
    if (!oldElem){
        $root.appendChild(newElem.render());
    } else if (!newElem) {
        $root.removeChild($root.childNodes[index]);
    } else if (isChanged(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)
        }
    }
}

注意,由于只为了说明原理,所以这个例子非常简化,没有对节点属性的变化进行处理。你会看到下方class虽然有变化,但是并没有更新。

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-1'},
    children: [
        Element({tagName: 'li', children: ['item1']}),
        Element({tagName: 'li', children: ['hahaha']})
    ]
});

var $root = document.querySelector('#root');
var $refresh = document.querySelector('#refresh');
updateElement($root, elem);
$refresh.addEventListener('click', () => {
    updateElement($root, newElem, elem);
});

当点击按钮,会看到页面有更新。

总结

  1. 虚拟DOM就是一个JS对象
  2. 对DOM的修改会反映到data上,data会反映到新的虚拟DOM上
  3. 新的虚拟DOM会跟老的虚拟DOM做对比,也就是使用diff算法做对比
  4. 最小化修改真实DOM

进阶

如果对虚拟DOM感兴趣,可以学习Vue.js的相关源码。

你可能感兴趣的:(从零写代码实现最简单的虚拟DOM)