最近太忙好久不写博客了, 欠了三篇啊!!!!!!!!!!!!!(以后有时间再补上)
本次博客, 是因为同事最近分享了React 18
新出的特性, 觉得长见识了,就一起学学习吧!!了解React 18
新特性。
本文主要参考最近同事分享的一文让你搞懂React18新特性及其实现 , react18新特性及实践总结。
Render API
我们知道, React 17
提供了 三种入口模式:
legacy
模式:表示没有开启新功能,是React 17
的默认模式。1
ReactDOM.render(, rootNode)
blocking
模式:是作为迁移到concurrent
模式的过渡模式。1
ReactDOM.createBlockingRoot(rootNode).render()
concurrent
模式:这模式表示开启了所有的新功能。1
ReactDOM.createRoot(rootNode).render()
而React 18
正式迁移到了 concurrent
模式。但是同时, 用户还是可以继续使用React 17
的下的旧API
(只是会有警告提示)。
1 | // React 17 |
Automatic Batching
批处理是React
将多个状态更新分组到一个渲染中一次获得更好的性能;即:React
将多次的setState
合并到一次进行渲染。
也就是说 setState
并不是试试修改State
的,而是将多次的setState
调用合并起来仅发出一次渲染,即可以减少数据状态存在中间值而导致的不稳定性,也可以提升渲染性能。
在React 18
之前,只能在事件处理程序中批处理更新;举个例子如下:
注意:
index.js
中使用的Render
模式
以上例子中采用的是React 17
版本, 在事件处理程序(handleClick
) 进行了两个setState
。但我们可以看到 log
每次只打印一次。这就表明在事件处理程序中 React 17
采用了批处理更新。
但是在React 18
之前,异步函数(定时器, Promise
等)中的setState
并不会执行合并, 由于丢失上下文,无法做到合并处理,所以每次setState
调用都会立即发出一次重新渲染,除了重复的setState
。举个例子如下:
注意:
index.js
中使用的Render
模式
以上例子中采用的是React 17
版本,在异步事件处理程序(setTimeOut
) 进行了两个setState
。 可以看到每次 log
都会打印两次。说明在异步函数(定时器, Promise
等)中的setState
并不会执行合并批处理。
React 18
则无论是在普通的事件处理程序中还是异步的事件处理程序中, 都会进行批处理。举个例子如下:
注意:
index.js
中使用的Render
模式
以上例子中采用的是React 18
版本,在异步事件处理程序进行了两个setState
。 可以看到每次 log
都会打印一次。说明在React 18
中异步函数(定时器, Promise
等)中的setState
会执行合并批处理。
但是,有些情况下, 我不想要批处理, 那该怎么解决的呢?!!!!!!!
flushSync
官方提供了一个 API
flushSync
来用于退出批处理。flushSync
会以函数为作用域,函数内部的多个setState
仍然为批更新,这样就可以很精确的控制哪些不需要批量的更新。
注意:
index.js
中使用的Render
模式
以上例子中采用的是React 18
版本,在异步事件处理程序进行了两个setState
。但是其中一个使用flushSync
来退出了批更新。结果可以看到每次 log
都会打印两次。
Transitons
产生的原因
Transitons
的直接翻译是“过渡”,本质上是为了解决渲染并发问题所提出。在React
中一旦组件状态改变并触发了重新渲染,则无法停止渲染。直到组件中心渲染完毕,页面才能继续响应用户的交互。
为此React 18
中提出一个新概念叫过渡,用于区分紧急和非紧急更新。
- 紧急更新:表示反映了直接交互,如:键入,单击,按下等等。
- 过渡(非紧急更新):表示UI 从一个视图转换到另外一个视图。
像一些行为,如 打字,点击或者按下等紧急更新需要立即响应, 以符合我们对物理对行为方式的直觉。否则用户就会觉得“不对劲”。但是过渡是不一样的,因为用户不希望在屏幕上看到每个中间值。
看如下的例子: 当滑块滑动的时候,下方的图表会一起更新,然后图表更新是一个cpu
密集型操作,比较耗时,由于阻塞了渲染导致页面失去响应,用户就会非常明显的感受到卡顿。
实际上在这个例子中, 我们滑动滑块的时候,需要做两次更新。一个是滑块的条的更新 ; 另外一个是图表的更新
1 | // 滑块的条的更新 |
在这里代码中, 用户期望第一个更新是即时的, 因为本地浏览器处理这些交互的速度很快,但是第二个更新可能会有点延迟,但是用户也并不期望它是立即完成, 因为用户还有很多工作要做(比如继续输入字符等)、实际上开发人员经常使用 debounce
(防抖)等技术人为的延迟此类更新。
在
React 18
之前,所有的更新都是紧急更新, 这意味着上面这两种这两种状态会被同时渲染,并在渲染完成之前阻塞用户看到的交互的反馈结果。 因此我们缺少了一种方法来告诉React
哪些是紧急更新, 哪些是非紧急更新。
startTransition
React
提供 startTransition
API 就可以解决如上问题,可以通过startTransition
来包装非紧急更新,如果出现更紧急的更新(如点击,键入)等则会中断。
startTransition
接受一个回调函数,用于告知React
需要延迟更新的state
.- 如果某个
state
的更新会导致组件挂起,则应该包裹startTransition
中。
1 | import { startTransition } from 'react'; |
使用后可以明显的感受到,虽然图表的更新还是会有些延迟,但是整体的用户体验相对之前是非常好的。
开发中开发者可以通过startTransition hook
来决定哪些更新被标记为transition
。一旦被标记则代表低优先级执行。即React
知道哪些state
可以延迟更新。通过区分更新优先级,让更高级的时间保持响应,提高用户交互体验,保持页面响应。
在startTransition
中的更新被视为非紧急更新,如果出现更紧急的更新(如点击或按键),更新则会被中断,如果用户中断了startTransition
中的更新(例如连续输入多个字符)、React
将丢掉未完成的过时的渲染工作,只渲染最新的更新。
再来看另外一个例子:输入字符串后展示结果得场景模拟,通过伪造大量搜索结果,模拟容易卡顿的情况。
在这个例子中, 我们输入字符串之后,对输入字符串进行 20000 个节点的渲染。在没有使用startTransition
的情况下, 我们输入一个字符串之后,result
值马上进行响应,列表渲染立即开始,造成卡顿,输入框停止了对用户输入的响应。知道渲染结束,输入框才会继续响应。
同样的例子我们使用startTransition
:
很明显,输入字符串之后,result
值响应被延后,保证了页面的反馈,知道输入结束, 才开始响应result
,渲染列表
startTransition 与 setTimeout 的区别
一个重要的区别是startTransition
不像 setTimeout
那样稍后执行,它是立即执行。传递给startTransition
的函数同步运行,但是函数内部的任何更新都标记为”transitions
“(过渡更新)。React
将在稍后处理更新时使用该信息来决定如何渲染更新。这表示我们开始更新的时间比在定时器中包裹的更新时间要早,在一个网络较快的设备商,两次更新之间的延迟非常小。在一个网速较慢的设备商,延迟会更大,但是UI会保持响应。
另外一个重要的区别是 setTimeout
内存在紧急更新时仍然会锁定页面。当定时器触发时,如果用户仍然在输入或与页面交互,它们仍然阻止与页面交互。但是用startTransition
标记的状态更新是可以中断的,所以它们不会锁定页面。
useTransition
1 |
|
-
useTransiton
接受一个回调函数,用来告诉React
需要延迟更新的state
-
isPeding
是一个布尔值,这是React
告知我们是过渡完成 -
useTransition
接受带有timeoutMs
的延迟响应值,,如果给定的timeoutMs
内未完成, 它会强制执行startTransition
函数内的state
的更新。
useDeferredValue
useDeferredValue
返回一个延迟响应的值,可以让一个state
延迟生效,只有当前没有紧急更新时, 该值才会变成最新值。useDeferredValue
与 useTransition
都是标记了一次非紧急更新。
1 |
|
useDeferredValue
与 useTransition
的异同:
- 相同点:
useDeferredValue
与useTransition
本质上是一样的, 都是标记成非紧急更新任务。 - 不同点:
useTransition
是把更新任务变成了延迟任务,useDeferredValue
是产生一个新值,这个值作为延迟状态。
useDeferredValue
与 debounce
,setTimeout
的区别:debounce
和setTimeout
都会有一个固定的延迟,而useDeferredValue
的值只会在渲染耗费的时间后更新。在性能好的机器上延迟会变小,反之会变长。
useId
useId
是一个新的hook
,用于在客户端上和服务器上生成唯一ID
, 同时避免hydration mismatches
。
简单介绍下SSR
的流程:
在服务端,我们将React
组件渲染成一个字符串,这个过程叫脱水(dehydrate
),字符串以HTML
的形式传送给客户端作为首屏的内容、到了客户端之后React
需要对该组件重新激活,用于参与新的渲染更新等过程, 这个过程叫注水hydrate
。
当我们使用React
进行服务端渲染时会遇到一个问题: 如果当前的组件已经在服务端渲染过了,但是在客户端并没有什么手段知道这个事情,于是客户端会再在渲染一次,这样就造成了冗余的渲染。
因此,React 18
提出了useId
这个hook
来解决这个问题。它使用的是树状的结构(在客户端和服务端绝对稳定)来生成id
useSyncExternalStore
useSyncExternalStore
主要是用来解决外部数据tearing
(撕裂)问题, 是由 useMutableSource
改变而来。
撕裂:在屏幕上看到了一个用一个物体的不同帧的影像,画面仿佛是”撕裂的”。对应
React
中,指使用了过去版本的状态进行画面渲染引起的UI不一致或者崩溃。
撕裂现象:
引入并发渲染后, 渲染十四肯呢个被更高优先级的任务中断, 这也使得tearing
成为可能。但事实React
本身对弈state
的更新做了很多工作来避免这个问题,但是如果我们依赖了外部的状态,比如Redux
, 它在控制状态的时并非直接使用React
的state
, 而是在外部值是维护了一个store
对象,脱离了React
的管理,也就无法依靠React
自定解决撕裂问题,因此React
提供了这样的一个API
。
同样的如上例子,只不过我们使用useSyncExternalStore
创建state
:
1 |
|
获取值的时候:
1 | // 使用 useSyncExternalStore 防止撕裂 |
具体的结果:
参考文章: