Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

理解 Virtual DOM #5

Open
y8n opened this issue May 4, 2017 · 11 comments
Open

理解 Virtual DOM #5

y8n opened this issue May 4, 2017 · 11 comments

Comments

@y8n
Copy link
Owner

y8n commented May 4, 2017

前言

使用过React的同学对于Virtual DOM并不陌生,作为React的重要核心概念,Virtual DOM凭借其高效的diff算法,让我们不用关心应用的性能问题,毫无顾忌地修改各种数据状态。在实际的开发中,我们并不需要关注Virtual DOM在一个框架中是如何运行的,但是理解Virtual DOM的实现原理却是非常有必要的,同时也有助于我们更加深入React。

一、前端应用状态管理

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

但是,双向数据绑定也并不是唯一的办法,还有一个非常粗暴有效的方式:一旦数据发生变化,重新绘制整个视图,也就是重新设置一下innerHTML。这样的做法确实简单、粗暴、有效,但是如果只是因为局部一个小的数据发生变化而更新整个视图,性价比未免太低了,而且,像事件,获取焦点的输入框等,都需要重新处理。所以,对于小的应用或者说局部的小视图,这样处理完全是可以的,但是面对复杂的大型应用,这样的做法不可取。

说到这里,你会说这跟Virtual DOM有什么关系呢?其实Virtual DOM就是这么做的,只是在高效的diff算法计算下,避免对整棵DOM树进行变更,而是进行针对性的视图变更,将效率做到最优化。

二、什么是Virtual DOM

Virtual DOM的概念有很多解释,从我的理解来看,主要是三个方面,分别是:一个对象,两个前提,三个步骤。

一个对象指的是Virtual DOM是一个基本的JavaScript对象,也是整个Virtual DOM树的基本。

两个前提分别是JavaScript很快和直接操作DOM很慢,这是Virtual DOM得以实现的两个基本前提。得益于V8引擎的出现,让JavaScript可以高效地运行,在性能上有了极大的提高。直接操作DOM的低效和JavaScript的高效相对比,为Virtual DOM的产生提供了大前提。

三个步骤指的是Virtual DOM的三个重要步骤,分别是:生成Virtual DOM树、对比两棵树的差异、更新视图。这三个步骤的具体实现也是本文将简述的一大重点。

三、Virtual DOM三板斧

下面就将介绍Virtual DOM的三个步骤具体的含义以及实现思路。

1、生成Virtual DOM树

DOM是前端工程师最常接触的内容之一,一个DOM节点包含了很多的内容,但是一个抽象出一个DOM节点却只需要三部分:节点类型,节点属性、子节点。所以围绕这三个部分,我们可以使用JavaScript简单地实现一棵DOM树,然后给节点实现渲染方法,就可以实现虚拟节点到真是DOM的转化。

DOM树的状态转化

2、对比两棵树的差异

比较两棵DOM树的差异是Virtual DOM算法最核心的部分,这也是我们常说的的 Virtual DOM的diff算法。在比较的过程中,我们只比较同级的节点,非同级的节点不在我们的比较范围内,这样既可以满足我们的需求,又可以简化算法实现。

diff

比较“树”的差异,首先是要对树进行遍历,常用的有两种遍历算法,分别是深度优先遍历和广度优先遍历,一般的diff算法中都采用的是深度优先遍历。对新旧两棵树进行一次深度优先的遍历,这样每个节点都会有一个唯一的标记。在遍历的时候,每遍历到一个节点就把该节点和新的树的同一个位置的节点进行对比,如果有差异的话就记录到一个对象里面。

深度优先遍历

例如,上面的div和新的div有差异,当前的标记是0,那么:patches[0] = [{difference}, {difference}, ...]同理ppatches[1]ulpatches[3],以此类推。这样当遍历完整棵树的时候,就可以获得一个完整的差异对象。

在这个差异对象中记录了有改变的节点,每一个发生改变的内容也不尽相同,但也是有迹可循,常见的差异包括四种,分别是:

  • 替换节点
  • 增加/删除子节点
  • 修改节点属性
  • 改变文本内容

所以在记录差异的时候要根据不同的差异类型,记录不同的内容。

3、更新视图

在第二步得到整棵树的差异之后,就可以根据这些差异的不同类型,对DOM进行针对性的更新。与四种差异类型相对应的,是更新视图时具体的更新方法,分别是:

  • replaceChild()
  • appendChild()/removeChild()
  • setAttribute()/removeAttribute()
  • textContent

更新视图

四、动手实现Virtual DOM

对原理有了一定的认识之后,自然是动手实现一番了,GitHub上有很多对Virtual DOM的实现,比如https://github.com/livoras/simple-virtual-dom/https://github.com/Matt-Esch/virtual-dom等,我也对其进行了一个基本的实现,比较简陋,传送门

五、进一步思考

Virtual DOM的原理和实现的说明已经结束了,但是对于Virtual DOM的思考远没有结束,Virtual DOM 对前端开发的影响难道就只是一堆算法吗?

1、性能对比

首先,先来看一下性能,在诸多的Virtual DOM实现中,都会强调算法的高效,那么在实际的使用中,Virtual DOM的性能到底如何呢?

简单性能对比

上图是对一个简单的DOM树进行不同方式的操作,由左边的结构更新为右边的结构,通过原生操作、jQuery、Virtual DOM和React四种方式,在Chrome的timeline中得到的性能对比,在这个图中,我们并没有看出Virtual DOM或者React的优势,通过对比我们发现,原生的操作要比其他三种方式快,而其他三种方式就相差无几了。当然,这样一个简单测试并没有说明什么,测试的DOM结构简单,和我们平时面对的业务场景不是一个量级,代表不了什么,但是起码我们可以看到,这种情况下好像Virtual DOM并没有我们想象的性能优势。

复杂性能对比

在接下来的测试中我们增加测试量。上图分别是使用原生操作、Virtual DOM和React三种方式进行两类测试:插入10000个节点100次和修改3000个节点的属性100次。分别取这100次的耗时最大值、最小值和平均值。从图中我们可以看到明显的差异,Virtual DOM和React的差异可以理解,毕竟我们自己实现的Virtual DOM没有那么庞大,只是针对虚拟DOM而实现的,比React快一点可以理解,但是原生的操作比Virtual DOM和React都要快得多,这又是怎么一回事,好像和我们预想的不一样,回到最初,我们提到,Virtual DOM的产生前提之一就是直接操作DOM很,现在看来直接操作不但不慢,反而快了很多,这不得不让我产生了怀疑,是我对Virtual DOM的理解有误还是对DOM的理解有误呢?

2、再次审视Virtual DOM

框架存在的意义是什么?是提高性能?提高开发效率?亦或是其他用途,每个人对框架的理解不同,答案也不尽相同。但是不得不承认,存在框架的情况下,项目的可维护性有了极大的提高,而对于其他方面就要做出牺牲,比如性能。在上面的性能测试中,其实完全走入了一个误区,在测试中我们用到的原生的操作其实是“人为”地对操作进行优化之后的结果,而如果抛开人为优化的前提,最终的结果可能就不是这样了。**Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。**这一点是原生操作远远无法替代的。

到此为止,再次审视Virtual DOM,可以简单得出如下结论:

  • Virtual DOM 在牺牲部分性能的前提下,增加了可维护性,这也是很多框架的通性
  • 实现了对DOM的集中化操作,在数据改变时先对虚拟DOM进行修改,再反映到真实的DOM中,用最小的代价来更新DOM,提高效率
  • 打开了函数式UI编程的大门
  • 可以渲染到DOM以外的端,比如ReactNative

六、结语

本文对Virtual DOM有一个简单的介绍,包括实现的部分也很简单,甚至对列表的diff算法也偷工减料,跟多高级的特性也没有涉及,比如事件绑定、生命周期、JSX语法等,如果加上这些内容,就是一个小型版的React了。

本文旨在让大家了解并认识Virtual DOM的基本概念、组成和实现,同时对Virtual DOM更深层的意义有所了解,这样在以后用到相关的框架的时候也不会两眼一抹黑了,起码在性能优化上有点认识,比如列表要带上key这样基本的优化操作。

七、参考资料

@ayqy
Copy link

ayqy commented May 4, 2017

评论参考2时恰好看到了
文章不错,比其它的全面一些

react的优势:做了一些默认优化项,比low的代码快,原理上自然没有直接操作dom高效,说以说快是和不太注重性能的一般业务代码比较

无论什么样的数据绑定,都没有直接操作DOM快(如果操作DOM的人知道基本性能优化原则的话),毕竟框架都是在揣测数据对应的视图变化是什么,而操作DOM的人明确知道要改哪里,怎么改影响最小(性能最好)

@xp6530
Copy link

xp6530 commented Oct 18, 2017

深度优先遍历,之后又是同级比较,麻烦解释下^-^

@xp6530
Copy link

xp6530 commented Oct 18, 2017

image

@jamokop
Copy link

jamokop commented Oct 20, 2017

深度優先遍歷是指node比較的順序(1->2->3...),同級比較系指兩個tree之間的同級(1<=>1, 2<=>2...)

@y8n
Copy link
Owner Author

y8n commented Oct 20, 2017

@xp6530 深度遍历以及同级比较参考楼上同学的解释 😄

@turkyden
Copy link

turkyden commented Nov 3, 2017

看了您的文章,感觉收获很大,从js原生==》jq插件封装==》MVVM,感觉个人的能力在随着团队发展,很棒,希望若干年后,从这里毕业时,出去行走江湖也是快意潇洒的。

@MontageD
Copy link

MontageD commented Jul 4, 2018

其实有句话真的说得挺有道理的,其实框架都是为了不断提高性能服务的,操作对象跟操作DOM哪个效率高,也就显而易见了。

@zhixuanziben
Copy link

@MontageD 框架并没有提高web的性能,只是让开发者更加专注的完成业务逻辑,而不用过渡的考虑性能上的优化。如果以性能来比的话,框架是绝对比不过优化后的原生代码的。

@MontageD
Copy link

@zhixuanziben 多谢大佬提点,可能是我对原生方面还不够足够了解,你说得很对。优化后的原生代码的确有难以比拟的优势,而框架本身也很大限度地加快了开发速度的问题,降低了开发成本。😇都是为了生产服务嘛。

@gnuser
Copy link

gnuser commented Feb 10, 2020

图好漂亮,能问下是啥工具画的?

@y8n
Copy link
Owner Author

y8n commented Mar 16, 2020

图好漂亮,能问下是啥工具画的?

@gnuser PPT... 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants