大家好,我卡颂

React18正式版曾经公布一段时间了,如果你降级到v18,且仍应用ReactDOM.render创立利用,会收到如下报警:

粗心是说:v18应用createRoot而不是render创立利用,如果你仍应用render创立利用,那么利用的行为将同v17一样。

React团队之所以有底气让大家都降级到v18,应用createRoot,是因为他们作出了承诺:

粗心是说:如果你降级到v18,只有不应用并发个性(比方useTransition),React会和之前版本体现统一(更新会同步、不可中断)

明天这篇文章想说的是:某些状况下,上述说法是谬误的。

欢送退出人类高质量前端框架群,带飞

不说废话,上示例

示例中有ab两个状态,首次渲染完2秒后会触发ab更新。

其中触发b更新的形式比拟非凡:模仿点击,间接触发b更新:

function App() {  const [a, setA] = useState(0);  const [b, setB] = useState(0);  const BtnRef = useRef<HTMLButtonElement>(null);  useEffect(() => {    setTimeout(() => {      setA(9000);      BtnRef.current?.click();    }, 2000);  }, []);  return (    <div>      <button         ref={BtnRef}         onClick={() => setB(1)}>        b: {b}      </button>      {Array(a).fill(0).map((_, i) => {        return <div key={i}>{a}</div>;      })}    </div>  );}
残缺示例地址

当初咱们有两种挂载<App/>的形式。

v18之前的形式:

const rootElement = document.getElementById("root");// v18之前创立利用的形式ReactDOM.render(<App/>, rootElement);

v18提供的形式:

const root = ReactDOM.createRoot(rootElement);// v18创立利用的形式root.render(  <App />);

为了看清这两者的区别,有两种形式:

  1. 调大setA(9000)中的值,使页面渲染更多项。页面渲染时卡顿越显著,渲染程序的差别越显著
setTimeout(() => {  setA(9000);  BtnRef.current?.click();}, 2000);
  1. react-dom.development.jscommitRootImpl办法中打断点

这个办法是React渲染时调用的办法,在这里打断点能够看出页面渲染的程序。

对于ReactDOM.render创立的利用,触发更新后渲染程序如下:

首先:

其次:

对于ReactDOM.createRoot创立的利用,触发更新后渲染程序如下:

首先:

其次:

渲染程序显然是变了,这和React文档里的说法是相悖的。

背地的起因是什么呢?

更新的优先级,无处不在

先解释下示例中的b为什么采纳触发onClick事件的形式间接触发更新:

BtnRef.current?.click();

这是因为:不同形式触发的更新有不同优先级onClick回调中触发的更新是最高优的,即同步优先级

那么问题来了,v18不应用并发个性,所有更新不都该是同步、不可中断么?

这话是没错,更新自身是同步、不可中断的。然而更新是须要调度的。

在示例中,如果采纳ReactDOM.createRoot创立利用,那么触发更新时的优先级如下:

setTimeout(() => {  // 触发更新,优先级为“默认优先级”  setA(9000);  // 触发更新,优先级为“同步优先级”  BtnRef.current?.click();}, 2000);

接下来React的执行流程如下:

  1. a触发更新,优先级为“默认优先级”
  2. 调度a的更新,优先级为“默认优先级”
  3. b触发更新,优先级为“同步优先级”
  4. 调度b的更新,优先级为“同步优先级”
  5. 此时发现曾经有个更新在调度(a的更新),且优先级更低(默认优先级 < 同步优先级)
  6. 勾销a的更新的调度,转而开始调度b的更新
  7. 调度流程完结,开始同步、不可中断的执行b的更新
  8. b对应更新渲染到页面中
  9. 此时发现还有一个更新(a的更新),调度他
  10. 调度流程完结,开始同步、不可中断的执行a的更新
  11. a对应更新渲染到页面中

可见,只有采纳ReactDOM.createRoot创立利用,那么优先级的影响就会始终存在,与应用了并发个性的区别是:

  • 只有默认优先级同步优先级
  • 优先级只会影响调度,不会中断更新的执行

老版React的历史包袱

那么采纳ReactDOM.render创立的利用执行程序又是怎么一回事呢?

记不记得一道经典(且毫无意义)的React面试题:React的更新是同步还是异步的?

上面两种状况,a打印的后果是1么?

// 状况1onClick() {  this.setState({a: 1});  console.log(a);}// 状况2onClick() {  setTimeout(() => {    this.setState({a: 1});    console.log(a);  })}

其中,状况2中a打印后果是1

之所以会有这种状况,是React晚期实现批处理时的瑕疵造成的,并不是什么无意为之的个性。

React应用Fiber架构重构后,齐全能够躲避这个瑕疵。但为了与老版本行为保持一致,刻意实现成这样。

所以,在咱们的示例中,这两个更新不会受到优先级的影响,但会受到为了兼容老版本造成的影响:

setTimeout(() => {  setA(9000);  BtnRef.current?.click();}, 2000);

React的执行流程如下:

  1. a触发更新,因为是在setTimeout中触发的,所以会同步执行后续更新流程
  2. a对应更新渲染到页面中
  3. b触发更新,因为是在setTimeout中触发的,所以会同步执行后续更新流程
  4. b对应更新渲染到页面中

总结

React作为一款保护了快10年的框架,在经验重大版本更新后要放弃框架行为前后一致,实属不易。

更新程序的变动对个别利用影响不大。

然而,如果你的利用依赖更新后页面中以后的值作出后续判断,那么须要留神降级到v18后的这些轻微变动。