面试: 虚拟DOM 是什么,有什么用
总结:
- 虚拟DOM 就是一个JS对象,用它来描述 真实的DOM。
- 使用 虚拟DOM 有助于极大提升性能:
React对性能的提升在于减少了 真实DOM对象 的生成与比较,取而代之的是使用 虚拟DOM(JS对象) 来完成数据改变后的生成与比较。
而用JS形成一个 JS对象 性能损耗非常小,生成一个 DOM元素 性能损耗大,涉及DOM的操作都很耗性能。(具体可看下面 例子 的“第二次优化”) - 有了 虚拟DOM 使得跨端应用(React Native)得以实现。(因为像是Android、ios、React Native这些原生应用里是没有 真实DOM 的 )
例子
我们通过假设没有 React 来渲染来一步步梳理优化来看看React在背后做了哪些工作。
假设没有 React 来渲染,我们需要做的是:
- state 数据
- JSX 模版
- 数据 + 模版 结合,生成 真实的DOM ,来挂载在页面上
- state 数据 发生改变
- 数据 + 模版 结合,生成 真实的DOM ,替换 原始的DOM
缺陷:
第一次生成了一个完整的DOM片段
第二次又生成了一个完整的DOM片段
第二次的DOM替换第一次的DOM,非常耗性能
第一次优化
- state 数据
- JSX 模版
- 数据 + 模版 结合,生成 真实的DOM ,来挂载在页面上
- state 数据 发生改变
- 数据 + 模版 结合,生成 新的 真实的DOM ,并不替换 原始的DOM
- 新的DOM(实际上是JS上的DocumentFragment(文档碎片))和 原始的DOM 做比对,找差异
- 找出只有input框发生了变化
- 只用 新的DOM中的input元素 替换 原始的DOM中的input元素
缺陷:
性能提升并不明显
第二次优化
(注意这并不是React完全正确的顺序,请看下面的“深入了解 虚拟DOM”)
- state 数据
- JSX 模版
- 数据 + 模版 结合,生成 真实的DOM ,来挂载在页面上
<div id="abc><span>hello world</span></div>
- 生成 虚拟的DOM (虚拟DOM就是一个JS对象,用它来描述 真实的DOM)
["div",{id:"abc"},["span",{},"hello world"]]
(可以理解为一个 数组结构的对象 )
【损耗了一点性能:用JS形成一个 JS对象 性能损耗非常小,生成一个 DOM元素 性能损耗大】 - state 数据 发生改变
- 数据 + 模版 结合,生成 新的 虚拟的DOM
["div",{id:"abc"},["span",{},"bye bye"]]
【极大的提升了性能:因为上一次优化在数据改变后生成的是新的DOM元素,而现在我们让他生成的虚拟DOM实际上是JS对象。】 - 比较 原始虚拟DOM 和 新的虚拟DOM 的区别,找到区别是 span中的内容
【上一次优化是将两个DOM做对比,而涉及DOM的操作就很耗性能。在这里我们对比的是虚拟DOM(JS对象),提升了性能】 - 直接操作DOM,改变span中的内容
补充:数组类型的对象
对象数组 就是数组里的每个元素都是类的对象,赋值时先定义对象,然后将对象直接赋给数组就行了。
注意:
- 在 JavaScript 中,几乎“所有事物”都是对象。所有 JavaScript 值,除了原始值,都是对象。(具体可以看笔记“js对象 学习笔记(1)”)
- 数组中每一项都可以存放不同类型的数据。
深入了解 虚拟DOM
- 只有页面需要渲染的时候才会生成真实的DOM。
- 在这里我们明白了render函数中的标签其实并不是页面上的DOM元素,他们是步骤中的 JSX 模版。
- render函数第一次执行的时候会将渲染的数据在内存中保存一份,当第二次数据发生了改变后,render会将这次的虚拟DOM与缓存中的虚拟DOM进行对比 这种对比叫做DIFF算法
- 只要
this.state
/this.props
发生了改变那么render函数就会执行
第三次优化
【注意和之前的“真实DOM”“虚拟DOM”顺序相反】
- state 数据
- JSX 模版
- 数据 + 模版 结合,生成 虚拟的DOM (保存在内存中)
["div",{id:"abc"},["span",{},"hello world"]]
- 用 虚拟的DOM 的结构 生成 真实的DOM ,显示在页面上
<div id="abc><span>hello world</span></div>
- state 数据 发生改变
- 数据 + 模版 结合,生成 新的 虚拟的DOM
["div",{id:"abc"},["span",{},"bye bye"]]
- 比较 原始虚拟DOM 和 新的虚拟DOM 的区别,找到区别是 span中的内容
- 直接操作有区别的DOM,改变span中的内容
优点:
- 性能提升了,如果不使用虚拟DOM,则需要生成所有的真实DOM进行替换,使用虚拟DOM则只需要生成有区别的真实DOM进行替换即可。
- 有了 虚拟DOM 使得跨端应用(React Native)得以实现。(因为像是Android、ios、React Native这些原生应用里是没有 真实DOM 的 )
React真实的操作顺序
JSX -> createElement -> JS 对象(虚拟DOM) -> 真实DOM
帮助理解:
下面两个return返回的内容其实是一样的:
1 | render(){ |
虚拟DOM中的Diff算法
在上面第7步中我们说去 “当state数据发生改变时,比较 原始虚拟DOM 和 新的虚拟DOM 的区别”, 这里的“比较”采用的就是Diff算法,(Diff->difference->找不同)。
Diff算法大大提升了两个虚拟DOM之间进行比对的性能。
下面的同层比较 和 key值匹配 就是 Diff算法 的一部分。
答之前的问题:为什么setState是异步的
(关于什么是异步可参考笔记“JS中的同步与异步”)
setState之所以是异步,是因为要提高React底层的性能。
提高性能主要靠减少比对次数:如果不是异步的,短时间内修改3次state,则react需要进行3次 虚拟DOM 的比对,而异步则可以把3次比对合并,react只需要进行1次 虚拟DOM 的比对。
异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。
我的理解:也就是说如果短时间内多次修改,他会等你停止修改以后才通知主线程可以对我进行比对啦。
同层比较
React的 虚拟DOM 采用的是 同层比较 的算法。
逐层比对,如果第一层就不同,react就不会往下比对,而是直接使用 新的虚拟DOM 去生成真实的DOM。虽然可能会造成DOM节点渲染的浪费,但同层比对的算法简单,效率快,大大减少了两个虚拟DOM之间比对的性能消耗。
答之前的问题:为什么index不能作为key值
key值需要是稳定的,可变的key值也就失去了它存在的意义。
原理
在React中我们会根据key值给 虚拟DOM 命名,数据修改后 新的 虚拟DOM 就会根据key值和 原始的 虚拟DOM 进行快速比对,多出来的就是修改的DOM。这会大大提升性能。
之前的 TodoList 的例子
如果我们使用数组下标index作为key值,我们就没办法保证 新的 虚拟DOM 和 原始的 虚拟DOM 的 名字(key值)一致,也就没办法快速匹配了。
所以使用一个稳定的内容作为key值才是稳妥的做法,数组下标index是会因为列表的变化而发生改变的,他是不稳定的。
结合我们之前的 TodoList 的例子,我们将数组下标index作为key值:
当我们在页面上依次输入“a,b,c”时,他们对应的key分别是“0 a,1 b,2 c”,而当我们点击a时,a被从list中删除掉,bc对应的key分别是“0 b,1 c”,可以发现如果我们将index作为key则key是可变的。但我们必须保证 新的 虚拟DOM 到了与 原始DOM 比对的树上时它的名字(key值)是没有变的,否则无法快速匹配。可变的key值也就失去了它存在的意义。