三世风尘仆仆,愿吾安于禅心。
React 的核心思想
- React 最为核心的就是 Virtual DOM 和 Diff 算法。React 在内存中维护一颗虚拟 DOM 树,当数据发生改变时(state & props),会自动的更新虚拟 DOM,获得一个新的虚拟 DOM 树,然后通过 Diff 算法,比较新旧虚拟 DOM 树,找出最小的有变化的部分,将这个变化的部分(Patch)加入队列,最终批量的更新这些 Patch 到实际的 DOM 中。
传统Diff 算法
- 将一颗 Tree 通过最小操作步数映射为另一颗 Tree,这种算法称之为 Tree Edit Distance(树编辑距离)。
- 最小操作步数(编辑距离)为 3
- 删除 ul 节点
- 添加 span 节点
- 添加 text 节点
React Diff
- 传统 diff 算法其时间复杂度最优解是 O(n^3),那么如果有 1000 个节点,则一次 diff 就将进行 10 亿次比较,这显然无法达到高性能的要求。而 React 通过大胆的假设,并基于假设提出相关策略,成功的将 O(n^3) 复杂度的问题转化为 O(n) 复杂度的问题。
- 两个假设
- 两个不同类型的元素会产生出不同的树
- 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定
- 三个策略
- Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计
- 拥有相同类型的两个组件将会生成相似的树形结构,拥有不同类型的两个组件将会生成不同树形结构
- 对于同一层级的一组子节点,它们可以通过唯一 key 进行区分
Diff 具体优化
- React 分别对以下三个部分进行了 diff 算法优化
- tree diff
- React 只对虚拟 DOM 树进行分层比较,不考虑节点的跨层级比较
- React 通过 updateDepth 对虚拟 Dom 树进行层级控制,只会对相同颜色框内的节点进行比较,根据对比结果,进行节点的新增和删除。如此只需要遍历一次虚拟 Dom 树,就可以完成整个的对比。
- 跨层操作,React 并不会复用 B 节点及其子节点,而是会直接删除 A 节点下的 B 节点,然后再在 C 节点下创建新的 B 节点及其子节点。因此,如果发生跨级操作,React 是不能复用已有节点,可能会导致 React 进行大量重新创建操作,这会影响性能。所以 React 官方推荐尽量避免跨层级的操作。
- component diff
- 如果是同类型组件,首先使用 shouldComponentUpdate()方法判断是否需要进行比较,如果返回true,继续按照 React diff 策略比较组件的虚拟 DOM 树,否则不需要比较
- 如果是不同类型的组件,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点
- element diff
element diff 涉及三种操作:移动、创建、删除。
不使用key的时候,React 对新老同一层级的子节点对比,发现新集合中的 B 不等于老集合中的 A,于是删除 A,创建 B,依此类推,直到删除 D,创建 C。这会使得相同的节点不能复用,出现频繁的删除和创建操作,从而影响性能。
- 使用key的时候,React 首先会对新集合进行遍历,通过唯一 key 来判断老集合中是否存在相同的节点,如果没有则创建,如果有的,则判断是否需要进行移动操作。
- element diff 就是通过唯一 key 来进行 diff 优化,通过复用已有的节点,减少节点的删除和创建操作。