第293天:React虚拟DOM的理解
React虚拟DOM的理解
Virtual DOM
是一棵以JavaScript
对象作为基础的树,每一个节点可以将其称为VNode
,用对象属性来描述节点,实际上它是一层对真实DOM
的抽象,最终可以通过渲染操作使这棵树映射到真实环境上,简单来说Virtual DOM
就是一个Js
对象,是更加轻量级的对DOM
的描述,用以表示整个文档。
描述
在浏览器中构建页面时需要使用DOM
节点描述整个文档。
<div class="root" name="root">
<p>1</p>
<div>11</div>
</div>
如果使用Js
对象去描述上述的节点以及文档,那么便类似于下面的样子。
{
type: "tag",
tagName: "div",
attr: {
className: "root"
name: "root"
},
parent: null,
children: [{
type: "tag",
tagName: "p",
attr: {},
parent: {} /* 父节点的引用 */,
children: [{
type: "text",
tagName: "text",
parent: {} /* 父节点的引用 */,
content: "1"
}]
},{
type: "tag",
tagName: "div",
attr: {},
parent: {} /* 父节点的引用 */,
children: [{
type: "text",
tagName: "text",
parent: {} /* 父节点的引用 */,
content: "11"
}]
}]
}
React中的虚拟DOM
Virtual DOM
是一种编程概念,在这个概念里,UI
以一种理想化的,或者说虚拟的表现形式被保存于内存中,并通过如ReactDOM
等类库使之与真实的DOM
同步,这一过程叫做协调。这种方式赋予了React
声明式的API
,您告诉React
希望让UI
是什么状态,React
就确保DOM
匹配该状态,这样可以从属性操作、事件处理和手动DOM
更新这些在构建应用程序时必要的操作中解放出来。
与其将Virtual DOM
视为一种技术,不如说它是一种模式,人们提到它时经常是要表达不同的东西。在React
的世界里,术语Virtual DOM
通常与React
元素关联在一起,因为它们都是代表了用户界面的对象,而React
也使用一个名为fibers
的内部对象来存放组件树的附加信息,上述二者也被认为是React
中Virtual DOM
实现的一部分。
React中的虚拟DOM的历史
在之前,Facebook
是PHP
大户,所以React
最开始的灵感就来自于PHP
。
在2004
年这个时候,大家都还在用PHP
的字符串拼接来开发网站。
$str = "<ul>";
foreach ($talks as $talk) {
$str += "<li>" . $talk->name . "</li>";
}
$str += "</ul>";
这种方式代码写出来不好看不说,还容易造成XSS
等安全问题。应对方法是对用户的任何输入都进行转义Escape
,但是如果对字符串进行多次转义,那么反转义的次数也必须是相同的,否则会无法得到原内容,如果又不小心把HTML
标签给转义了,那么HTML
标签会直接显示给用户,从而导致很差的用户体验。
到了2010
年,为了更加高效的编码,同时也避免转义HTML
标签的错误,Facebook
开发了XHP
。XHP
是对PHP
的语法拓展,它允许开发者直接在PHP
中使用HTML
标签,而不再使用字符串。
$content = <ul />;
foreach ($talks as $talk) {
$content->appendChild(<li>{$talk->name}</li>);
}
这样的话,所有HTML
标签都使用不同于PHP
的语法,我们可以轻易的分辨哪些需要转义哪些不需要转义。不久的后来,Facebook
的工程师又发现他们还可以创建自定义标签,而且通过组合自定义标签有助于构建大型应用。
到了2013
年,前端工程师Jordan Walke
向他的经理提出了一个大胆的想法:把XHP
的拓展功能迁移到Js
中,首要任务是需要一个拓展来让JS
支持XML
语法,该拓展称为JSX
。因为当时由于Node.js
在Facebook
已经有很多实践,所以很快就实现了JSX
。
const content = (
<TalkList>
{talks.map(talk => <Talk talk={talk} />)}
</TalkList>
);
在这个时候,就有另外一个很棘手的问题,那就是在进行更新的时候,需要去操作DOM
,传统 DOM API
细节太多,操作复杂,所以就很容易出现Bug
,而且代码难以维护。然后就想到了PHP
时代的更新机制,每当有数据改变时,只需要跳到一个由PHP
全新渲染的新页面即可。从开发者的角度来看的话,这种方式开发应用是非常简单的,因为它不需要担心变更,且界面上用户数据改变时所有内容都是同步的。为此React
提出了一个新的思想,即始终整体刷新页面,当发生前后状态变化时,React
会自动更新UI
,让我们从复杂的UI
操作中解放出来,使我们只需关于状态以及最终UI
长什么样。这个时候,我只需要关系我的状态(数据是什么),以及UI
长什么样(布局),不再需要关系操作细节。
这种方式虽然简单粗暴,但是很明显的缺点,就是很慢。另外还有一个问题就是这样无法包含节点的状态,比如它会失去当前聚焦的元素和光标,以及文本选择和页面滚动位置,这些都是页面的当前状态。
为了解决上面说的问题,对于没有改变的DOM
节点,让它保持原样不动,仅仅创建并替换变更过的DOM
节点,这种方式实现了DOM
节点复用Reuse
。至此,只要能够识别出哪些节点改变了,那么就可以实现对DOM
的更新,于是问题就转化为如何比对两个DOM
的差异。说到对比差异,可能很容易想到版本控制git
。DOM
是树形结构,所以diff
算法必须是针对树形结构的,目前已知的完整树形结构的编辑距离diff
算法复杂度为O(n^3)
。但是时间复杂度O(n^3)
太高了,所以Facebook
工程师考虑到组件的特殊情况,进行了一些优化与折中,然后将复杂度降低到了O(n)
。DOM
是复杂的,对它的操作尤其是查询和创建是非常慢非常耗费资源的。看下面的例子,仅创建一个空白的div
,其实例属性就达到294
个。
// Chrome v84
const div = document.createElement("div");
let m = 0;
for (let k in div) m++;
console.log(m); // 294
对于DOM
这么多属性,其实大部分属性对于做Diff
是没有任何用处的,所以如果用更轻量级的Js
对象来代替复杂的DOM
节点,然后把对DOM
的diff
操作转移到Js
对象,就可以避免大量对DOM
的查询操作。这个更轻量级的Js
对象就称为Virtual DOM
。那么现在的过程就是这样:
维护一个使用
Js
对象表示的Virtual DOM
,与真实DOM
一一对应。对前后两个
Virtual DOM
做diff
,生成变更Mutation
。把变更应用于真实
DOM
,生成最新的真实DOM
。
可以看出,因为要把变更应用到真实DOM
上,所以还是避免不了要直接操作DOM
,但是React
的diff
算法会把DOM
改动次数降到最低。关于React
中的虚拟DOM
创建过程可以参考https://github.com/facebook/react/blob/9198a5cec0936a21a5ba194a22fcbac03eba5d1d/packages/react/src/ReactElement.js#L348
。
总结
传统前端的编程方式是命令式的,直接操纵DOM
,告诉浏览器该怎么干,这样的问题就是,大量的代码被用于操作DOM
元素,且代码可读性差,可维护性低。React
的出现,将命令式变成了声明式,摒弃了直接操作DOM
的细节,只关注数据的变动,DOM
操作由框架来完成,从而大幅度提升了代码的可读性和可维护性。
在初期我们可以看到,数据的变动导致整个页面的刷新,这种效率很低,因为可能是局部的数据变化,但是要刷新整个页面,造成了不必要的开销。所以就有了Diff
过程,将数据变动前后的DOM
结构先进行比较,找出两者的不同处,然后再对不同之处进行更新渲染。但是由于整个DOM
结构又太大,所以采用了更轻量级的对DOM
的描述—虚拟DOM
。
不过需要注意的是,虚拟DOM
和Diff
算法的出现是为了解决由命令式编程转变为声明式编程、数据驱动后所带来的性能问题的。换句话说,直接操作DOM
的性能并不会低于虚拟DOM
和Diff
算法,甚至还会优于。框架的意义在于为你掩盖底层的DOM
操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护,没有任何框架可以比纯手动的优化DOM
操作更快,因为框架的DOM
操作层需要应对任何上层API
可能产生的操作,它的实现必须是普适的。
虚拟DOM优缺点
优点
Virtual DOM
在牺牲(牺牲很关键)部分性能的前提下,增加了可维护性,这也是很多框架的通性。实现了对
DOM
的集中化操作,在数据改变时先对虚拟DOM
进行修改,再反映到真实的DOM
,用最小的代价来更新DOM
,提高效率。打开了函数式
UI
编程的大门。可以渲染到
DOM
以外的端,使得框架跨平台,比如ReactNative
,React VR
等。可以更好的实现
SSR
,同构渲染等。组件的高度抽象化。
缺点
首次渲染大量
DOM
时,由于多了一层虚拟DOM
的计算,会比innerHTML
插入慢。虚拟
DOM
需要在内存中的维护一份DOM
的副本,多占用了部分内存。如果虚拟
DOM
大量更改,这是合适的。但是单一的、频繁的更新的话,虚拟DOM
将会花费更多的时间处理计算的工作。所以如果你有一个DOM
节点相对较少页面,用虚拟DOM
,它实际上有可能会更慢,但对于大多数单页面应用,这应该都会更快。
每日一题
https://github.com/WindrunnerMax/EveryDay
参考
https://zhuanlan.zhihu.com/p/99973075
https://www.jianshu.com/p/e0a3ac85db5c
https://www.jianshu.com/p/9a1d2750457f
https://github.com/livoras/blog/issues/13
https://juejin.cn/post/6844904165026562056
https://juejin.cn/post/6844903640512086029
https://zh-hans.reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom