程序员的知识教程库

网站首页 > 教程分享 正文

react 最新版本解决了什么问题 加了哪些东西

henian88 2024-10-20 05:58:21 教程分享 160 ℃ 0 评论

React16

1)React 16.x的三大新特性

Time Slicing, Suspense,hooks

Time Slicing(解决CPU速度问题)使得在执行任务的期间可以随时暂停,跑去干别的事情,这个特性使得react能在性能极其差的机器跑时,仍然保持有良好的性能

Suspense (解决网络IO问题)和lazy配合,实现异步加载组件。 能暂停当前组件的渲染, 当完成某件事以后再继续渲染,解决从react出生到现在都存在的「异步副作用」的问题,而且解决得非

的优雅,使用的是「异步但是同步的写法」,我个人认为,这是最好的解决异步问题的方式

此外,还提供了一个内置函数 componentDidCatch,当有错误发生时, 我们可以友好地展示 fallback 组件;可以捕捉到它的子元素(包括嵌套子元素)抛出的异常;可以复用错误组件。

2)React16.8


加入hooks,让React函数式组件更加灵活

hooks之前,React存在很多问题

在组件间复用状态逻辑很难

复杂组件变得难以理解,高阶组件和函数组件的嵌套过深。

class组件的this指向问题

难以记忆的生命周期

hooks很好的解决了上述问题,hooks提供了很多方法

useState 返回有状态值,以及更新这个状态值的函数

useEffect 接受包含命令式,可能有副作用代码的函数。

useContext 接受上下文对象(从React.createContext返回的值)并返回当前上下文值,

useReducer useState的替代方案。接受类型为(state,action) => newState的reducer,并返回与dispatch方法配对的当前状态。

useCallback 返回一个回忆的memoized版本,该版本仅在其中一个输入发生更改时才会更改。纯函数的输入输出确定性

useMemo 纯的一个记忆函数

useRef 返回一个可变的ref对象,其.current属性被初始化为传递的参数,返回的 ref 对象在组件的整个生命周期内保持不变。

useImperativeMethods 自定义使用ref时公开给父组件的实例值

useMutationEffect 更新兄弟组件之前,它在React执行其DOM改变的同一阶段同步触发

useLayoutEffect DOM改变后同步触发。使用它来从DOM读取布局并同步重新渲染

3)React16.9


重命名 Unsafe 的生命周期方法。新的 UNSAFE_ 前缀将有助于在代码 review 和 debug 期间,使这些有问题的字样更突出

废弃 javascript: 形式的 URL。以 javascript: 开头的 URL 非常容易遭受攻击,造成安全漏洞。

废弃 “Factory” 组件。 工厂组件会导致 React 变大且变慢。

act() 也支持异步函数,并且你可以在调用它时使用 await。

使用 <React.Profiler> 进行性能评估。 在较大的应用中追踪性能回归可能会很方便

4)React16.13.0


支持在渲染期间调用setState,但仅适用于同一组件

可检测冲突的样式规则并记录警告

废弃unstable_createPortal,使用createPortal

将组件堆栈添加到其开发警告中,使开发人员能够隔离bug并调试其程序,这可以清楚地说明问题所在,并更快地定位和修复错误。

React 17

没有新功能

React 17版本不寻常,因为它没有添加任何面向开发人员的新功能。取而代之的是,此发行版主要侧重于使其更易于升级React本身

我们正在积极开发新的React功能,但它们不是此版本的一部分。React 17发行版是我们将其推广到任何人的战略的关键部分。

特别地,React 17是一个“垫脚石”版本,它使将由一个版本的React管理的树嵌入到由另一个版本的React管理的树中更加安全。

逐步升级

在过去的七年中,React升级一直是“全有或全无”。您可以使用旧版本,也可以将整个应用升级到新版本。中间没有。

到目前为止,这已经解决了,但是我们遇到了“全有或全无”升级策略的局限性。某些API更改(例如,不赞成使用旧版上下文API)是不可能以自动化方式进行的。即使今天编写的大多数应用程序从未使用过它们,我们仍然在React中支持它们。我们必须选择无限期地在React中支持它们,还是将某些应用程序留在旧版本的React中。这两个选项都不是很好。

因此,我们想提供另一种选择。

React 17支持逐步的React升级。从React 15升级到16(或者很快从React 16升级到17)时,通常会立即升级整个应用程序。这适用于许多应用程序。但是,如果代码库是在几年前编写的,并且没有得到积极维护,则挑战可能会越来越大。尽管可以在页面上使用两个版本的React,但是直到React 17仍然脆弱,并导致事件问题。

我们正在使用React 17解决许多这些问题。这意味着当React 18和下一个未来版本问世时,您现在将有更多选择。第一种选择是像以前一样,一次升级整个应用程序。但是您也可以选择逐个升级您的应用程序。例如,您可能决定将大部分应用程序迁移到React 18,但在React 17上保留一些延迟加载的对话框或子路由。

这并不意味着您必须逐步升级。对于大多数应用程序,一次全部升级仍然是最好的解决方案。加载两个版本的React(即使其中一个是按需延迟加载)仍然不是理想的选择。但是,对于没有积极维护的大型应用程序,可以考虑使用此选项,并且React 17可以使这些应用程序不落伍。

要启用渐进式更新,我们需要对React事件系统进行一些更改。React 17是主要版本,因为这些更改可能会被破坏。实际上,我们只需要在100,000个以上的组件中更改少于二十个组件,因此我们希望大多数应用程序可以升级到React 17而不会带来太多麻烦

逐步升级演示

我们准备了一个例子展示了如何在必要时延迟加载旧版本的React。该演示使用Create React App,但应该可以对其他工具采用类似的方法。我们欢迎使用其他工具作为拉取请求的演示。

我们已将其他更改推迟到React 17之后。此版本的目标是实现逐步升级。如果升级到React 17太困难了,那将无法实现其目标。

对事件委派的更改

从技术上讲,始终可以嵌套使用不同版本的React开发的应用程序。但是,由于React事件系统的工作原理,它相当脆弱。

在React组件中,通常会内联编写事件处理程序:

<button onClick={handleClick}>

等效于此代码的原始DOM类似于:

myButton.addEventListener('click', handleClick);

但是,对于大多数事件,React实际上不会将它们附加到在其上声明它们的DOM节点上。相反,React会直接在document节点上为每种事件类型附加一个处理程序。这称为事件委托。除了在大型应用程序树上具有性能优势外,它还使添加新功能(如重播事件)更加容易。

自从第一个版本发布以来,React一直在自动进行事件委派。当文档上触发DOM事件时,React会找出要调用的组件,然后React事件会在整个组件中“冒泡”。但是在幕后,本机事件已经冒出来,达到了documentReact安装其事件处理程序的水平。

但是,这是逐步升级的问题。

如果页面上有多个React版本,它们都将在顶部注册事件处理程序。这将中断e.stopPropagation():如果嵌套树停止了事件的传播,则外部树仍将接收该事件。这使得嵌套不同版本的React变得很困难。这种担心不是假设的,例如,Atom编辑器在四年前就遇到了这种情况。

这就是为什么我们要改变React在幕后将事件附加到DOM的方式。

在React 17中,React将不再在该document级别附加事件处理程序。相反,它将把它们附加到渲染您的React树的根DOM容器中:

const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);

在React 16和更早的版本中,React会document.addEventListener()处理大多数事件。React 17将rootNode.addEventListener()在后台调用。

编辑切换为居中

添加图片注释,不超过 140 字(可选)

该图显示了React 17如何将事件附加到根而不是文档

由于此更改,现在可以更安全地将由一个版本管理的React树嵌入到由其他React版本管理的树中。请注意,要使其正常工作,两个版本都必须为17或更高版本,这就是为什么升级到React 17很重要的原因。从某种意义上说,React 17是一个“垫脚石”版本,使下一个逐步升级成为可能。

这一变化还使将React嵌入到使用其他技术构建的应用程序中变得更加容易。例如,如果应用程序的外部“外壳”是用jQuery编写的,但其中的较新代码是用React编写的,e.stopPropagation()那么React代码内部现在将阻止它到达jQuery代码-正如您所期望的那样。这在另一个方向上也起作用。如果您不再喜欢React并想重写您的应用程序(例如,在jQuery中),则可以开始将外壳从React转换为jQuery,而不会破坏事件传播。

我们已经证实,很多 问题 报道 过 的 年 对 我们的 问题 跟踪器与整合与之反应的非反应的代码已经被固定在新的行为。

您可能想知道这是否会破坏根容器之外的Portal。答案是,React侦听门户网站容器上的事件,因此这不是问题。

解决潜在问题

与任何重大更改一样,可能需要调整一些代码。在Facebook,我们必须调整总共约10个模块(成千上万个模块)以适应此更改。

例如,如果您通过添加手动DOM侦听器document.addEventListener(...),则可能希望它们捕获所有React事件。在React 16及更早版本中,即使您调用e.stopPropagation()React事件处理程序,您的自定义document侦听器仍会收到它们,因为本机事件已经在文档级别。使用React 17,传播停止(按要求!),因此您的document处理程序将不会触发:

document.addEventListener('click', function() {
  // This custom handler will no longer receive clicks
  // from React components that called e.stopPropagation()
});

您可以通过将侦听器转换为使用捕获阶段来修复此类代码。为此,您可以将{ capture: true }第三个参数传递给document.addEventListener:

document.addEventListener('click', function() {
  // Now this event handler uses the capture phase,
  // so it receives *all* click events below!
}, { capture: true });

请注意,此策略在整体上如何更具弹性-例如,它可能会修复代码中在e.stopPropagation()React事件处理程序外部调用时发生的现有错误。换句话说,React 17中的事件传播更接近常规DOM

其他重大变化

我们将React 17中的重大更改保持在最低水平。例如,它不会删除以前版本中已弃用的任何方法。但是,它的确包含一些其他重大更改,根据我们的经验,这些更改相对安全。总体而言,由于这些因素,我们必须在100,000+个组件中调整少于20个。

与浏览器对应

我们对事件系统进行了一些较小的更改:

  • 该onScroll事件不再冒泡, 以防止常见的混乱。
  • ReactonFocus和onBlurevent已转为使用幕后的nativefocusin和focusoutevents,这与React的现有行为更加接近,有时还会提供额外的信息。
  • 捕获阶段事件(例如onClickCapture)现在使用真实的浏览器捕获阶段侦听器。

这些更改使React与浏览器行为更加接近,并提高了互操作性。

尽管该事件从React 17切换focus到focusin 了幕后,但onFocus请注意,这并未影响冒泡行为。在React中,onFocus事件总是冒泡的,它在React 17中继续冒泡,因为通常它是一个更有用的默认值。请参阅此沙箱,了解可以针对不同的特定用例添加的不同检查。

没有事件池

React 17从React移除了“事件池”优化。它不会提高现代浏览器的性能,甚至会使经验丰富的React用户感到困惑:

function handleChange(e) {
  setData(data => ({
    ...data,
    // This crashes in React 16 and earlier:
    text: e.target.value
  }));
}

这是因为React在旧浏览器中重用了不同事件之间的事件对象以提高性能,并将所有事件字段都设置null在它们之间。在React 16及更早版本中,您必须调用e.persist()以正确使用该事件,或读取您之前需要的属性。

在React 17中,此代码可以按您期望的那样工作。旧的事件池优化已被完全删除,因此您可以在需要时阅读事件字段。

这是一种行为更改,这就是我们将其标记为破坏的原因,但实际上,在Facebook上我们还没有看到它破坏任何东西。(也许它甚至修复了一些错误!)请注意,e.persist()React事件对象仍然可用,但是现在它什么也没做。

高效的清理

我们正在使useEffect清理功能的时间更加一致。

useEffect(() => {
  // This is the effect itself.
  return () => {    // This is its cleanup.  };});

大多数效果不需要延迟屏幕更新,因此React在屏幕上反映出更新后立即异步运行它们。(在极少数情况下,您需要一种效果来阻止油漆,例如,测量和定位工具提示,请使用useLayoutEffect。)

但是,在卸载组件时,效果清理函数将用于同步运行(类似于componentWillUnmount类中的同步)。我们发现这不适用于大型应用程序,因为它会减慢大屏幕过渡(例如切换选项卡)的速度。

在React 17中,清除功能始终异步运行-例如,如果要卸载组件,则在更新屏幕后运行清除。**

这反映了效果本身如何更紧密地运行。在极少数情况下,您可能希望依靠同步执行,可以useLayoutEffect改为使用。

您可能想知道这是否意味着您现在将无法修复有关setState未安装组件的警告。别担心-专门针对这种情况作出反应检查,确实没有触发setState在卸载和清理之间的短间隔警告。因此,取消代码的请求或间隔几乎总是可以保持不变。

另外,React 17将在运行任何新效果之前始终执行所有效果清理功能(针对所有组件)。React 16仅保证组件中效果的这种顺序。

潜在问题

尽管可重用的库可能需要对其进行更彻底的测试,但我们仅看到几个组件随此更改而中断。有问题的代码的一个示例可能如下所示:

useEffect(() => {
  someRef.current.someSetupMethod();
  return () => {
    someRef.current.someCleanupMethod();
  };
});

问题是someRef.current可变的,因此在运行清除功能时,可能已将其设置为null。解决方案是捕获效果的任何可变值:

useEffect(() => {
  const instance = someRef.current;
  instance.someSetupMethod();
  return () => {
    instance.someCleanupMethod();
  };
});

我们不希望这是一个常见的问题,因为我们的eslint-plugin-react-hooks/exhaustive-deps(请确保您使用它!)始终对此发出警告。

返回未定义的一致错误

在React 16和更早的版本中,返回undefined始终是一个错误:

function Button() {
  return; // Error: Nothing was returned from render
}

这部分是因为很容易undefined无意地返回:

function Button() {
  // We forgot to write return, so this component returns undefined.
  // React surfaces this as an error instead of ignoring it.
  <button />;
}

以前,React仅对类和函数组件执行此操作,但不检查forwardRefandmemo组件的返回值。这是由于编码错误。

在React 17中,forwardRef和memo组件的行为与常规函数和类组件一致。undefined从他们那里回来是错误的。

let Button = forwardRef(() => {
  // We forgot to write return, so this component returns undefined.
  // React 17 surfaces this as an error instead of ignoring it.
  <button />;
});

let Button = memo(() => {
  // We forgot to write return, so this component returns undefined.
  // React 17 surfaces this as an error instead of ignoring it.
  <button />;
});

对于您要有意不渲染任何内容的情况,请改为返回null。

本机组件堆栈

当您在浏览器中引发错误时,浏览器会为您提供带有JavaScript函数名称及其位置的堆栈跟踪。但是,JavaScript堆栈通常不足以诊断问题,因为React树的层次结构可能同样重要。您不仅要知道Button引发了错误,还想知道在React树中的哪个位置Button。

为了解决这个问题,当您遇到错误时,React 16开始打印“组件堆栈”。尽管如此,它们仍然不如原生JavaScript堆栈。特别是,它们在控制台中不可单击,因为React不知道函数在源代码中声明的位置。此外,它们在生产中几乎毫无用处。与常规的最小化JavaScript堆栈可以通过源映射自动恢复到原始函数名称不同,使用React组件堆栈,您必须在生产堆栈和捆绑包大小之间进行选择。

在React 17中,使用不同的机制生成组件堆栈,该机制将它们与常规的本机JavaScript堆栈缝合在一起。这使您可以在生产环境中获得完全符号化的React组件堆栈跟踪。

React实现这一点的方式有些不合常规。当前,浏览器没有提供获取函数的堆栈框架(源文件和位置)的方法。因此,当React捕获到错误时,它现在将通过在可能的情况下从上面每个组件内部抛出(并捕获)一个临时错误来重建其组件堆栈。这会增加少量的崩溃性能损失,但是每个组件类型只会发生一次。

如果您感到好奇,可以在pull请求中阅读更多详细信息,但是在大多数情况下,这种确切的机制不会影响您的代码。从您的角度来看,新功能是现在可以单击组件堆栈(因为它们依赖于本机浏览器堆栈框架),并且可以像常规JavaScript错误那样在生产中对其进行解码。

构成重大变化的部分是,要使此工作正常进行,React将在捕获错误后在堆栈中重新执行上面某些React函数和React类构造函数的部分。由于渲染函数和类构造函数不应具有副作用(这对于服务器渲染也很重要),因此这不应引起任何实际问题。

删除私有导出

最后,最后一个值得注意的重大变化是我们删除了一些以前暴露给其他项目的React内部组件。特别是,React Native for Web过去曾经依赖于事件系统的某些内部组件,但是这种依赖关系是脆弱的并且经常被破坏。

在React 17中,这些私有导出已被删除。据我们所知,React Native for Web是唯一使用它们的项目,并且他们已经完成了向不依赖于那些私有导出的其他方法的迁移。

这意味着旧版本的React Native for Web不会与React 17兼容,但是新版本将与它兼容。实际上,这并没有太大变化,因为React Native for Web必须发布新版本以适应内部React更改。

此外,我们还删除了ReactTestUtils.SimulateNative辅助方法。他们从未被记录下来,没有按照他们的名字所暗示的那样去做,并且不能与我们对事件系统所做的更改一起使用。如果您想要一种方便的方法来在测试中触发本机浏览器事件,请查看React测试库。


React 18 的三大特性

Automatic batching

在 React 中会将多次setState合并到一次进行渲染

也就是说,setState 并不是实时修改 State 的,而将多次 setState 调用合并起来仅触发一次渲染,既可以减少程序数据状态存在中间值导致的不稳定性,也可以提升渲染性能。可以理解为如下代码所示:

function handleClick() {
  setCount((c) => c + 1);
  setFlag((f) => !f);
  // 仅触发一次渲染
}

但是,在 React 18 以前,异步函数中的 setState 并不会进行合并,由于丢失了上下文,无法做合并处理,所以每次 setState 调用都会立即触发一次重渲染;React 18 带来的优化就是可以在任何情况下进行渲染优化了(异步回调函数,promise,定时器)的回调函数中调用多次的 setState 也会进行合并渲染

当然如果你非要 setState 调用后立即重渲染也行,只需要用 flushSync 包裹:

function handleClick() {
  // React 18+
  fetch(/*...*/).then(() => {
    ReactDOM.flushSync(() => {
      setCount((c) => c + 1); // 立刻重渲染
      setFlag((f) => !f); // 立刻重渲染
    });
  });
}

开启这个特性的前提是,将 ReactDOM.render 替换为 ReactDOM.createRoot 调用方式。

新的 ReactDOM Render API

升级方式很简单:

const container = document.getElementById("app");

// 旧 render API
ReactDOM.render(<App tab="home" />, container);

// 新 createRoot API
const root = ReactDOM.createRoot(container);
root.render(<App tab="home" />);

API 修改的主要原因还是语义化,即当我们多次调用 render 时,不再需要重复传入 container 参数,因为在新的 API 中,container 已经提前绑定到 root 了。

Concurrent APIS

Concurrent Mode 就是一种可中断渲染的设计架构。什么时候中断渲染呢?当一个更高优先级渲染到来时,通过放弃当前的渲染,立即执行更高优先级的渲染,换来视觉上更快的响应速度。

有人可能会说,不对啊,中断渲染后,之前渲染的 CPU 执行不就浪费了吗,换句话说,整体执行时常增加了。这句话是对的,但实际上用户对页面交互及时性的感知是分为两种的,第一种是即时输入反馈,第二种是这个输入带来的副作用反馈,比如更新列表。其中,即使输入反馈只要能优先满足,即便副作用反馈更慢一些,也会带来更好的体验,更不用说副作用反馈大部分情况会因为即使输入反馈的变化而作废。

由于 React 将渲染 DOM 树机制改为两个双向链表,并且渲染树指针只有一个,指向其中一个链表,因此可以在更新完全发生后再切换指针指向,而在指针切换之前,随时可以放弃对另一颗树的修改。

startTransition

首先看一下用法:

import { startTransition } from "react";

// 紧急更新:
setInputValue(input);

// 标记回调函数内的更新为非紧急更新:
startTransition(() => {
  setSearchQuery(input);
});

简单来说,就是被 startTransition 回调包裹的 setState 触发的渲染 被标记为不紧急的渲染,这些渲染可能被其他紧急渲染所抢占。

比如这个例子,当 setSearchQuery 更新的列表内容很多,导致渲染时 CPU 占用 100% 时,此时用户又进行了一个输入,即触发了由 setInputValue 引起的渲染,此时由 setSearchQuery 引发的渲染会立刻停止,转而对 setInputValue 渲染进行支持,这样用户的输入就能快速反映在 UI 上,代价是搜索列表响应稍慢了一些。而一个 transition 被打断的状态可以通过 isPending 访问到:

import { useTransition } from "react";
const [isPending, startTransition] = useTransition();

SSR for Suspense

完整名称是:Streaming SSR with selective hydration

即像水流一样,打造一个从服务端到客户端持续不断的渲染管线,而不是 renderToString 那样一次性渲染机制。selective hydration 表示选择性水合,水合指的是后端内容打到前端后,JS 需要将事件绑定其上,才能响应用户交互或者 DOM 更新行为,而在 React 18 之前,这个操作必须是整体性的,而水合过程可能比较慢,会引起全局的卡顿,所以选择性水合可以按需优先进行水合。

所以这个特性其实是转为 SSR 准备的,而功能启用载体就是 Suspense(所以以后不要再认为 Suspense 只是一个 loading 作用)。其实在 Suspense 设计之初,就是为了解决服务端渲染问题,只是一开始只实装了客户端测的按需加载功能,后面你会逐渐发现 React 团地逐渐赋予了 Suspense 更多强大能力。

SSR for Suspense 解决三个主要问题:

  • SSR 模式下,如果不同模块取数效率不同,会因为最慢的一个模块拖慢整体 HTML 吞吐时间,这可能导致体验还不如非 SSR 来的好。举一个极端情况,假设报表中一个组件依赖了慢查询,需要五分钟数据才能出来,那么 SSR 的后果就是白屏时间拉长到 5 分钟。
  • 即便 SSR 内容打到了页面上,由于 JS 没有加载完毕,所以根本无法进行 hydration,整个页面处于无法交互状态。
  • 即便 JS 加载完了,由于 React 18 之前只能进行整体 hydration,可能导致卡顿,导致首次交互响应不及时。

在 React 18 的 server render 中,只要使用 pipeToNodeWritable 代替 renderToString 并配合 Suspense 就能解决上面三个问题。

最大的区别在于,服务端渲染由简单的 res.send 改成了 res.socket,这样渲染就从单次行为变成了持续性的行为。

那么总结一下,新版 SSR 性能提高的秘诀在于两个字:按需。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表