原文链接
首先现在通过 react 17.0.2 创建新项目,会直接提示需要更新 api:
v 17 的版本主要是是垫脚石版本,用以稳定 CM;V18 作为 React 的下一个大版本将关注点放在了并发模式上也就是谈论了很久的(Concurrent Mode);
Root API 的改变
如前面截图,通过 react 17.0.2 创建新项目,会直接提示需要更新 api;从原有的 ReactDOM.render 修改为 createRoot;
具体可以参考:https://github.com/reactwg/react-18/discussions/5
修改渲染 API:
import * as ReactDOM from 'react-dom';
function App() {
return (
<div>
<h1>Hello World</h1>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement, () => console.log("renderered"));
修改为:
import * as ReactDOM from 'react-dom';
function App({ callback }) {
// Callback will be called when the div is first created.
return (
<div ref={callback}>
<h1>Hello World</h1>
</div>
);
}
const rootElement = document.getElementById("root");
const root = ReactDOM.createRoot(rootElement);
root.render(<App callback={() => console.log("renderered")} />);
ref 属性
这里简单提一下 ref 属性;
上面的以回调的形式给 ref 传入函数,只会触发一次;
对于 ref 属性,React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。
ref 会在 componentDidMount 或 componentDidUpdate 生命周期钩子触发前更新。
如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。
import logo from './logo.svg';
import './App.css';
import { useState } from 'react';
function App() {
const [num, setNum] = useState(0)
const callback= (e) => {
console.log(e)
console.log("renderered")
}
const onhandleClick = () => {
console.log(num)
setNum(num + 1)
}
return (
<div className="App" ref={callback}>
<header className="App-header" onClick={onhandleClick}>
<img src={logo} className="App-logo" alt="logo" />
<h1>{num}</h1>
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
CM 是什么
react 渲染模式
首先我们需要知道 react 渲染的几种模式:
具体看参考官方这篇文章:https://zh-hans.reactjs.org/docs/concurrent-mode-adoption.html
react 渲染过程
然后我们需要了解一下 react 渲染的过程:
- render 阶段:在render阶段会计算一次更新中变化的部分(通过diff算法),因组件的render函数在该阶段调用而得名。render阶段可能是异步的(取决于触发更新的场景)。
- commit 阶段:在commit 阶段会将 render 阶段计算的需要变化的部分渲染在视图中。对应 ReactDOM 来说会执行 appendChild、removeChild 等。commit 阶段一定是同步调用(这样用户不会看到渲染不完全的 UI)
我们通过 ReactDOM.render 创建的应用属于 legacy 模式。在该模式下一次 render 阶段对应一次 commit 阶段。
如果我们通过ReactDOM.createRoot 创建的应用属于 CM(concurrent模式)在CM下,更新有了优先级的概念,render阶段可能被高优先级的更新打断。
所以 render 阶段可能会重复多次(被打断后重新开始)。
可能多次 render 阶段对应一次 commit 阶段。
CM 模式存在很多新特性:异步可中断更新、优先级调度、批量更新等;
节点自动批量更新 (Automatic batching )
v 17 仅在事件处理函数中实现。而 Promise 链、异步代码或者原生事件处理函数的使用会打破这种行为(在这些场景中批处理会失效)。在 v 18 中,自动批处理会在原生事件处理函数、Promise 链和异步代码自动完成。
function handleClick() {
// React 17: 重新渲染发生在所有状态都更新之后。这被称为批处理。
// 这也是 React 18 的默认行为。
setIsBirthday(b => !b)
setAge(a => a + 1)
}
// 在下面代码块中,React 18 会自动批处理,但是 React 17 不会。
// 1. Promises:
function handleClick() {
fetchSomething().then(() => {
setIsBirthday(b => !b)
setAge(a => a + 1)
})
}
// 2. 异步代码:
setInterval(() => {
setIsBirthday(b => !b)
setAge(a => a + 1)
}, 5000)
// 3. 原生事件监听器:
element.addEventListener("click", () => {
setIsBirthday(b => !b)
setAge(a => a + 1)
})
如果某些关键组件不想被自动更新(比如某个状态更改后要立刻从 DOM 中获取一些内容)可以使用 ReactDOM.flushSync() 退出操作:
import { flushSync } from 'react-dom'; // Note: react-dom, not react
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React has updated the DOM by now
flushSync(() => {
setFlag(f => !f);
});
// React has updated the DOM by now
}
具体可以查看这篇官方文章;
其他:
- 新的 Suspense fallback
- 新的 Suspense SSR 架构
参考
https://juejin.cn/post/6974617278784471048
https://juejin.cn/post/7014683796821770247
https://juejin.cn/post/6968777636801675301
https://zh-hans.reactjs.org/docs/concurrent-mode-adoption.html