Virtual DOM 浅析

Virtual DOM是React中的一个很重要的概念,在日常开发中,前端工程师们需要将后台的数据呈现到界面中,同时要能对用户的操作提供反馈,作用到UI上。这些都离不开DOM操作(还不清楚DOM是什么的也就不用往后看了。。),但是我们知道,频繁的DOM操作会造成极大的资源浪费,也通常是性能瓶颈的原因。于是React 引入了Virtual DOM

为什么我们需要Vitaul DOM?

前言中已经说到,引入Virtual DOM的原因是,避免频繁的DOM操作。那DOM操作的性能到底是消耗到哪去了呢?我所查资料的观点普遍是以下两个方面:

  • js访问DOM
  • DOM操作引起的浏览器的重绘重排操作

抱着实践出真知的态度,我先写了以下两个demo进行测试:

第一次对比试验:

1000次访问DOM
1000次访问DOM
1000次字符串计算,1次访问DOM
1000次字符串计算,1次访问DOM

第一张图中,访问了1000次DOM,并对其属性进行了操作(不会引起重绘重排),第二张图,访问了1次DOM,但进行了1000次字符串操作。但结果是相差甚大,并且1000次如此简单的DOM操作就消耗了6.315ms,这显然是不能接受的。接下来看看,这6.315ms消耗去了哪里。

1000次访问DOM
1000次访问DOM
1000次字符串计算,1次访问DOM
1000次字符串计算,1次访问DOM

从以上Chrome Timeline时间线分析来看,主要还是script执行时间的差异比较大。但看起来并不如之前的差异那么明显。

第二次对比试验:
这次我将DOM节点属性操作替换成了innerHTML的内容操作,我们再来看看效果

很明显,总时间一下爆炸,同时对比上面的两张图,可以看到1000次的DOM节点的innerHTML内容操作比1000次的DOM节点属性操作整整翻了两个数量级。再来看看Timeline。


消耗最为巨大的这次变成了Loading,先看看loading代表的是什么



可以看出,这里应该是Parse HTML消耗了大量时间,也就是解析HTML
我们知道浏览器展现出我们的网页是需要经过这样一个过程的:

浏览器解析DOM生成DOM树 + 解析CSS生成样式树 => 进行layout布局和paint绘制 => 展现到设备上

那么从上面那张图我们可以看出来,JS操作DOM的innerHTML消耗了巨大的时间在Parse HTMl上,这里应该没有涉及到大面积的重绘重排,因为时间饼图中render和paint的部分并没有太多。但其消耗还是很庞大的,一千次微小的DOM操作就共用时接近3s。

由此可见,Js操作DOM的确慢,而且真正的性能瓶颈应该是引起了浏览器的后续操作,也就是解析HTML以及重绘重排等,这些都是非常消耗性能的。因此我们为了尽量避免因操作改变DOM而想了一些方式来进行改善,例如使用Canvas替代原有的DOM动画,或者使用Css3动画。以及,将列表数据渲染进页面时,以一个根节点保存一次性插入,而不是每行数据都插入渲染。

当然,在以前我们使用jQuery的时候,在我们对DOM性能优化相当了解并且能注意到的时候,是可以写出性能很高的代码的,但是并不是所有工程师都能做到那一点,因此,Virtual DOM就应运而生了。

Virtual DOM 是什么?

我所理解的Virtual DOM是一个黑盒,我们不需要去关心它如何映射渲染到DOM上,可以随心所欲的去操作而不担心性能消耗。那下面就让我们具体来看一看Virtual DOM是什么吧。

如果说DOM是一棵枝繁叶茂的树,那Virtual DOM就是一棵被修剪了很多多余的东西,只保留了最基础的一个树形的Js的数据结构。
操作DOM会引发浏览器后续的操作,但操作Js数据结构却不会。我们以下面的例子来看看:

现在需要将上图左边的DOM结构替换成右边的结构,这种情景在实战项目中是经常会遇到的。但是如果直接操作DOM的话,进行移除的话可能就是四次删除,五次插入,这种消耗是很大的。但是使用Virtual DOM,那就是比较两个结构的差异,发现仅仅改变了四次内容,一次插入。这种消耗就小很多,无非加上一个比较的时间。

React 中的Virtual DOM

React中,我们知道,当一个父组件的state发生改变的时候,会引发所有子组件的render,即使这个子组件本身的state是没有改变的。这点在我刚学React的时候非常不能理解,因为我觉得这样频繁无故的渲染会造成性能浪费。但是看完了Virtual DOM之后,我理解了,原来render函数渲染出来的仅仅是Virtual DOM,因为操作Js的数据结构是很快很方便的,所以即使重新渲染一遍Virtual DOM树也是非常快的。同时,渲染出来的组件的新的Virtual DOM树会跟旧的Virtual DOM树进行差异比较,如果有修改,变动,那就将新的Virtual DOM渲染成DOM树。如果没有,则不变动

简单实现 低配版 Virtual DOM

从前面的部分我们可以看到,Virtual DOM需要做的事情有两件:

  • 从DOM树中模拟出相应的Virtual DOM,Js的操作都作用在Virtual DOM上
  • Diff算法比较新旧两颗DOM树,找出其中的差异
  • 将差异进行处理,以最小代价渲染成DOM

呐,我们分析一下,如果想自己实现一个低配版我们需要做哪些事情呢?往下看。

以Js对象模拟出Virtual DOM树

一棵树嘛,最重要的是哪个部分?自然是每个树的节点。那我们首先建立一个节点类。

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

  var el = function (tagName, props, children) {
    return new Element(tagName, props, children)
  }

DOM节点中最重要的特征——标签名、属性、以及子节点,都在这个节点类里面有相应表示。以这样一个节点类的实例去表示DOM的一个节点。

呐,现在如果我们想将一堆DOM结构以上面的节点类转成虚拟DOM该怎么做呢?

是的,朋友们!就是这么简单!我们实现了从DOM到Virtual DOM的一个过程。但是,问题又来了,当我们想从Virtual DOM到DOM又该怎么办呢?接着看。

Element.prototype.render = function () {
    var el = document.createElement(this.tagName)
    var props = this.props

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

    children.forEach(function (child) {
      var childEl = (child instanceof Element)
        ? child.render()
        : document.createElement(childEl)
    })

    return el
  }

我们往Element类上面增加了一个静态方法,这个方法用于将我们之前建立的节点实例还原成真实的DOM元素。
我们可以看到,这个方法内部新建了一个对应标签的DOM元素,并且将属性名还原,最后递归调用,得到内部子元素。返回该新建的DOM元素。

然而,到现在我们也只是完成了DOM与Virtual DOM之间的相互转化。Virtual DOM实现的最重要的部分,其实还没有完成,那就是Diff算法。真正的Diff算法我也不懂,这里也只能简单描述一下Virtual DOM中的Diff算法的思路。

  1. 首先对Virtual DOM树进行一个深度遍历,也就是以深度优先的原则进行,对每一个遍历到的Virtual DOM给一个标记,譬如,顶层的div标记为0。

这样每个节点上都有我们的标记,一旦Virtual DOM重新生成,在比较新旧两颗Virtual DOM树的时候,就可以直接比较对应节点上的变化,如果有变化,则把这个变化与编号一起推入一个差异数组。

// 用数组存储新旧节点的不同
patches[0] = [{difference}, {difference}, ...]

这样我们就得到了这次Virtual DOM树的一个改变内容。但是推入进差异数组中的差异到底怎么表示呢?首先这样一个差异对象,起码有一个type属性是用来表示这次差异的内容吧,不然我哪知道是进行了节点替换还是节点顺序重排呢,这里我简单的列了几个,当然真正的实践中肯定不止这几种类型,其他可以自行列举。

var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3

当我们知道了Virtual DOM上的差异之后,剩下的最后一步自然就是,将这个差异作用到DOM上。
既然Virtual DOM可以进行深度遍历标记,那DOM与Virtual DOM是一样的树状结构,自然也是可以进行深度的遍历标记,那么标记相同,自然就可以将那个差异数组上的每个差异对象对应上其所匹配的DOM元素。而每个DOM元素拿到了其对应差异(以currentPatches表示),就会依据差异的不同,进行不同的处理。这里同样列举几个:

function applyPatches (node, currentPatches) {
  currentPatches.forEach(function (currentPatch) {
    switch (currentPatch.type) {
      case REPLACE:
        node.parentNode.replaceChild(currentPatch.node.render(), node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case PROPS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        node.textContent = currentPatch.content
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
}

这段代码也是很好理解的吧,根据当前差异currentPatches的类型type来进行不同的DOM操作,这样就能将Virtual DOM中有差异的部分,作用到DOM中,而那些并没有差异的部分,就不需要进行额外的变动,自然就节省了很多资源。

最后,Virtual DOM理解清楚一点之后,理解React会更清楚一些
第三部分实现低配版Virtual DOM代码和思路源自深度剖析:如何实现一个 Virtual DOM 算法, 如有侵权,立删。

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

推荐阅读更多精彩内容