乐趣区

关于javascript:悄悄告诉你React18文档里写错的地方

大家好,我卡颂

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 么?

// 状况 1
onClick() {this.setState({a: 1});
  console.log(a);
}
// 状况 2
onClick() {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 后的这些轻微变动。

退出移动版