1. 过渡更新
过渡(transition)更新是 React 中一个新的概念,用于区分紧急和非紧急的更新。
- 紧急更新 对应直接的交互,如输入,点击,按压等。
- 过渡更新 将 UI 从一个视图过渡到另一个。
总体而言,从React 16到18,React逐步增强了性能、稳定性、易用性和对未来技术的适应能力,同时降低了开发者在处理复杂状态和异步交互时的难度。
像输入,点击,按压等紧急更新,需要立刻响应以符合人们对物理对象行为的预期。否则,他们就会觉得“不对劲”。但是,过渡更新不太一样,因为用户对感知到屏幕上的每一个中间值这件事是没有预期的。
举个例子,当我们在一个下拉菜单中选择了一个过滤器,你期望的是这个过滤器按钮在你点击的时候立即就能响应。然而,实际结果可能是不连贯的过渡。这样一个较短的延迟是难以察觉的,而且这往往也是能符合预期的。并且如果你在渲染完成之前,再次改变了过滤器,你需要关心的其实只是最新的结果。
通常情况下,为了更好的用户体验,一个用户输入应该同时产生一个紧急更新和一个过渡更新。你可以在一个输入事件中使用 startTransition API 告诉 React 哪些更新是紧急更新,哪些又是过渡更新:
import { startTransition } from 'react';
// 紧急更新: 显示输入的内容
setInputValue(input);
// 将任何内部的状态更新都标记为过渡更新
startTransition(() => {
// 过渡更新: 展示结果
setSearchQuery(input);
});
被包裹在 startTransition 中的更新会被处理为过渡更新,如果有紧急更新出现,比如点击或者按键,则会中断过渡更新。如果一个过渡更新被用户中断(比如,快速输入多个字符),React 将会抛弃未完成的渲染结果,然后仅渲染最新的内容。
- useTransition: 一个用于开启过渡更新的 Hook,用于跟踪待定转场状态。
- startTransition: 当 Hook 不能使用时,用于开启过渡的方法。
并发渲染中将会加入过渡更新,允许更新被中断。如果更新内容被重新挂起,过渡机制也会告诉 React 在后台渲染过渡内容时继续展示当前内容(查看 Suspense 意见征求 了解更多信息)。
2. 新的 Hook
2.1.1. useId
useId 是一个新的Hook,用于生成在客户端和服务端两侧都独一无二的 id,避免 hydrate 后两侧内容不匹配。它主要用于需要唯一 id 的,具有集成 API 的组件库。这个更新不仅解决了一个在 React 17 及更低版本中的存在的问题,而且它会在 React 18 中发挥更重要的作用,因为新的流式服务端渲染响应 HTML 的方式将是无序的,需要独一无二的 id 作为索引。参阅文档。
Note
useId不是 为了生成 列表中的 key。key 应该根据你的数据生成。
2.1.2. useTransition
useTransition 和 startTransition 让你能够将一些状态更新标记为过渡更新。默认情况下,状态更新都被视为紧急更新。React 将允许紧急更新(例如,更新一个文本输入内容)打断过渡更新(例如,渲染一个搜索结果列表)。参阅文档。
2.1.3. useDeferredValue
useDeferredValue 允许推迟渲染树的非紧急更新。这和防抖操作非常相似,但是有一些改进。它没有固定的延迟时间,React 会在第一次渲染在屏幕上出现后立即尝试延迟渲染。延迟渲染是可中断的,它不会阻塞用户输入。参阅文档。
2.1.4. useSyncExternalStore
useSyncExternalStore 是一个新的 Hook,允许使用第三方状态管理来支持并发模式,并且能通过对 store 进行强制更新实现数据同步。对第三方数据源的订阅能力的实现上,消除了对 useEffect 的依赖,推荐任何 React 相关的第三方状态管理库使用这个新特性。参阅文档。
Note
useSyncExternalStore 旨在供库使用,而不是应用程序代码。
2.1.5. useInsertionEffect
useInsertionEffect 是一个新的 Hook ,允许 CSS-in-JS 库解决在渲染中注入样式的性能问题。除非你已经建立了一个 CSS-in-JS 库,否则我们不希望你使用它。这个 Hook 将在 DOM 变更发生后,在 layout Effect 获取新布局之前运行。这个功能不仅解决了一个在 React 17 及以下版本中已经存在的问题,而且在 React 18 中更加重要,因为 React 在并发渲染时会为浏览器让步,给它一个重新计算布局的机会。参阅文档。
Note
useInsertionEffect 旨在供库使用,而不是应用程序代码。
3. 并发模式
并发渲染本身并不是一个功能。它是一个新的底层机制,使得 React 能够同时准备多个版本的 UI。你可以把并发视为一种底层实现的细节——它解锁了许多新功能因而非常有价值。React 在底层实现上使用了非常复杂的技术,如优先队列和多级缓冲。但是你不会在任何公共 API 中感知到这些。
不过,并发模式 React 比典型的实现细节更重要──它是 React 核心渲染模型的基础性更新。因此,尽管了解并发渲染底层工作原理不是很重要,但如果是为了追求更高的技术层次,倒是值得去了解它。
并发模式的一个关键特性是渲染可中断。当首次升级到 React 18,在加入任何并发功能之前,更新内容渲染的方式和 React 之前的版本一样——通过一个单一的且不可中断的同步事务进行处理。同步渲染意味着,一旦开始渲染就无法中断,直到用户可以在屏幕上看到渲染结果。
在并发渲染中,情况并不总是如此。React 可能开始渲染一个更新,然后中途挂起,稍后又继续。它甚至可能完全放弃一个正在进行的渲染。React 保证即使渲染被中断,UI 也会保持一致。为了实现这一点,它会在整个 DOM 树被计算完毕前一直等待,完毕后再执行 DOM 变更。这样做,React 就可以在后台提前准备新的屏幕内容,而不阻塞主线程。这意味着用户输入可以被立即响应,即使存在大量渲染任务,也能有流畅的用户体验。
另一个例子是可重用状态。并发 React 可以从屏幕中移除部分 UI,然后在稍后将它们再添加回来,并重用之前的状态。例如,当用户来回切换标签页,React 应该能够立即将屏幕恢复到它先前的状态。在即将到来的次要版本中,我们计划添加一个新的名为 <Offscreen> 的组件,它实现了这种模式。同样地,你将能够使用 Offscreen 在后台准备新的 UI,在显示前就准备完毕以便快速响应。
并发渲染是一个 React 中非常强大的工具,并且我们大多数新功能都是利用了它的优势来创建的,包括 Suspense,transition 和流式服务端渲染。但是在并发渲染这个方向,React 18 也仅仅只是实现我们最终目标的第一步
在React 18之前,渲染是一个单一的、不间断的、同步的事务,一旦渲染开始,就不能被打断。这是因为早期采用的是“stack reconciler"调度(类似串行调度),stack reconciler采用递归的方式创建虚拟DOM并提交Dom Mutation,整个过程同步并且无法中断工作或进行拆分。如果组件树的层级很深,递归会占用线程很多时间,递归更新时间超过了16ms,用户交互就会卡顿。
React 18是并发渲染,并发是React渲染机制的一个基础性更新,React可以进行任务挂起(暂停)、恢复、中止、插入高优任务。这使得React可以快速响应用户的交互,即使它正处于一个繁重的渲染任务中。
并发是React渲染机制的一个基础性更新,suspense、流式服务器渲染和transitions等新功能都是由并发渲染提供的。
3.1. 使用场景
- 游戏应用:游戏应用有频繁的交互,并发模式可以用来提升交互体验。当然,很多场景其实也可以用防抖和节流来优化。
- 地图应用:地图应用往往有大量的数据渲染,会带来复杂的计算,开启并发模式能起到一定优化作用。
- 机器学习应用:比如用Tesorflow.js开发浏览器人工智能。因为机器学习的计算大多是CPU密集型的且计算量繁大,理论上也是并发模式的适用场景之一。
3.2. react 中并发模式和 fiber 有什么关系
- React Fiber 架构:
-
- React Fiber 是 React 16 中引入的新的核心算法,旨在提高 React 的可扩展性和响应性。
- 在 Fiber 架构中,React 将更新过程拆分成多个较小的任务(或称为“fibers”),这使得 React 可以在主线程上进行更精细的调度。
- 这种架构允许 React 在长时间运行的任务中进行中断和恢复,从而使主线程可以处理其他高优先级任务,如用户输入或动画。
- 并发模式(Concurrent Mode):
-
- 并发模式是 React 18 中引入的一种新模式,它建立在 Fiber 架构之上。
- 在并发模式下,React 可以将渲染工作切分为多个较小的任务,并根据优先级在主线程上进行调度。这允许页面在渲染过程中保持响应性,特别是在低端设备或大型应用程序中。
- 并发模式还引入了新的特性,如过渡模式(Transition Mode)和延迟渲染(Deferred Rendering),这些都有助于提高应用的性能和响应性。
关系:
- Fiber 架构为并发模式提供了基础。没有 Fiber 架构的细粒度任务调度能力,并发模式是不可能实现的。
- 并发模式是 Fiber 架构的一个高级应用,它充分利用了 Fiber 架构的特性,使得 React 可以在保持应用响应性的同时处理复杂的 UI 更新。
3.3. 并发和并行
并发:指应用能够交替执行不同的任务,其实并发有点类似于多线程的原理,多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已.
就类似于你,吃一口饭喝一口水,以正常速度来看,完全能够看的出来,当你把这个过程以n倍速度执行时…可以想象一下.
并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行
3.4. 18 版本之前 没有并发模式 fiber 主要有什么作用
一句话理解:无并发模式之前 fiber 可以让任务暂停并根据优先级执行 并支持异步渲染,有并发模式 可以让任务交替执行
- 任务拆分与可中断性:在之前的 React 版本中,组件的渲染和更新是递归进行的,且不可中断。这在处理大型或复杂的 UI 时可能导致长时间的阻塞,从而影响应用的响应性。Fiber 架构通过将渲染任务拆分成更小的单元(即 "fibers"),允许 React 在执行过程中中断当前任务,转而处理其他更高优先级的任务,如用户交互或动画。
- 优先级调度:Fiber 架构引入了一个调度器(Scheduler),它可以根据任务的优先级来管理任务的执行顺序。高优先级的任务可以打断低优先级的任务,确保重要的更新能够尽快得到处理,从而提高应用的响应性和用户体验。
- 异步渲染与更新:Fiber 架构为异步渲染和更新铺平了道路。通过将渲染任务切分,React 可以在主线程空闲时逐步完成这些任务,而不是一次性同步完成所有渲染工作。这有助于减少页面在更新时的卡顿现象,提升用户界面的流畅性。
- 为并发模式做准备:虽然 React 16 中还没有正式的并发模式,但 Fiber 架构的引入是为后续版本中实现并发模式做技术铺垫。它改变了 React 的工作方式,使得未来的并发渲染成为可能。
3.5. 并发在 Suspense 和 useTransition 中的应用
- Suspense 属于 IO 耗时型
- useTransition属于 CPU 耗时型
4. 自动批处理
React 17 不会在事件处理程序之外进行批处理,比如不会在一个 promise 中合并处理 setState
批处理是指,当 React 在一个单独的重渲染事件中批量处理多个状态更新以此实现优化性能。如果没有自动批处理的话,我们仅能够在 React 事件处理程序中批量更新。在 React 18 之前,默认情况下 promise、setTimeout、原生应用的事件处理程序以及任何其他事件中的更新都不会被批量处理;但现在,这些更新内容都会被自动批处理:
从 React 18 开始createRoot,所有更新都将自动批处理,无论它们来自何处。
这意味着超时、promise、本机事件处理程序或任何其他事件内部的更新将以与 React 事件内部的更新相同的方式进行批处理。我们希望这会减少渲染工作,从而提高应用程序的性能:
4.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
}
4.2. 对 hooks 有什么影响
暂时没影响,有的话请提给 react 官方
4.3. 对 class 有影响
请记住, React 事件处理程序期间的更新始终是批处理的,因此对于这些更新,没有任何更改。
在类组件中存在一些边缘情况,这可能会成为问题。
类组件有一个实现怪癖,可以同步读取事件内部的状态更新。这意味着您将能够this.state在调用之间读取setState:
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
// { count: 1, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
在 React 18 中,情况不再如此。由于所有更新都是setTimeout批处理的,React 不会同步渲染第一个结果setState——渲染发生在下一个浏览器更新期间。所以渲染还没有发生:
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
// { count: 0, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
handleClick = () => {
setTimeout(() => {
ReactDOM.flushSync(() => {
this.setState(({ count }) => ({ count: count + 1 }));
});
// { count: 1, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
5. 新的 Suspense 特性
Suspense 的底层实现依赖于 错误边界(Error Boundaries) 组件,从描述中我们知道, 错误边界 是一种组件,生成一个 错误边界 组件也很容易,任何实现了 static getDerivedStateFromError() 静态方法的 class 组件 就是一个 错误边界 组件。
错误边界 组件的主要作用在于, 错误边界 组件能够捕获子组件(不包括自身) throw 出的 Error,
错误边界 使我们在子组件树崩溃时,可以渲染 备用UI 而非 错误UI
错误边界 组件能够捕获子组件(不包括自身) throw 出的 任何东西 。可以将 Suspense 当做一种特殊 错误边界 组件,当 Suspense 捕获到子组件抛出的时 Promise 时会暂时挂起 Promise 渲染 fallback UI ,当其 Resolved 之后重新渲染。
react-cache 暂时处于实验性阶段,是对 React 如何获取数据的一种新的思考方式
Suspense 允许你声明式地为一部分还没有准备好被展示的组件树指定加载状态:
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
Suspense 使得“UI 加载状态”成为了 React 编程模型中最高级的声明式概念。我们基于此能够构建更高级的功能。
本质上讲Suspense内的组件子树比组件树的其他部分拥有更低的优先级。
几年前,我们推出了一个受限制版的 Suspense。但是唯一支持的场景就是用 React.lazy 拆分代码,而且在服务端渲染时完全没有作用。
在 React 18 中,我们已经支持了服务端 Suspense,并且使用并发渲染特性扩展了其功能。
React 18 中的 Suspense 在与 Transition API 结合时效果最好。如果你在 Transition 期间挂起,React 不会让已显示的内容被后备方案取代。相反,React 会延迟渲染,直到有足够的数据,以防止出现加载状态错误。
从React 18开始,React.lazy 和 <Suspense> 更多地被用于与新的并发特性相结合,在之前的React版本中,Suspense 主要用于代码拆分和懒加载
5.1. 替换原有的 loading 状态方案
- 可以替换原有的 loading 状态方案,减少多余的状态维护甚至 包裹路由时支持路由切换
但是直接用axios或者fetch是无法进入suspense的fallback的,但是react提供了一个库供我们使用react-cache(暂不建议使用的),它具体是做什么的,原理是什么,我们后面在讨论,这里先体验一下效果如何。
使用suspense的方式,在开发的时候完全不用维护loading状态,而且还有一个比较大的差别,suspense中的list是没有使用state的,它获取的点是在B渲染时,而loading获取数据则是发生在A渲染后
react-cache 个当promise处于Pending时,会throw出这个promise 而此时suspense看到这个promise自然就知道还处于数据请求中,就会展示fallback中的内容,当这个promise已决时,则代表数据请求结束,suspense就应该展示数据内容。
1. 事先throw
2. 在 completeWork 之前 catch 住
3. 然后添加到 updateQueue 里
4. updateQueue 批量更新
6. 新的客户端和服务端渲染 APIs
我们利用这次版本更新的机会,重新设计了我们为在客户端和服务端进行渲染所暴露的 API。这些更改允许用户在升级到 React 18 使用新的 API 时,也能继续使用 React 17 中的旧 API。
6.1.1. React DOM Client
这些新的 API 现在可以从 react-dom/client 中导出:
- createRoot:为 render 或者 unmount 创建根节点的新方法。请用它替代 ReactDOM.render。如果没有它,React 18 中的新功能就无法生效。
- hydrateRoot:hydrate 服务端渲染的应用的新方法。使用它来替代 ReactDOM.hydrate 与新的 React DOM 服务端 API 一起使用。如果没有它,React 18 中的新功能就无法生效。
createRoot 和 hydrateRoot 都能接受一个新的可选参数叫做 onRecoverableError,它能在 React 在渲染或者 hydrate 过程发生错误后又恢复时,做日志记录对你进行通知。默认情况下,React 会使用 reportError,如果在老旧版本浏览器中,则会使用 console.error。
6.1.2. React DOM Server
这些新的 API 现在可以从 react-dom/server 中导出,并且在服务端端完全支持流式 Suspense:
- renderToPipeableStream:用于 Node 环境中的流式渲染。
- renderToReadableStream:对新式的非主流运行时环境,比如 Deno 和 Cloudflare workers。
现有的 renderToString 方法仍然可以使用,但是并不推荐这样做。
7. 新的严格模式行为
在未来,我们希望新增一个功能,允许 React 在保留状态的同时添加和移除 UI。例如,当一个用户标签页切出又切回时,React 应该能够立即将之前的页面内容恢复到它先前的状态。为了实现这一点,React 将在卸载后又重新挂载组件树时,复用之前的状态。
这个功能将给 React 应用带来更好的开箱即用能力,但要求组件能够灵活应对多次安装和销毁的副作用。对于大多数副作用不需要任何改动也依然能够生效,但是部分副作用需要保证它们只进行一次挂载或销毁。
为了利于暴露这些问题,React 18 为严格模式下的开发环境引入了一个新的检查机制。每当组件第一次挂载时,这个检查机制将自动卸载又重新挂载每个组件,并在第二次挂载时复用先前的状态。
在这个变更之前,React 是在挂载组件时产生一些副作用:
- React 装载组件
-
- layout Effect 创建
- Effect 创建
在 React 18 的严格模式下,React 在开发模式下将会模拟组件的卸载和挂载:
- React 挂载组件
-
- layout Effect 创建
- Effect 创建
- React 模拟卸载组件
-
- layout Effect 销毁
- Effect 销毁
- React 模拟挂载组件,并复用之前的状态
-
- layout Effect 创建
- Effect 创建