上一章讲解了React源码解析系列(十一) -- react生命周期与事件系统的解读,这使得我们对React生命周期,事件系统更加清晰了一些,React
作为一个流行的框架,在国内国外都享有盛名,无非归结于几点:
React
维护团队来自facebook
,背景强大且稳定。React
是一个构建用户UI
界面的js
库,jsx
语法使得程序组件编写起来更为顺手。- 当然还有一些其他的优势,这里也不再复述了。
这一章一起来探讨一下React18
的新特性(本章不会深入源码)。
React的发布时间线
- 0.3.0 (2013 年 05 月 29 日)首次发布
- 远古时期的
React.js
库
- 远古时期的
- 15.0.0(2016 年 04 月 07 日)React15发布
- 解决
document.createElement
生成HTML的问题。 - 解决渲染
null
节点不再使用<noscript>
,而是用注释节点问题。 - 解决
纯文本
节点的渲染与注释
节点的渲染问题。 - 功能组件可以返回
null
与SVG
的支持。
- 解决
- 16.0.0(2017 年 09 月 26 日)React16发布
- 解决
React
与浏览器
之间的兼容
问题,主要是Set
、Map
、requestAnimationFrame
问题。 - 更精准的自
子组件
到根组件
的错误的定位问题。 - 新增
ReactDOM.createPortal()
方法以及调整了生命周期的一些方法。比如componentWillUnMount
会比componentWillMount
先执行。
- 解决
- 16.8.0(2019 年 02 月 06 日)Hooks版本发布
- 新增
hooks
函数,让函数组件
能够拥有类组件
的功能。 - 使用
Object.is
进行浅比较依赖。
- 新增
- 17.0.0(2020 年 10 月 20 日)React17发布
- 新的
jsx
解析编译环境react/jsx-runtime
的支持。 - 事件委托到
root
上,而不是document
上。
- 新的
- 18.0.0(2022 年 03 月 29 日)React18发布
- 新增
createRoot
方法,废弃ReactDOM.render
方法。 - 新增
useId
、startTransition
、useDeferredValue
、useSyncExternalStore
、useInsertionEffect
钩子或方法。 - Automatic batching自动批处理。
- Stricter Strict Mode严格模式。
- 新增
Adopting Concurrent Mode
其实在react18
里面,createRoot
方案并不是意象而生的,其实早在v17
版本里面就提出过了。
- 传统模式(Legacy Mode):
ReactDOM.render(<App />, rootNode)
. 这就是React
应用程序今天使用的。没有计划在可观察的未来移除传统模式——但它将无法支持这些新功能。 - 封锁模式(Blocking Mode):
ReactDOM.createBlockingRoot(rootNode).render(<App />)
。它目前是实验性的。它旨在作为想要获得并发模式功能子集的应用程序的第一个迁移步骤。 - 并发模式(Concurrent Mode):
ReactDOM.createRoot(rootNode).render(<App />)
. 它目前是实验性的。未来,等它稳定下来后,我们打算让它成为默认的 React 模式。此模式启用所有新功能。
// React 17 with Legacy Mode
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
document.getElementById('root')
);
// React 18 with Concurrent Mode
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<App />
</StrictMode>
);
Automatic Batching
- 首先自动批处理发生在
React
的框架内部,并不需要用户有过多的操作。 - 在
React17
版本的事件处理程序中,批处理适用于少量事件,如setTimeout
等并没有做良好的批处理支持,但是React18
默认给所有事件都做了批处理操作,比如
// App.js
import "./styles.css";
import { useState } from "react";
export default function App() {
const [fristState, setFristStateToUpdate] = useState(0);
const [secondState, setSecondStateToUpdate] = useState(10);
// 打印一次render
const handleBatching = () => {
// 第一次 re-render
setFristStateToUpdate((fristState) => fristState + 10);
// 第二次 re-render
setSecondStateToUpdate((secondState) => secondState + 20);
};
// 3000ms之后打印一次render
const handleBatchingWithSetTimeout = () => {
setTimeout(() => {
// 第一次 re-render
setFristStateToUpdate((fristState) => fristState + 10);
// 第二次 re-render
setSecondStateToUpdate((secondState) => secondState + 20);
}, 3000);
};
// 打印一次render
const handleBatchingWithPromise = () => {
Promise.resolve().then(() => {
// 第一次 re-render
setFristStateToUpdate((fristState) => fristState + 10);
// 第二次 re-render
//setSecondStateToUpdate((secondState) => secondState + 20);
});
//setFristStateToUpdate((fristState) => fristState + 10);
// 第二次 re-render
setSecondStateToUpdate((secondState) => secondState + 20);
};
console.log("render");
return (
<div className="App">
<h3>fristState: {fristState}</h3>
<h3>secondState: {secondState}</h3>
<button onClick={handleBatchingWithSetTimeout}>按钮</button>
</div>
);
}
- 在
React17
版本中,只有在handleBatching
做了批处理,render
只打印了一次。在handleBatchingWithSetTimeout
、handleBatchingWithPromise
中都没有做,都是渲染多次。 - 在
React18
版本中,handleBatching
、handleBatchingWithSetTimeout
、handleBatchingWithPromise
都做了批处理,render
只会打印一次。 - 各位同学可自行去codeSandBox上面去体验,根据不同的
依赖版本
去修改index.js
里面的入口文件,尝试测试一下。
那如果我们有的场景我不想使用批处理,我们该如何做呢?React
官方提供了一个flushSync
方法。
// 如果不想实现批处理更新,只需要用flushSync包裹当前任务,那么之后的任务就不会被批量处理了
flushSync(()=>{
setFristStateToUpdate((fristState) => fristState + 10);
})
useId的使用
useId
的作用是创建唯一标识,比如:
const App = () => {
// 创建表单唯一id
const id = useId();
// 创建ref
const ref = createRef(null);
//获得表单上的id属性
const getId = () => {
console.log(ref.current); // <input type="input" name="react" id=":r0:"></input>
console.log(ref.current.id); // :r0:
};
// 打印初始化创建的id
console.log(id);
return (
<>
<label htmlFor={id}>show id</label>
<br />
<input type="input" name="react" id={id} ref={ref} />
<br />
<button onClick={getId}>get Element</button>
</>
);
}
有什么作用?创建唯一值啊。创建唯一值不是有很多方法吗?比如:
const id = useId(); // :r0:
const id1 = useId(); // :r1:
const id2 = useId(); // :r2:
const id3 = Math.random(2).toFixed(2); // 0.99
const id4 = Symbol(10); // Symbol(10)
useId
创建的值,唯一且稳定,每一次都是:r0:
。- 多个
useId
创建值,则会往下排列。 Math.random
创建的值不稳定
且有局限性
。Symbol
确实唯一,但是它与任何数作比较都是不等
的。
另外React
提出这个hook
的真实用意是为了解决SSR
,服务端与客户端渲染的组件id
不一致的问题的。
Suspense
Suspense
的适用场景
- 代码拆分:将
React
应用拆分为多个模块,只有当用户访问到了组件的时候,才会去加载组件对应的模块。 - 数据加载:在异步请求场景中会因为前后端交互链路耗时比较长、或者因为网络的原因导致前端没有拿到数据,造成白屏的情况。
我们针对这种情况会给组件加一个过渡spin
组件,Suspense
的作用就在于在根组件上面加一个spin
,提高用户体验。
root.render(
<StrictMode>
// maxDuration={100}为控制在100ms之后触发
// fallback={<spin />}为在等待<App />组件渲染完毕之前,显示的组件
<Suspense fallback={<spin />} maxDuration={100}>
<App />
</Suspense>
</StrictMode>
);
New Strict Mode Behaviors
Concurrent rendering
关于并发渲染有两个关键
的点
- 渲染可中断
- 非并发模式中,更新的渲染方式与之前版本的
React
相同——在一个单一的、不间断的、同步的事务中。使用同步渲染,一旦更新开始渲染,在用户可以在屏幕上看到结果之前,没有任何东西可以中断它。 - 并发模式中,
React
可能会开始渲染更新,在中间暂停,然后再继续。它甚至可能完全放弃正在进行的渲染。React
保证即使渲染被中断,UI
也会保持一致。为此,它会等待执行DOM
修改操作,直到完成整个树的渲染。有了这个能力,React
可以在后台准备新的屏幕而不阻塞主线程。这意味着UI可以立即响应用户输入,即使它处于大型渲染任务的中间,从而创造流畅的用户体验。
- 非并发模式中,更新的渲染方式与之前版本的
- 状态可重用
Concurrent React
可以从屏幕上删除部分UI
,然后在重用之前的状态时将它们添加回来。例如,当用户从一个屏幕上移开并返回时,React
应该能够将前一个屏幕恢复到与之前相同的状态。依赖的是组件。
那么如何开启Concurrent rendering
呢?React18
提供了useTransition
,useDeferredValue
两个函数。
useTransition
useTransition
能够提供数据更改到ui
渲染过程的视觉平滑过度的功能,可适用于频繁地触发setState
,或者一次性大量通过setState
去更新视图的场景。用法如下:
import { useState, useTransition } from "react";
const App = () => {
const [state, setState] = useState(0);
const [isPending, startTransition] = useTransition();
// 处理函数
const handleClick = (e) => {
// 处理卡顿情况
startTransition(() => {
setState(e.target.value);
});
};
return (
<>
<input onChange={handleClick} />
{isPending && <span>加载中...</span>}
{!isPending && (
<ul>
{Array(10)
.fill(state)
.map((item) => (
<li>{item}</li>
))}
</ul>
)}
</>
);
};
export default App;
注意startTransition
可以单独从react
中导出,isPending
的作用在于告诉用户,当前还有一批待处理的状态更新,等待执行,在这个阶段你可以通过自定义ui组件进行展示。
useDeferredValue
useDeferredValue
的作用在于把一些优先级较低的更新任务(非紧急更新)推迟到紧急渲染任务之后。
import { useState, useDeferredValue } from "react";
const App = () => {
const [text, setText] = useState("一溪之石");
const deferredText = useDeferredValue(text);
const handleChange = (e) => {
setText(e.target.value);
};
return (
<div className="App">
<input value={text} onChange={handleChange} />
<ul>
{Array(100)
.fill(deferredText)
.map((item) => (
<li>{item}</li>
))}
</ul>
</div>
);
};
export default App;
这里讲的是基础的用法,以及理论上的实现效果。如果要谈论到它的使用场景的话,那应该跟防抖
的场景差不多。只不过他并不是在规定的时间之后执行的(防抖是在规定时间之后执行回调函数)。
useSyncExternalStore
- 它通过强制的同步状态更新,使得外部 store 可以支持并发读取。它实现了对外部数据源订阅时不再需要 useEffect,并且推荐用于任何与 React 外部状态集成的库。
- 注意:这个钩子不是供React用户使用的。
useInsertionEffect
useInsertionEffect
与useLayoutEffect
相似,但是他执行比useLayoutEffect
早,那么在这个阶段无法获取dom
、ref
。- 注意:这个钩子不是供React用户使用的。
总结
通过查阅文献,学习了React18
的新特性。
- Concurrent Mode
- Automatic Batching
- useId的使用
- Suspense使用
- New Strict Mode Behaviors
- Concurrent rendering
- useSyncExternalStore
- useInsertionEffect
到目前为止,React18
版本的新特性也学习完了,前面的十一章讲了主要的React17.0.2
的执行机制,下一章我将会去对前面的这几篇文章做一个系统性的总结
以及会针对这些机制整理一些面试题
,直通车 >>> React源码解析系列(十三) -- 源码解析终结与展望