从零写代码实现最简单的虚拟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代码应该是:

<ul class="list">
  <li>item1</li>
  <li>item2</li>
</ul>

所以现在的问题是,怎么让虚拟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的相关源码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1.安装 可以简单地在页面引入Vue.js作为独立版本,Vue即被注册为全局变量,可以在页面使用了。 如果希望搭建...
    Awey阅读 11,109评论 4 129
  • 下载安装搭建环境 可以选npm安装,或者简单下载一个开发版的vue.js文件 浏览器打开加载有vue的文档时,控制...
    冥冥2017阅读 6,103评论 0 42
  • 这篇笔记主要包含 Vue 2 不同于 Vue 1 或者特有的内容,还有我对于 Vue 1.0 印象不深的内容。关于...
    云之外阅读 5,082评论 0 29
  • vue.js官网教程学习笔记和学习摘要 起步 安装 一个简单的方法,直接把一个vue.js引入你的HTML页面中,...
    恰皮阅读 3,411评论 2 22
  • 文【忆会飞的鱼】 “肉体不就是拿来用的,又不是拿来伺候的。”“是真的阿,如果你整天伺候你这个皮囊,不会有出息的,只...
    忆会飞的鱼阅读 743评论 0 2