前言
- 假设有如下代码,input接收用户输入的数量num同时渲染出num个li
import React, { useState, useTransition } from "react";
import ReactDOM from "react-dom/client";
function App() {
const [num, setNum] = useState(0);
const handleInputChange = ({ target: { value } }) => {
const foramtVal = parseInt(value) || 0;
setNum(foramtVal);
};
return (
<>
<input onChange={handleInputChange} value={num}></input>
<ul>
{Array(num)
.fill(0)
.map((_, i) => (
<li key={i}>{num - i}</li>
))}
</ul>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
1 不使用useTransition
- 直接调用setState时react采用Legacy模式来协调fiber tree,最终会调用renderRootSync进入workLoopSync,整个过程是同步不可中断的循环
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
-
当不使用useTransition时输入43210发现,当输入到0时出现明显的卡顿,input输入框响应从4321到43210等了约3秒时间;
-
浏览器在一帧(一般是16.6ms)中要完成下面的任务,因为JS可以操作DOM,所以
GUI渲染线程与JS线程是互斥的,如果一个task执行js时间过长,那么就没有时间留给浏览器做渲染工作,就会导致掉帧;
一个task(宏任务) -- 队列中全部job(微任务) -- requestAnimationFrame -- 浏览器重排/重绘 -- requestIdleCallback
- 这时,整个调用栈都在一个task中,消耗了约3秒,从performance分析发现js处理占用了约2.68s,明显大于一帧的16.6毫秒,这时候如果有用户输入,点击等操作时都不能及时响应,会出现页面掉帧卡顿
2 使用useTransition
- 改造一下代码,区分出列表渲染为低优先级更新;
import React, { useState, useTransition } from "react";
import ReactDOM from "react-dom/client";
function App() {
const [num, setNum] = useState(0);
const [length, setLength] = useState(0);
const [isPending, startTransition] = useTransition();
const handleInputChange = ({ target: { value } }) => {
const foramtVal = parseInt(value) || 0;
setNum(foramtVal);//紧急更新
startTransition(() => setLength(foramtVal));//低优先级更新
};
return (
<>
<input onChange={handleInputChange} value={num}></input>
<ul>
{isPending
? "pending..."
: Array(length)
.fill(0)
.map((_, i) => <li key={i}>{length - i}</li>)}
</ul>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
- 通过useTransition调用setState,react采用Concurrent模式来协调fiber tree,最终会调用renderRootConcurrent进入workLoopConcurrent,每次进入循环前都会调用shouldYield(判断执行时间是否大于frameInterval,默认是5ms)方法判断是否暂停,这样就实现了可中断;
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
-
当使用useTransition时输入43210发现,输入框响应从4321到43210比刚才明显要及时,列表渲染经历了从pending到真实列表的过程;
-
通过performance面板发现render阶段的工作被拆分成了多个task,此时每个task的时间占用都较小(约5ms),浏览器有剩余时间可以响应input框中的紧急更新,所以可以看到输入框的变化比不使用useTransition要流畅很多;
3 react可中断更新原理之时间切片
- react16后的fiber架构把每一个元素每一个组件分成了一个个fiber节点,每次调用performUnitOfWork都只处理一个fiber节点,由原来的递归不可中断更新变成了循环可中断更新,这里进入循环前都会调用shouldYield(判断执行时间是否大于frameInterval,默认是5ms)方法判断是否暂停,这样就实现了可中断;
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
- 这样react在中断render之后,就把剩余时间的控制权交还给了浏览器,浏览器就有时间去做渲染工作了,减少了掉帧的可能性(如果一个fiber节点的处理时间非常长那也是会存在执行js时间过长导致掉帧的)
- 上面shoulYield中断render后,
Scheduler将需要被执行的回调函数作为MessageChannel的回调在下一个task中执行。如果当前宿主环境不支持MessageChannel,则使用setTimeout。背后实现原理需要学习Scheduler的lane模型及优先级调度等知识,等后续搞懂了再来更新啦~