上节讲述了React中的JSX,实际上就是一种语法糖,最终是通过React的createElement转换为reactElement对象即虚拟DOM,再通过render方法转换为实际DOM的。
可以这样理解虚拟DOM:他是一个存储在内存里描述真实DOM结构的一棵树,使用的是javascript对象。他是React自己维护的轻量级结构快照。
React的策略是先在虚拟DOM上计算出最小的改动,再去真实DOM上施工。
那么问题来了,为什么要用虚拟DOM呢,为什么不直接转换成DOM呢?也就是说虚拟DOM有什么好处呢?
实际的好处有三点:
- 很多人认为DOM操作很慢,所以搞了虚拟DOM。 这句话不完全对,实际上DOM的操作本身不是慢,是“复杂 + 不可控”。 DOM的问题在于API多,浏览器差异,修改开销很大(直接更新DOM导致频繁的回流和重绘)
- React的核心思想是 UI = render(state), 所以在改变状态时必然要重新render,比较前后两次render的差异,再决定如何更新,因此有了虚拟DOM这个产物
- 虚拟DOM提供了批量更新和跨平台的能力,有了虚拟DOM就可以合并多次setState。 并且通过虚拟DOM这个中间产物可以做到分发到不同的平台上:
- 浏览器(React DOM)
- 原生(React Native)
- Canvas WebGL
- 终端设备
在更新实际DOM之前,React需要先对比虚拟DOM和实际DOM的区别,然后将有差异的DOM更新,而不是全量更新,这里的对比使用的就是diff算法。 下面简单介绍一下这个diff算法
React不做全量更新的前提是使用了三个工程化假设
- 只在同一层级进行比较,也即只要层级发生了变化,整个节点就被认为是有变化
- 不同的type 会被直接认为是有变化,会被直接替换。type指的是HTML标签,如div, span等
- key决定了节点的身份。 在循环输出时react要求必须给子节点一个key值,当key值发生变化时节点也被认为产生了变化。
总结: React 的虚拟 DOM 本质是用 JS 对象描述 UI,通过 Diff 算法计算最小更新,再批量同步到真实 DOM,从而实现声明式、可预测、跨平台的 UI 更新模型。
接下来我们详细看一下react从一次state更新到真实DOM发生变化内部到底发生了什么。
- setState -> dispatch
- 调度 -> scheduler
- Render阶段 (构建fiber + diff, 可中断)
- Commit阶段 (构建真实DOM, 不可中断)
- 浏览器绘制 (Paint)
下面我们具体看一下整个过程
1. 触发更新
我们知道触发react组件的更新是state的变化,在state变化之后React做的第一件事是创建一个update对象,挂载到当前Fiber的updateQueue上。 (Fiber是React内部的一种数据结构, 可以把它理解为一种可以恢复也可以打断的数据节点)
2. 进入调度阶段 scheduler
React会根据下面的信息来决定是否立即计算:
- 更新来源
- 当前是否在渲染
- 优先级
3. 进入render阶段
从根节点开始循环处理Fiber工作单元。
注1: render阶段不会更改DOM
注2: Fiber不仅仅包括组件,也包括DOM标签, DOM节点,Fragment等
3.1 执行函数组件 (重新render)
在render阶段,react对每一个Fiber调用函数,执行hooks,生成新的React Element (虚拟DOM)
3.2 新旧虚拟DOM对比
这是构建fiber tree的前提,WIP Fiber tree就是通过对比虚拟节点和current Fiber tree逐步构建成的
3.3 给Fiber打副作用标记:(effect flags)
在这里给要变化的fiber打上需要如何操作的标记:
- placement -> 插入
- update -> 更新
- deletion -> 删除
3.4 从根节点开始构建workInProgress Fiber tree
current Fiber tree(屏幕上正在用的节点) -> workInProgress Fiber tree(正在构建的新节点)
一句话总结: React 的 render 阶段是一个遍历 Fiber 的过程:在遍历每个 Fiber 时,React 会执行组件函数生成新的 React Element,并立即在 reconcile 阶段将这些 Element 与 current Fiber 对齐,逐步构建 workInProgress Fiber 树并打上副作用标记。
4. Commit 阶段 (修改DOM)
此阶段不可中断
4.1 before mutation – 在修改DOM之前, react会先执行getSnapshotBeforeUpdate,创建一个快照,并保存当前DOM的状态 (如滚动位置等)
4.2 mutation – React会按照之前打上的标记来操作DOM,常见操作包括:
- appendChild
- removeChild
- setAttribute
- textContent = “”
这里是唯一会操作真实DOM的地方
4.3 DOM更新完成后,浏览器还未进行重绘
React会执行useLayoutEffect 这个hook
5. 浏览器渲染 (paint)
在这里浏览器进行绘制需要的操作:样式计算,布局,绘制等,用户看到了变化
React也会在浏览器渲染之后执行useEffect这个hook。
总结: React在state更新后会在内存中重新执行render,构建fiber树,并通过diff算法标记,合并变化计算出最小更新,再在commit阶段将这些更新同步到真实DOM上,最后交给浏览器渲染
