悄悄告诉你:React18文档错误的地方
时间:2023-03-19 10:28:27
科技观察
大家好,我已经发布了卡森React18正式版一段时间了,如果你升级到v18后仍然使用ReactDOM.render创建应用,你会收到如下信息alarm:大意是:v18使用createRoot代替render创建应用,如果你仍然使用render创建应用,那么应用的行为会和v17一样。React团队之所以有信心让大家升级到v18并使用createRoot,是因为他们做出了承诺:大意是如果升级到v18,只要不使用“并发特性”(比如useTransition),React将与之前版本一致的行为(更新是同步的,不可中断的)。今天这篇文章想说的是:在某些情况下,上面的说法是错误的。废话不多说,上例中有a和b两种状态,第一次渲染后2秒会触发a和b的更新。其中,触发b更新的方式比较特殊:模拟点击,间接触发b更新:functionApp(){const[a,setA]=useState(0);const[b,setB]=useState(0);constBtnRef=useRef(null);useEffect(()=>{setTimeout(()=>{setA(9000);BtnRef.current?.click();},2000);},[]);返回(setB(1)}>b:{b}{Array(a).fill(0).map((_,i)=>{return{a} ;})}
);}完整示例地址[1]现在我们有两种挂载方式。v18之前的方式:constrootElement=document.getElementById("root");//v18之前创建应用的方式ReactDOM.render(
,rootElement);v18提供的方式:constroot=ReactDOM.createRoot(rootElement);//v18??创建应用的方式root.render(
);为了看出两者的区别,有两种方法:增加setA(9000)中的值,让页面渲染更多的item。页面渲染时的滞后越明显,渲染顺序的差异就越明显。setTimeout(()=>{setA(9000);BtnRef.current?.click();},2000);react-dom.development.js的commitRootImpl方法中的断点。该方法是React渲染时调用的方法。这里打断点可以看到页面渲染的顺序。对于ReactDOM.render创建的应用,触发更新后的渲染顺序如下:first:second:对于ReactDOM.createRoot创建的应用,触发更新后的渲染顺序如下:first:second:渲染order明显变了,这和React文档中的声明是矛盾的。其背后的原因是什么?更新的优先级无处不在。先解释一下为什么例子中的b采用“触发onClick事件”的方式间接触发更新:BtnRef.current?.click();这是因为:不同方法触发的更新有不同的“优先级”,在onClick回调中触发的更新是最优的,即“同步优先级”。那么问题来了,v18没有使用并发特性,难道所有更新不应该是“同步不间断”的吗?没错,更新本身就是“同步且不可中断”的。但是需要安排更新。示例中,如果使用ReactDOM.createRoot创建应用,触发更新时的优先级如下:setTimeout(()=>{//触发更新,优先级为“默认优先级”setA(9000);//触发更新,优先级为“同步优先级”BtnRef.current?.click();},2000);接下来React的执行流程如下:a触发更新,优先级为“默认优先级”。安排具有优先级“默认优先级”的更新。b触发优先级为“同步优先级”的更新。安排b的更新优先级为“sync-priority”。这时发现已经调度了一个更新(a的更新),优先级较低(默认优先级<同步优先级)。取消调度a的更新,开始调度b的更新。调度过程结束,开始同步不间断地执行b的update。b对应于更新和渲染到页面。这时候发现又有一个更新(a的更新),被调度了。调度过程结束,开始同步不间断地执行a的更新。a对应于页面的更新和呈现。可见,只要使用ReactDOM.createRoot创建应用,“优先级”的影响就一直存在。与“使用并发特性”的区别在于:只有“默认优先级”和“同步优先级”。优先级只影响调度,它不会中断更新的执行。旧版本React的历史包袱那么用ReactDOM.render创建的应用程序的执行顺序呢?记得一个经典的(而且毫无意义的)React面试问题:React更新是同步的还是异步的?下面这两种情况,打印出来的结果是1吗?//案例1onClick(){this.setState({a:1});console.log(a);}//Case2onClick(){setTimeout(()=>{this.setState({a:1});console.log(a);})}其中,打印结果为情况2中的a为1。造成这种情况的原因是React早期实现批处理的缺陷造成的,并非有意为之。当使用Fiber架构重构React时,完全可以避免这个缺陷。但是为了和老版本的行为保持一致,特意这样实现。因此,在我们的示例中,这两个更新不会受到“优先级”的影响,但会受到“与旧版本的兼容性”的影响:setTimeout(()=>{setA(9000);BtnRef.current?.click();},2000);React的执行过程是这样的:a触发更新,因为是在setTimeout中触发的,所以后面的更新过程会同步执行。a对应于页面的更新和渲染。b触发更新,因为是在setTimeout中触发的,所以后面的更新过程会同步执行。b对应于更新和渲染到页面。总结一下,React是一个维护了将近10年的框架。在主要版本更新后保持框架的行为一致并不容易。更新顺序的变化对一般应用影响不大。但是,如果你的应用依赖于更新后的“页面中的当前值”来进行后续判断,升级到v18后需要注意这些细微的变化。参考[1]完整示例地址:https://codesandbox.io/s/strange-cartwright-iq1s2m?file=/src/index.tsx。