React 17
**有三种模式 **
legacy 模式:ReactDom.render(, rootNode),在17中默认是legacy 模式
blocking 模式:ReactDOM.createBlockingRoot(rootNode).render()
concurrent 模式:ReactDOM.createRoot(rootNode).render(),这个模式开启了所有的新功能
批量处理:
legacy模式 在合成事件中自动开启批量处理的功能, 如果在非react事件,想使用批量处理的功能,必须使用unstable_batchedUpdates
在blocking 和 concurrent中,任何setSate在默认情况下都是批量处理
React 18
- 放弃了对ie11的支持, React 17的legacy模式,依旧保留(会有一个warning)
Render APi
- 入口,卸载和ReactDOM的引入不一样
// React 17采用的
import ReactDOM from 'react-dom'
ReactDom.render(<APP />, rootNode)
// React 18采用的是
import ReactDOM from 'react-dom/client'
ReactDOM.createRoot(rootNode).render(<App />)
依然保留了legacy模式
// 卸载组件
React 17
ReactDOM.unmountComponentAtNode(root);
React 18
root.unmount();
- 删除了render的回调函数
// React 17
const root = document.getElementById('app')
ReactDOM.render(<App />, root, () => { console.log('渲染完成'); })
// React 18
const root = document.getElementById('app')
ReactDOM.createRoot(root).render(<App />)
3.TypeScript 的类型定义
// React 17
interface TextProps { color: string; }
const Text: React.FC<TextProps> = ({ children }) => {
// 在 React 17 的 FC 中,默认携带了 children 属性
return <div>{children}</div>;
};
export default Text;
// React 18
interface TextProps {
color: string;
children?: React.ReactNode;
}
const Text: React.FC<TextProps> = ({ children }) => {
// 在 React 18 的 FC 中,不存在 children 属性,需要手动申明
return <div>{children}</div>;
};
export default Text;
自动批处理(batchedUpdates)
React 17
legacy模式 在合成事件中自动开启批量处理的功能, 如果在非react事件,想使用批量处理的功能,必须使用unstable_batchedUpdates
非react事件: Promise, setTimeout, 原声事件等
eg.1
// React 事件
const App = {
const [num, setNum] = useState(0)
const addNum = () => {
setNum(num => num + 1)
setNum(num => num + 1)
}
console.log('App 组件渲染')
return (
<div onClick={addNum}>
点击:{num}
</div>
)
}
// 1.点击1次
// 2,打印1次 ‘App 组件渲染’
// 3.点击2次
// 4,打印2次 ‘App 组件渲染’
// React 事件采用自动批处理
eg.2
// 使用setTimeout
// 修改一下addNum 事件
const App = {
const [num, setNum] = useState(0)
const addNum = () => {
setTimeout(() => {
setNum(num => num + 1)
setNum(num => num + 1)
})
}
console.log('App 组件渲染')
return (
<div onClick={addNum}>
点击:{num}
</div>
)
}
// 1.点击1次
// 2,打印2次 ‘App 组件渲染’
// 3.点击2次
// 4,打印4次 ‘App 组件渲染’
// setTimeout中,setSate 没有合并
React 18
注意: 在严格模式下会render 2次
请安装了React DevTools
,第二次渲染的日志信息将显示为灰色
React18的出现 添加了React17 在非React事件(如:setTimiout, 原声事件等)不能批量更新的功能
// 使用以上 eg.2
const App = {
const [num, setNum] = useState(0)
const addNum = () => {
setTimeout(() => {
setNum(num => num + 1)
setNum(num => num + 1)
})
}
console.log('App 组件渲染')
return (
<div onClick={addNum}>
点击:{num}
</div>
)
}
// 1.点击1次
// 2,打印1次 ‘App 组件渲染’
// 3.点击2次
// 4,打印2次 ‘App 组件渲染’
// setTimeout 已经采用了自动批量更新
以下有一个特殊的案列, 不会合并
import { useState } from 'react'
function App() {
const [num, setNum] = useState(0)
const addNum = async() => {
await setNum(num => num + 1)
setNum(num => num + 1)
}
console.log('App 渲染')
return (
<div onClick={addNum} className="App">
click: {num}
</div>
);
}
export default App;
第一个setNum(num => num + 1) 可以理解为peomise.resolve 的时候执行
第二个setNum(num => num + 1) 可以理解为promise.then 的时候执行
flushSync
如果你还是想使用批量更新,那你就不得不考虑 flushSync
import { useState } from 'react'
import { flushSync } from 'react-dom';
function App() {
const [num, setNum] = useState(0)
const addNum = () => {
flushSync(() => {
setNum(num => num + 1)
})
flushSync(() => {
setNum(num => num + 1)
})
}
console.log('App 渲染', num)
return (
<div onClick={addNum} className="App">
click: {num}
</div>
);
}
export default App;
点击click, 会渲染2次
注意: 如果flushSync里面有多个setState, setState 依旧会批量更新
Strict Mode
当你使用严格模式时,React 会对每个组件进行两次渲染,以便你观察一些意想不到的结果。在 React 17 中,取消了其中一次渲染的控制台日志,以便让日志更容易阅读。
为了解决社区对这个问题的困惑,在 React 18 中,官方取消了这个限制。如果你安装了React DevTools,第二次渲染的日志信息将显示为灰色,以柔和的方式显式在控制台
Hook Api
useDeferredValue
useDeferredValue
接受一个值并返回该值的新副本,该副本将推迟到更紧急的更新。如果当前渲染是紧急更新的结果,比如用户输入,React 将返回之前的值,然后在紧急渲染完成后渲染新的值(可以理解成:React 将在其他工作完成后立即进行更新)
如:当前渲染是紧急更新的结果,比如用户输入,React 将返回之前的值,然后在紧急渲染完成后渲染新的值。
本质上和 startTransition
是一个延时更新的任务
没有使用useDeferredValue的执行堆栈图
const [list, setList] = useState([]);
useEffect(() => {
setList(new Array(10000).fill(null));
}, []);
return (
<>
{list.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
使用useDeferredValue的执行堆栈图
const [list, setList] = useState([]);
useEffect(() => {
setList(new Array(10000).fill(null));
}, []);
const deferredList = useDeferredValue(list);
return (
<>
{deferredList.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
通过对比,可以看到任务被拆分到每一帧不同的 task
中,
useTransition
useTransition和useDeferredValue 作用基本类似,都是标记成了延迟更新
任务。
不同:useTransition
是把更新任务变成了延迟更新任务,而 useDeferredValue
是产生一个新的值,这个值作为延时状态。(一个用来包装方法,一个用来包装值)
startTransition: 允许您将提供的回调中的更新标记为转换(简单来说,就是被 startTransition
、 回调包裹的 setState
触发的渲染被标记为不紧急渲染,这些渲染可能被其他紧急渲染
所抢占)
isPending: 指示转换何时处于活动状态以显示挂起状态
function App() {
const [isPending, startTransition] = useTransition();
const [list, setList] = useState([]);
function handleClick() {
startTransition(() => setList(new Array(10000).fill(null)))
}
console.log(isPending, 'isPending')
return (
<>
{
!isPending && list.map((_, i) => (
<div key={i}>{i}</div>
))
}
<button onClick={handleClick}>click</button>
</>
);
}
export default App;
点击click, isPending的状态
useId
useId
是一个钩子,用于生成在服务器和客户端之间稳定的唯一 ID,同时避免hydration mismatches
Note: useId不适用于在列表中生成key。key应该从您的数据中生成。
useSyncExternalStore
useSyncExternalStore
是一个新的api,经历了一次修改,由 useMutableSource
改变而来,主要用来解决外部数据tearing
问题
(tearing): 假设我们的渲染任务被分成了两部分(注意不是render两次,而是一次render渲染分成两部分),两部分都读取了外部状态A。如果两部分任务之间,浏览器处理了一个事件使得外部状态A发生了变化,那前半部分的任务读取到的是旧值,后半部分读取到的却是新值,这就造成了渲染结果的不一致性
useSyncExternalStore
主要对于框架开发者,比如 redux
,它在控制状态时可能并非直接使用的 React
的 state
,而是自己在外部维护了一个 store
对象,用发布订阅模式
实现了数据更新,脱离了 React
的管理,也就无法依靠 React
自动解决撕裂问题。因此 React
对外提供了这样一个 API
useInsertionEffect
function useCSS(rule) {
useInsertionEffect(() => {
if (!isInserted.has(rule)) {
isInserted.add(rule);
document.head.appendChild(getStyleForRule(rule));
}
});
return rule;
}
function Component() {
let className = useCSS(rule);
return <div className={className} />;
}
这个 Hooks 只建议 css-in-js
库来使用。 这个 Hooks 执行时机在 DOM
生成之后,useLayoutEffect
之前,只是此时无法访问 DOM
节点的引用,一般用于提前注入 <style>
脚本。