最近对React18提到的新特性做了一些尝试,探索了一下其API的使用场景和效果。
React并发机制
背景
React16之后用Fiber对核心算法进行了重新实现。
React Fiber 用类似 requestIdleCallback 的机制来做异步 diff。把渲染任务拆分成多个子任务(执行单元),完成一个子任务,检查下剩余时间。如果有则继续执行下一个子任务,没有则挂起当前任务,将控制权交还给主线程,当主线程不忙的时候继续执行。
但是虚拟dom是树结构,节点里只有 children,遍历的时候不能中断, 于是 React 实现带有有 parent、sibling 信息的fiber节点,这样就算打断了也可以找到下一个节点继续处理,这样就能方便的做中断和恢复了。
由此,React获得了可中断的任务切片处理和调整任务优先级的并发能力(可交替处理多件任务,提高UI渲染效率)。
通过React18基于并发能力实现的API:useTransition、useDeferredValue, 才会开启带时间片循环的并发工作模式。
具体实现:
- 在 workLoop 里通过 shouldYield (根据时间分片是否过期)打断渲染,之后把剩下的节点加入 Schedule 调度的任务队列,来恢复渲染。
准备工作
首先要体验并发特性需要升级React和React-Dom到18.0.0以上,并且使用它们新的Root API。
需要注意的是新的Root API和18之前的版本有所不同,由:
ReactDOM.render(<div>Hello</div>,document.getElementById('root'));
转变成了:
const root = document.getElementById('root'); ReactDOM.createRoot(root).render(<div></div>);
这么做只是开启了并发模式,但是并没有开启并发更新,想要体验并发更新,还需要使用React提供的一些新API。比如useTransition和useDeferredValue
关键API对比
不使用并发API
首先看看不使用并发API,用最朴实的代码实现一个容易造成阻塞的长任务,这种写法不会开启并发更新。上代码:
页面渲染了一个超长的数组,在Chrome的Performance菜单下可以看到这个长任务FunctionCall是连续的一整块,没有被切分,大概阻塞了242ms。
useTransition
被useTransition包裹的任务,会被赋予更低的优先级,源码是通过在调度的时候把任务往后排来实现的。
并且因为使用useTransition API,React渲染时会自动开启并发更新机制,也就是时间分片。
查看Performance中打印出来每一帧的缩略图,可以发现quickList中的状态会比list优先渲染,并且打印出来的Frame中发现被useTransition包裹的长任务被拆分到每一帧里面分片执行。
(isPending是当处于延迟状态的标志)
useDeferredValue
useDeferredValue和useTransition非常相似,他们都会导致更新延迟,创建优先级比普通更新低的更新,但是前者包裹的是值(状态),后者包裹的是任务。
通常useDeferredValue可以用于包裹那些通过父组件传入的值,因为这种传入值的延迟渲染是没法通过useTransition来控制的,如以下代码所示:
通过在控制打印text和deferredText就能发现,deferredText的值总是延后text的值,在下一次渲染的时候才更新。
参考资料
- [New in 18: useDeferredValue · Discussion #129 · reactwg/react-18|github.com/reactwg/rea…]
- [彻底搞懂 React 18 并发机制的原理 - 知乎|zhuanlan.zhihu.com/p/587588034]
- [React18 新特性解读 & 完整版升级指南 - 掘金|juejin.cn/post/709403…]