1. react 最新版本不同
React 16
- 错误边界(Error Boundaries) :React 16 引入了错误边界 ,允许组件捕获其子组件树中发生的JavaScript错误 ,并优雅地渲染回退 UI,而不是整个应用程序崩溃。
- Fiber架构 :虽然不是表面上的新特性,React 16 内部重构为 Fiber 架构,提供更好的任务调度和异步渲染能力,提高了性能和灵活性。
- Portals :允许组件渲染到DOM树的任意位置,而不局限于父组件的DOM范围。
- Context API:官方规范化了上下文API,使状态管理和跨层级组件通信更为方便。
- 生命周期方法更改:废弃了一些旧的生命周期方法,并引入了新的生命周期钩子,如
getDerivedStateFromProps和getSnapshotBeforeUpdate。
React 17
- 事件委托的改进:React 17 最显著的改变在于事件系统的优化,允许React事件处理程序直接添加到实际DOM元素上,而不是像React 16那样在顶层document上代理事件。这让与非React库更好地协同工作成为可能。
- 移除事件池:React 17 删除了事件池机制,简化了事件系统的实现。
- 兼容性改进:此版本主要致力于减少破坏性变更,改进与其他库的兼容性,尤其是针对未来版本的React做铺垫。
React 18
- 并发渲染(Concurrent Mode) :正式推出了并发渲染模式(但仍处于试验阶段),允许React在多个渲染之间切换,根据浏览器空闲时间进行渲染,提升了应用的响应性和交互性。
- Suspense for Data Fetching:进一步完善了Suspense的支持,使之不仅限于代码分割,还可以用于异步数据加载。
- 自动批处理(Automatic Batching) :React 18 提供了自动批处理更新的功能,对同一事件循环内的setState调用进行合并,减少了不必要的渲染次数。
- Server Components(服务器组件) :React 18 Alpha 版本引入了服务器组件的概念,这是一项实验性功能,可以在服务器端生成HTML,只发送给客户端必要的JavaScript,从而改善初始加载速度和SEO表现
-
- React DOM Client 这些新的 API 现在可以从 react-dom/client 中导出:
-
-
- createRoot:为 render 或者 unmount 创建根节点的新方法。请用它替代 ReactDOM.render。如果没有它,React 18 中的新功能就无法生效。
- hydrateRoot:hydrate 服务端渲染的应用的新方法。使用它来替代 ReactDOM.hydrate 与新的 React DOM 服务端 API 一起使用。如果没有它,React 18 中的新功能就无法生效。
-
-
- React DOM Server 这些新的 API 现在可以从 react-dom/server 中导出,并且在服务端端完全支持流式 Suspense:
-
-
- renderToPipeableStream:用于 Node 环境中的流式渲染。
- renderToReadableStream:对新式的非主流运行时环境,比如 Deno 和 Cloudflare workers。
-
React 19
预计将推出 4 个全新 Hooks,这些 Hooks 主要关注 React 中的两个痛点:数据获取和表单。 这些 Hooks 目前在 React 预览版本中作为实验性 API 提供,预计会成为 React 19 的一部分,但是最终发布之前,API 可能会有所变化。
React Compiler 自动记忆化编译器
在使用新编译器以前,我们使用 useMemo、useCallback 和 memo 来手动缓存状态,以减少不必要的重新渲染,这种实现方式虽然可行,但 React 团队认为这并不是他们认为理想的方式,他们一直寻找让 React 在状态变化时自动且只重新渲染必要部分的方案。
React团队表示,新的编译器,目前已经在 instagram 的生产环境中应用,React 团队计划在 Meta 的更多平台中应用,并在后续分享更多细节,相信这对开发者来说是又一次开发范式的改变。
2. 过渡更新
过渡(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 意见征求 了解更多信息)。
3. 新的 Hook
3.1.1. useId
useId 是一个新的Hook,用于生成在客户端和服务端两侧都独一无二的 id,避免 hydrate 后两侧内容不匹配。它主要用于需要唯一 id 的,具有集成 API 的组件库。这个更新不仅解决了一个在 React 17 及更低版本中的存在的问题,而且它会在 React 18 中发挥更重要的作用,因为新的流式服务端渲染响应 HTML 的方式将是无序的,需要独一无二的 id 作为索引。参阅文档。
Note
useId不是 为了生成 列表中的 key。key 应该根据你的数据生成。
3.1.2. useTransition
useTransition 和 startTransition 让你能够将一些状态更新标记为过渡更新。默认情况下,状态更新都被视为紧急更新。React 将允许紧急更新(例如,更新一个文本输入内容)打断过渡更新(例如,渲染一个搜索结果列表)。参阅文档。
3.1.3. useDeferredValue
useDeferredValue 允许推迟渲染树的非紧急更新。这和防抖操作非常相似,但是有一些改进。它没有固定的延迟时间,React 会在第一次渲染在屏幕上出现后立即尝试延迟渲染。延迟渲染是可中断的,它不会阻塞用户输入。参阅文档。
3.1.4. useSyncExternalStore
useSyncExternalStore 是一个新的 Hook,允许使用第三方状态管理来支持并发模式,并且能通过对 store 进行强制更新实现数据同步。对第三方数据源的订阅能力的实现上,消除了对 useEffect 的依赖,推荐任何 React 相关的第三方状态管理库使用这个新特性。参阅文档。
Note
useSyncExternalStore 旨在供库使用,而不是应用程序代码。
3.1.5. useInsertionEffect
useInsertionEffect 是一个新的 Hook ,允许 CSS-in-JS 库解决在渲染中注入样式的性能问题。除非你已经建立了一个 CSS-in-JS 库,否则我们不希望你使用它。这个 Hook 将在 DOM 变更发生后,在 layout Effect 获取新布局之前运行。这个功能不仅解决了一个在 React 17 及以下版本中已经存在的问题,而且在 React 18 中更加重要,因为 React 在并发渲染时会为浏览器让步,给它一个重新计算布局的机会。参阅文档。
Note
useInsertionEffect 旨在供库使用,而不是应用程序代码。
- 对于受 CPU 影响的更新(例如创建 DOM 节点和运行组件代码),并发意味着更紧急的更新可以“中断”已经开始的渲染。
- 对于受 IO 影响的更新(例如从网络中获取代码或数据),并发意味着 React 甚至可以在所有数据到达之前就开始在内存中渲染,无需显示令人讨厌的加载中状态。
4. 并发模式
4.1. 什么是并发
并发是指在操作系统中,同一时间段内多个程序同时启动并在同一处理机上运行,但任一时刻仅有一个程序在运行。并发的关键是能交替处理多个任务,而非同时处理。在React中,并发有助于优化性能,实现非阻塞性的更新和渲染,从而提升应用的响应性和用户体验。在并发模式下,React 可以并行处理多个状态更新。
并发渲染本身并不是一个功能。它是一个新的底层机制,使得 React 能够同时准备多个版本的 UI。你可以把并发视为一种底层实现的细节——它解锁了许多新功能因而非常有价值。React 在底层实现上使用了非常复杂的技术,如优先队列和多级缓冲。
它是 React 核心渲染模型的基础性更新。
并发模式的依赖的一个 关键特性是渲染可中断。
4.2. react 为什么需要并发
React需要并发的原因主要是解决单线程JavaScript在处理耗时任务时阻塞后续执行的问题。在React中,若更新过程耗时,会导致界面卡死且用户无法进行交互,影响用户体验。并发能够让React在进行更新的同时,响应用户的其他交互,如点击事件。React通过将更新和交互看作不同的任务,并在必要时交替执行,从而保持应用的交互性。
4.3. 浏览器的一帧里做了什么?
升级到 React 18 后,默认渲染行为与旧版保持一致,即同步渲染直至完成,不支持中断。而并发渲染是 React 18 引入的新特性,允许渲染过程可中断、暂停及恢复,并确保即使渲染中途变更,UI 仍保持一致。React 通过延迟DOM更新至计算阶段结束,实现在不影响主线程的情况下预加载新内容,从而即时响应用户交互,即便存在复杂或长时间运行的更新。
另一个例子是可重用状态。并发 React 可以从屏幕中移除部分 UI,然后在稍后将它们再添加回来,并重用之前的状态。例如,当用户来回切换标签页,React 应该能够立即将屏幕恢复到它先前的状态。在即将到来的次要版本中,我们计划添加一个新的名为 的组件,它实现了这种模式。同样地,你将能够使用 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等新功能都是由并发渲染提供的。
4.4. 使用场景
- 游戏应用:游戏应用有频繁的交互,并发模式可以用来提升交互体验。当然,很多场景其实也可以用防抖和节流来优化。
- 地图应用:地图应用往往有大量的数据渲染,会带来复杂的计算,开启并发模式能起到一定优化作用。
- 机器学习应用:比如用Tesorflow.js开发浏览器人工智能。因为机器学习的计算大多是CPU密集型的且计算量繁大,理论上也是并发模式的适用场景之一。
4.5. 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 更新。
4.6. 并发和并行
并发:指应用能够交替执行不同的任务,其实并发有点类似于多线程的原理,多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已.
就类似于你,吃一口饭喝一口水,以正常速度来看,完全能够看的出来,当你把这个过程以n倍速度执行时…可以想象一下.
并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行
4.7. 18 版本之前 没有并发模式 fiber 主要有什么作用
一句话理解:无并发模式之前 fiber 可以让任务暂停并根据优先级执行 并支持异步渲染,有并发模式 可以让任务交替执行
- 任务拆分与可中断性:在之前的 React 版本中,组件的渲染和更新是递归进行的,且不可中断。这在处理大型或复杂的 UI 时可能导致长时间的阻塞,从而影响应用的响应性。Fiber 架构通过将渲染任务拆分成更小的单元(即 "fibers"),允许 React 在执行过程中中断当前任务,转而处理其他更高优先级的任务,如用户交互或动画。
- 优先级调度:Fiber 架构引入了一个调度器(Scheduler),它可以根据任务的优先级来管理任务的执行顺序。高优先级的任务可以打断低优先级的任务,确保重要的更新能够尽快得到处理,从而提高应用的响应性和用户体验。
- 异步渲染与更新:Fiber 架构为异步渲染和更新铺平了道路。通过将渲染任务切分,React 可以在主线程空闲时逐步完成这些任务,而不是一次性同步完成所有渲染工作。这有助于减少页面在更新时的卡顿现象,提升用户界面的流畅性。
- 为并发模式做准备:虽然 React 16 中还没有正式的并发模式,但 Fiber 架构的引入是为后续版本中实现并发模式做技术铺垫。它改变了 React 的工作方式,使得未来的并发渲染成为可能。
4.8. 并发在 Suspense 和 useTransition 中的应用
- 对于受 CPU 影响的更新(例如创建 DOM 节点和运行组件代码),并发意味着更紧急的更新可以“中断”已经开始的渲染。
- 对于受 IO 影响的更新(例如从网络中获取代码或数据),并发意味着 React 甚至可以在所有数据到达之前就开始在内存中渲染,无需显示令人讨厌的加载中状态。
- Suspense 属于 IO 耗时型
- useTransition属于 CPU 耗时型
4.9. 为什么需要并发?
- 因为我们期望一些不重要的更新不会影响用户的操作,比如长列表渲染不会阻塞用户 input 输入,从而提升用户体验。
4.10. 并发模式是怎样的?
- 在多个更新并存的情况下,我们需要根据更新优先级,优先执行紧急的更新,其次再执行不那么紧急的更新。比如优先响应click事件触发的更新,其次再响应长列表渲染的更新。
4.11. 并发模式是如何实现的?
- 对于每个更新,为其分配一个优先级lane,用于区分其紧急程度。
- 通过Fiber结构将不紧急的更新拆分成多段更新,并通过宏任务的方式将其合理分配到浏览器的帧当中。这样就能使得紧急任务能够插入进来。
- 高优先级的更新会打断低优先级的更新,等高优先级更新完成后,再开始低优先级更新。
5. 自动批处理
React 17 不会在事件处理程序之外进行批处理,比如不会在一个 promise 中合并处理 setState
批处理是指,当 React 在一个单独的重渲染事件中批量处理多个状态更新以此实现优化性能。如果没有自动批处理的话,我们仅能够在 React 事件处理程序中批量更新。在 React 18 之前,默认情况下 promise 、 setTimeout 、原生应用的事件处理程序以及任何其他事件中的更新都不会被批量处理; 但现在,这些更新内容都会被自动批处理:
从 React 18 开始createRoot,所有更新都将自动批处理,无论它们来自何处。
这意味着超时、promise、本机事件处理程序或任何其他事件内部的更新将以与 React 事件内部的更新相同的方式进行批处理。我们希望这会减少渲染工作,从而提高应用程序的性能:
5.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
}
5.2. 对 hooks 有什么影响
暂时没影响,有的话请提给 react 官方
5.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.4. react18 之前无法这次? 而 react18 就可以支持了呢
在React 18之前的版本中,React的批处理主要是基于事件循环和合成事件系统的。React会将在同一个事件循环或同一个合成事件处理函数中的多个setState调用进行批处理,即将它们合并到一次重新渲染中。然而,这种批处理方式是有限的,因为它仅限于特定的情况下的状态更新。
对于异步操作(如setTimeout、Promise等)或原生事件触发的状态更新,React 18之前的版本并不会进行自动批处理。这意味着,如果在这些异步操作或原生事件处理程序中调用setState,每次调用都会导致一次独立的重新渲染,从而可能引发性能问题。
此外,React团队在之前的版本中没有实现全面的自动批处理,可能也是出于技术上的考虑和复杂性。实现全面的自动批处理需要对React的内部机制进行大规模的修改和优化,这可能是一个复杂且耗时的过程。
随着技术的发展和React团队的持续努力,React 18终于引入了全面的自动批处理机制。这个机制通过改进调度算法、优化内部数据结构以及可能的其他技术手段,使得React能够更智能、更高效地管理和优化状态更新的过程,从而提高应用程序的性能和响应性能。
每一次更新都存在优先级,对于有相同优先级的多次更新,只要实际调度第一个更新,而在后续的更新请求中提前返回函数就能实现批处理。
透过react 18.2.0的相关源码来了解批处理的实现
从调用setState(或者dispatch),到最后的react完成更新,流程大致是这样的:
- setState
- scheduleUpdateOnFiber
- ensureRootIsScheduled(有部分逻辑判断是否批处理,如需要,提前return)
- 如果第三步没有提前中断,调度react更新的回调函数performSyncWorkOnRoot或者performConcurrentWorkOnRoot
- 异步地执行performXXXWorkOnRoot(包含了render阶段)
在第四步中,根据条件的不同,更新回调会注册在微任务或者是MessageChannel的onmessage回调中,所以第五步中的异步是因条件而异的。
5.5. react18为什么可以实现批处理
关于批处理我们要关注的是前三点,当发生自动批处理时,ensureRootIsScheduled会提前返回
- 调度器的重写:React团队对内部的调度器进行了重写,采用了更先进的调度算法。这个新的调度器可以更智能地管理和调度任务的执行,包括状态更新。通过优化任务队列和优先级管理,React能够更有效地合并和处理多个状态更新,实现自动批处理。
- 并发模式的引入:React 18引入了并发模式(Concurrent Mode),这种新模式允许React在等待异步任务(如数据获取或用户交互)时,中断并延迟某些不紧急的更新。这使得React有更多的灵活性来决定何时以及如何进行渲染,从而更有效地实现批处理。
- 内部数据结构的优化:为了更好地支持批处理,React可能对其内部数据结构进行了优化。通过改进状态更新的追踪和管理方式,React能够更高效地处理多个状态变更,并将它们合并到一个批处理操作中。
- 开发者的渐进式升级体验:React 18还考虑了开发者的体验,允许渐进式升级。这意味着开发者可以逐步采用新特性,而不需要一次性重写整个应用程序。这种灵活性可能也促进了自动批处理特性的广泛应用和接受度。
- 对异步和原生事件的批处理支持:与之前的版本相比,React 18可能在底层对异步操作和原生事件触发的状态更新进行了特殊处理,使得这些情况下的状态变更也能被有效地批处理。
6. 新的 Suspense 特性
在v16/v17中,Suspense主要是配合React.lazy进行code spliting。在v18中,Suspense加入了fallback属性,用于将读取数据和指定加载状态分离
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 和 更多地被用于与新的并发特性相结合,在之前的React版本中, Suspense 主要用于代码拆分和懒加载
6.1. 替换原有的 loading 状态方案
最常见的处理异步数据的方式loading 状态方案
存储了两套数据isLoading/data和两种渲染结果,并且代码比较冗余,不利于开发维护。如果用Suspense,可以直接读取数据而不关心加载状态,
如果用Suspense,可以直接读取数据而不关心加载状态
另外一个问题:如果有两个组件Header和List,它们分别有自己的loading状态。现在我们想要把这两个loading状态合并在一起,放到page里,如果按照传统的方式,我们需要将大量的代码移动到上一层page里。但是在React18里,Suspense能够很轻松的解决这一问题
如果Header组件和List组件都在请求数据当中,那么就会显示Skeleton组件。如果我们想给List组件添加一个单独的占位组件,只需要再套一层Suspense即可实现,无需对数据进行做特殊处理
<Suspense fallback={<Skeleton />}>
<Header />
<Suspense fallback={<ListPlaceholder />}>
<List pageId={pageId} />
</Suspense>
</Suspense>
Suspense通过数据和加载状态分离的方式,极大地简化了加载状态的处理
Suspense捕捉错误后触发的更新为低优先级更新,会通过时间切片的形式去更新,因此不会阻塞用户交互和渲染流程,这也是前面提到的并发更新的一个实际应用。
- 可以替换原有的 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 批量更新
7. 新的客户端和服务端渲染 APIs
我们利用这次版本更新的机会,重新设计了我们为在客户端和服务端进行渲染所暴露的 API。这些更改允许用户在升级到 React 18 使用新的 API 时,也能继续使用 React 17 中的旧 API。
7.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。
7.1.2. React DOM Server
这些新的 API 现在可以从 react-dom/server 中导出,并且在服务端端完全支持流式 Suspense:
- renderToPipeableStream:用于 Node 环境中的流式渲染。
- renderToReadableStream:对新式的非主流运行时环境,比如 Deno 和 Cloudflare workers。
现有的 renderToString 方法仍然可以使用,但是并不推荐这样做。
8. 新的严格模式行为
在未来,我们希望新增一个功能,允许 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 创建
9. 严格模式下重复渲染
不启用严格模式,一次渲染是执行一次的。严格模式的特性只会在开发环境生效。无论开不开启严格模式线上打包后也只会执行一次。
9.1.1. 开发阶段重复多渲染一次
React 假定每个组件都是一个纯函数。这意味着 React 组件在接收相同的输入(props、state 和 context)的情况下,总是返回相同的 jsx。但是组件还是会有打破上述规则的一些不可预期的行为,从而引发 bug。为了帮助开发者发现这些偶发的非纯函数的代码,严格模式会在开发阶段重复多调用一次那些理应是纯函数的函数。这些函数包括:
- 组件的主函数体,只包含顶层逻辑,不包含内部事件处理等逻辑;
- 传递给 useState、set 函数、useMemo、useReducer 的函数;
- 一些类组件方法,如 constructor、render、shouldComponentUpdate 等,详见完整列表;
如果一个函数是纯函数,那么两次渲染,它的行为将是一致的。如果一个函数不是纯函数,那么两次渲染它的行为将比较明显的不一致。这就帮我们能够在开发阶段快速发现 bug。
useEffect 的使用也是会很容易引入非预期 bug 的场景,严格模式也通过多运行一次 useEffect 的方式,帮助开发者更容易发现 bug。**
**
在 useEffect 中我们会进行各种副作用的处理,比如事件监听、轮训、setInterval、数据库连接等,这些副作用一般都是会大量占用内存资源的。因此,useEffect 提供了清理副作用的方式,即通过 return 一个函数来让用户自定义清理逻辑。
但是,因为开发者对 useEffect 熟悉度较低、疏忽等各种原因,清理副作用的逻辑会经常忘记添加,这样则极易引发 bug。严格模式通过在开发阶段多运行一次 Effect 的方式,帮助开发者更容易发现这些非预期 bug。
使用代码举例
useEffect(() => {
setInterval(() => {
console.log("count", count);
}, 1000);
}, [count]);
9.1.2. 弃用 API 提示
严格模式下,会在开发环节对代码中使用的 React 弃用 API 进行提示。如:
-
findDOMNode;
-
UNSAFE_ 类声明周期方法,如 UNSAFE_componentWillMount;
-
旧版内容,如 childContextTypes/contextTypes/getChildContext 等;
-
旧版字符串 refs(this.refs)。
在没有了解到根结之前,我们或许会被突然出现的「一次渲染两次执行」所震惊到。但是,追根溯源到 StrictMode 的出发点和用途之后,发现 StrictMode 还是蛮贴心的一个 React 特性。大家后续好好利用 StrictMode 吧,让开发过程少些坑,让世界更美好。