vitual-dom原理与简单实现

前言

目前广为人知的React和Vue都采用了virtual-dom,Virtual DOM凭借其高效的diff算法,让我们不再关心性能问题,可以随心所欲的修改数据状态。在实际开发中,我们并不需要关心Virtual DOM是如何实现的,但是理解Virtual DOM的实现原理确实有必要的。本文是参照https://github.com/livoras/simple-virtual-dom源码进行理解vitual DOM。

一、前端应用状态管理

在日益复杂的前端应用中,状态管理是一个经常被提及的话题,从早期的刀耕火种时代到jQuery,再到现在流行的MVVM时代,状态管理的形式发生了翻天覆地的变化,我们再也不用维护茫茫多的事件回调、监听来更新视图,转而使用使用双向数据绑定,只需要维护相应的数据状态,就可以自动更新视图,极大提高开发效率。

但是,双向数据绑定也并不是唯一的办法,还有一个非常粗暴有效的方式:一旦数据发生变化,重新绘制整个视图,也就是重新设置一下innerHTML。这样的做法确实简单、粗暴、有效,但是如果只是因为局部一个小的数据发生变化而更新整个视图,性价比未免太低了,而且,像事件,获取焦点的输入框等,都需要重新处理。所以,对于小的应用或者说局部的小视图,这样处理完全是可以的,但是面对复杂的大型应用,这样的做法不可取。所以我们可以采取用JavaScript的方法来模拟DOM树,用新渲染的对象树去和旧的树进行对比,记录下变化的变化,然后应用到真实的DOM树上,这样我们只需要更改与原来视图不同的地方,而不需要全部重新渲染一次。这就是virtual-DOM的优势

二、视图渲染

相对于DOM对象,原生的JavaScript对象处理得更快,而且简单。DOM树上的结构,属性信息我们都能通过JavaScript进行表示出来,例如:

var element = {
    tagName: 'ul', // 节点标签名
    props: { // dom的属性键值对
        id: 'list'
    },
    children: [
        {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
        {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
        {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]}
    ]
}

那么在html渲染的结果就是:

<ul id="list">
    <li class="item">Item 1</li>
    <li class="item">Item 2</li>
    <li class="item">Item 3</li>
</ul>

既然能够通过JavaScript表示DOM树的信息,那么就可以通过使用JavaScript来构建DOM树。

然而光是构建DOM树,没什么卵用,我们需要将JavaScript构建的DOM树渲染到真实的DOM树上,用JavaScript表现一个dom一个节点非常简单,我们只需要记录他的节点类型,属性键值对,子节点:

function Element(tagName, props, children) {
    this.tagName = tagName
    this.props = props
    this.children = children
}

那么ul标签我们就可以使用这种方式来表示

var ul = new Element('ul', {id: 'list'}, [
    {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]}
])

说了这么多,他只是用JavaScript表示的一个结构,那该如何将他渲染到真实的DOM结构中呢:

Element.prototype.render = function() {
    let el = document.createElement(this.tagName), // 节点名称
        props = this.props // 节点属性

    for (var propName in props) {
        propValue = props[propName]
        el.setAttribute(propName, propValue)
    }

    this.children.forEach((child) => {
        var childEl = (child instanceof Element)
            ? child.render()
            : document.createTextNode(child)
        el.appendChild(childEl)
    })
    return el
}

如果我们想将ul渲染到DOM结构中,就只需要

ulRoot = ul.render()
document.appendChild(ulRoot)

这样就完成了ul到DOM的渲染,也就有了真正的DOM结构

<ul id="list">
    <li class="item">Item 1</li>
    <li class="item">Item 2</li>
    <li class="item">Item 3</li>
</ul>

三、比较虚拟DOM树的差异

React的核心算法是diff算法(这里指的是优化后的算法)我们来看看diff算法是如何实现的:

633390461-5a1fcb9842d23_articlex.png

diff只会对相同颜色方框内的DOM节点进行比较,即同一个父节点下的所有子节点。当发现节点不存在,则该节点和子节点会被完全删除,不会做进一步的比较。

在实际的代码中,会对新旧两棵树进行深度的遍历,给每一个节点进行标记。然后在新旧两棵树的对比中,将不同的地方记录下来。

// diff 算法,对比两棵树
function diff(oldTree, newTree) {
    var index = 0   // 当前节点的标志
    var patches = {} // 记录每个节点差异的地方
    dfsWalk(oldTree, newTree, index, patches)
    return patches
}
function dfsWalk(oldNode, newNode, index, patches) {
    // 对比newNode和oldNode的差异地方进行记录
    patches[index] = [...]

    diffChildren(oldNode.children, newNode.children, index, patches)
}
function diffChildren(oldChildren, newChildren, index, patches) {
    let leftNode = null
    var currentNodeIndex = index
    oldChildren.forEach((child, i) => {
        var newChild = newChildren[i]
        currentNodeIndex =  (leftNode && leftNode.count) // 计算节点的标记
                ? currentNodeIndex + leftNode.count + 1
                : currentNodeIndex + 1
        dfsWalk(child, newChild, currentNodeIndex, patches) // 遍历子节点
        leftNode = child
    })
}

例如:


1799022089-5a1fcbe828b58_articlex.png

在图中如果div有差异,标记为0,那么:

patches[0] = [{difference}, {difference}]

同理,有p是patches[1], ul是patches[3],以此类推
patches指的是差异变化,这些差异包括:1、节点类型的不同,2、节点类型相同,但是属性值不同,文本内容不同。所以有这么几种类型:

var REPLACE = 0,    // replace 替换
    REORDER = 1,    // reorder 父节点中子节点的操作
    PROPS   = 2,    // props 属性的变化
    TEXT    = 3     // text 文本内容的变化

如果节点类型不同,就说明是需要替换,例如将div替换成section,就记录下差异:

patches[0] = [{
    type: REPLACE,
    node: newNode // section
},{
    type: PROPS,
    props: {
        id: 'container'
    }
}]

四、将差异应用到DOM树上

在标题二中构建了真正的DOM树的信息,所以先对那一棵DOM树进行深度优先的遍历,遍历的时候同
patches对象进行对比,找到其中的差异,然后应用到DOM操作中。

function patch(node, patches) {
    var walker = {index: 0} // 记录当前节点的标志
    dfsWalk(node, walker, patches)
}

function dfsWalk(node, walker, patches) {
    var currentPatches = patches[walker.index] // 这是当前节点的差异

    var len = node.childNodes
        ? node.childNodes.length
        : 0

    for (var i = 0; i < len; i++) { // 深度遍历子节点
        var child = node.childNodes[i]
        walker.index++
        dfsWalk(child, walker, patches)
    }

    if (currentPatches) {
        applyPatches(node, currentPatches) // 对当前节点进行DOM操作
    }
}
// 将差异的部分应用到DOM中
function applyPatches(node, currentPatches) {
    currentPatches.forEach((currentPatch) => {
        switch (currentPatch.type) {
            case REPLACE:
                var newNode = (typeof currentPatch.node === 'string')
                    ? document.createTextNode(currentPatch.node)
                    : currentPatch.node.render()
                node.parentNode.replaceChild(newNode, node)
                break;
            case REORDER:
                reorderChldren(node, currentPatch.moves)
                break
            case PROPS:
                setProps(node, currentPatch.props)
                break
            case TEXT:
                if (node.textContent) {
                    node.textContent = currentPatch.content
                } else {
                    node.nodeValue = currentPatch.content
                }
                break
            default:
                throw new Error('Unknown patch type ' + currentPatch.type)
        }
    })
}

这次的粗糙的virtual-dom基本已经实现了,具体的情况更加复杂。但这已经足够让我们理解virtual-dom。
具体的带解析的代码已经上传到github

五、 References

https://www.cnblogs.com/justany/archive/2015/04/08/4401118.html
https://github.com/livoras/blog/issues/13
https://github.com/y8n/blog/issues/5
https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060
http://www.infoq.com/cn/articles/react-dom-diff

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • 参考文章:深度剖析:如何实现一个Virtual DOM 算法 作者:戴嘉华React中一个没人能解释清楚的问题——...
    waka阅读 5,951评论 0 21
  • 目录: 1 前言 2 对前端应用状态管理思考 3 Virtual DOM 算法 4 算法实现4.1 步骤一:用JS...
    RThong阅读 388评论 0 2
  • 1.为什么需要虚拟DOM DOM是很慢的,其元素非常庞大,页面的性能问题由JS引起的,大部分都是由DOM操作引起的...
    Ecl_02b8阅读 2,202评论 1 4
  • React的贡献在于它提出的组件化、虚拟DOM、帮助整个前端进入工程化、将函数式编程的思想带给前端。这里记录一下学...
    婷楼沐熙阅读 357评论 0 0
  • 你没有读懂现实,却说现实很残酷(来自:你没有读明白尘世,却说尘世是个骗子) 不能认同大多数认同的大多数
    小茞阅读 231评论 0 1