React中的虚拟DOM

面试: 虚拟DOM 是什么,有什么用

总结

  1. 虚拟DOM 就是一个JS对象,用它来描述 真实的DOM。
  2. 使用 虚拟DOM 有助于极大提升性能
    React对性能的提升在于减少了 真实DOM对象 的生成与比较,取而代之的是使用 虚拟DOM(JS对象) 来完成数据改变后的生成与比较
    用JS形成一个 JS对象 性能损耗非常小,生成一个 DOM元素 性能损耗大,涉及DOM的操作都很耗性能。(具体可看下面 例子 的“第二次优化”)
  3. 有了 虚拟DOM 使得跨端应用(React Native)得以实现。(因为像是Android、ios、React Native这些原生应用里是没有 真实DOM 的 )

例子

我们通过假设没有 React 来渲染来一步步梳理优化来看看React在背后做了哪些工作。

假设没有 React 来渲染,我们需要做的是

  1. state 数据
  2. JSX 模版
  3. 数据 + 模版 结合,生成 真实的DOM ,来挂载在页面上
  4. state 数据 发生改变
  5. 数据 + 模版 结合,生成 真实的DOM ,替换 原始的DOM

缺陷
第一次生成了一个完整的DOM片段
第二次又生成了一个完整的DOM片段
第二次的DOM替换第一次的DOM,非常耗性能

第一次优化

  1. state 数据
  2. JSX 模版
  3. 数据 + 模版 结合,生成 真实的DOM ,来挂载在页面上
  4. state 数据 发生改变
  5. 数据 + 模版 结合,生成 新的 真实的DOM ,并不替换 原始的DOM
  6. 新的DOM(实际上是JS上的DocumentFragment(文档碎片))和 原始的DOM 做比对,找差异
  7. 找出只有input框发生了变化
  8. 只用 新的DOM中的input元素 替换 原始的DOM中的input元素

缺陷
性能提升并不明显

第二次优化

(注意这并不是React完全正确的顺序,请看下面的“深入了解 虚拟DOM”)

  1. state 数据
  2. JSX 模版
  3. 数据 + 模版 结合,生成 真实的DOM ,来挂载在页面上
    <div id="abc><span>hello world</span></div>
  4. 生成 虚拟的DOM (虚拟DOM就是一个JS对象,用它来描述 真实的DOM)
    ["div",{id:"abc"},["span",{},"hello world"]](可以理解为一个 数组结构的对象 )
    【损耗了一点性能:用JS形成一个 JS对象 性能损耗非常小,生成一个 DOM元素 性能损耗大】
  5. state 数据 发生改变
  6. 数据 + 模版 结合,生成 新的 虚拟的DOM ["div",{id:"abc"},["span",{},"bye bye"]]
    【极大的提升了性能:因为上一次优化在数据改变后生成的是新的DOM元素,而现在我们让他生成的虚拟DOM实际上是JS对象。】
  7. 比较 原始虚拟DOM 和 新的虚拟DOM 的区别,找到区别是 span中的内容
    【上一次优化是将两个DOM做对比,而涉及DOM的操作就很耗性能。在这里我们对比的是虚拟DOM(JS对象),提升了性能】
  8. 直接操作DOM,改变span中的内容

补充:数组类型的对象

对象数组 就是数组里的每个元素都是类的对象,赋值时先定义对象,然后将对象直接赋给数组就行了。

注意

  1. 在 JavaScript 中,几乎“所有事物”都是对象。所有 JavaScript 值,除了原始值,都是对象。(具体可以看笔记“js对象 学习笔记(1)”)
  2. 数组中每一项都可以存放不同类型的数据

深入了解 虚拟DOM

  1. 只有页面需要渲染的时候才会生成真实的DOM
  2. 在这里我们明白了render函数中的标签其实并不是页面上的DOM元素,他们是步骤中的 JSX 模版
  3. render函数第一次执行的时候会将渲染的数据在内存中保存一份,当第二次数据发生了改变后,render会将这次的虚拟DOM缓存中的虚拟DOM进行对比 这种对比叫做DIFF算法
  4. 只要this.state/this.props发生了改变那么render函数就会执行

第三次优化

【注意和之前的“真实DOM”“虚拟DOM”顺序相反】

  1. state 数据
  2. JSX 模版
  3. 数据 + 模版 结合,生成 虚拟的DOM (保存在内存中)
    ["div",{id:"abc"},["span",{},"hello world"]]
  4. 用 虚拟的DOM 的结构 生成 真实的DOM ,显示在页面上
    <div id="abc><span>hello world</span></div>
  5. state 数据 发生改变
  6. 数据 + 模版 结合,生成 新的 虚拟的DOM ["div",{id:"abc"},["span",{},"bye bye"]]
  7. 比较 原始虚拟DOM 和 新的虚拟DOM 的区别,找到区别是 span中的内容
  8. 直接操作有区别的DOM,改变span中的内容

优点

  1. 性能提升了,如果不使用虚拟DOM,则需要生成所有的真实DOM进行替换使用虚拟DOM则只需要生成有区别的真实DOM进行替换即可。
  2. 有了 虚拟DOM 使得跨端应用(React Native)得以实现。(因为像是Android、ios、React Native这些原生应用里是没有 真实DOM 的 )

React真实的操作顺序

JSX -> createElement -> JS 对象(虚拟DOM) -> 真实DOM
帮助理解
下面两个return返回的内容其实是一样的:

1
2
3
4
render(){
return <div>item</div>
return React.createElement("div",{},"item");
}

虚拟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值:
TodoList
当我们在页面上依次输入“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值也就失去了它存在的意义。

,