【React扩展】 React 18 新特性

想来想去 还是要好好补习一下React
掘金搬运点文章

5.13

参考 https://juejin.cn/post/7094037148088664078

  • 发行时间 2022.3
  • React 18 放弃了对ie11的支持
  • 旧项目升级: 先把依赖中的版本号改成最新,然后删掉 node_modules 文件夹,重新安装 npm -i

新特性

  • 官方删除了这个报错:无法对未挂载(已卸载)的组件执行状态更新。这是一个无效操作,并且表明我们的代码中存在内存泄漏。
  • 新的 root API 支持 new concurrent renderer(并发模式的渲染),它允许你进入concurrent mode(并发模式)。
// React 18
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = document.getElementById('root')!;

ReactDOM.createRoot(root).render(<App />);

// React 18 卸载组件
root.unmount();
  • 在 render 方法中使用回调函数,可以在组件中通过 useEffect 实现
// React 18
const AppWithCallback: React.FC = () => {
  useEffect(() => {
    console.log('渲染完成');
  }, []);
  return <App />;
};
const root = document.getElementById('root')!;
ReactDOM.createRoot(root).render(<AppWithCallback />);

// React 18 ssr
import ReactDOM from 'react-dom/client';
const root = document.getElementById('root')!;
ReactDOM.hydrateRoot(root, <App />);
  • React 18中要显式定义children(ts里)
// React 18
interface MyButtonProps {
  color: string;
  children?: React.ReactNode;
}

const MyButton: React.FC<MyButtonProps> = ({ children }) => {
  // 在 React 18 的 FC 中,不存在 children 属性,需要手动申明
  return <div>{children}</div>;
};

export default MyButton;
  • React 18中所有的更新都将自动批处理,除了明显的异步,比如async=>await
// React 18 渲染两次
import React, { useState } from 'react';
const App: React.FC = () => {
  console.log('App组件渲染了!');
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
    <div
      onClick={async () => {
        await setCount1(count => count + 1);
        setCount2(count => count + 1);
      }}
    >
      <div>count1: {count1}</div>
      <div>count2: {count2}</div>
    </div>
  );
};

export default App;
  • 如果想退出批量更新,可以使用 flushSync
  • 注意:flushSync 函数内部的多个 setState 仍然为批量更新,这样可以精准控制哪些不需要的批量更新。
import React, { useState } from 'react';
import { flushSync } from 'react-dom';

const App: React.FC = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
    <div
      onClick={() => {
        flushSync(() => {
          setCount1(count => count + 1);
        });
        // 第一次更新
        flushSync(() => {
          setCount2(count => count + 1);
        });
        // 第二次更新
      }}
    >
      <div>count1: {count1}</div>
      <div>count2: {count2}</div>
    </div>
  );
};

export default App;
  • 在 React 18 中,不再检查因返回 undefined 而导致的崩溃,需要返回一个空组件时,既能返回 null,也能返回 undefined
  • 当使用严格模式时,React 会对每个组件进行两次渲染,React 18 中官方不再抑制控制台日志,第二次渲染的日志信息将显示为灰色,以柔和的方式显式在控制台
  • 现在,React 18将使用当前组件的 Suspense 作为边界,即使当前组件的 Suspense 的值为 null 或 undefined,这个更新意味着我们不再跨越边界组件。相反,我们将在边界处捕获并呈现 fallback,就像你提供了一个返回值为 null 的组件一样。这意味着被挂起的 Suspense 组件将按照预期结果去执行,如果忘记提供 fallback 属性,也不会有什么问题。
// React 18
const App = () => {
  return (
    <Suspense fallback={<Loading />}> // <--- 不使用
      <Suspense>                      // <--- 这个边界被使用,将 fallback 渲染为 null
        <Page />
      </Suspense>
    </Suspense>
  );
};

export default App;

New API

  • useId
    支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不兼容,这解决了在 React 17 及 17 以下版本中已经存在的问题。因为我们的服务器渲染时提供的 HTML 是无序的,useId 的原理就是每个 id 代表该组件在组件树中的层级结构。
const id = useId();
  • useSyncExternalStore
    由 useMutableSource 改变而来,主要用来解决外部数据撕裂问题。
    useSyncExternalStore 能够通过强制同步更新数据让 React 组件在 CM 下安全地有效地读取外接数据源。 在 Concurrent Mode 下,React 一次渲染会分片执行(以 fiber 为单位),中间可能穿插优先级更高的更新。假如在高优先级的更新中改变了公共数据(比如 redux 中的数据),那之前低优先的渲染必须要重新开始执行,否则就会出现前后状态不一致的情况。
    useSyncExternalStore 一般是三方状态管理库使用,我们在日常业务中不需要关注。因为 React 自身的 useState 已经原生的解决的并发特性下的 tear(撕裂)问题。useSyncExternalStore 主要对于框架开发者,比如 redux,它在控制状态时可能并非直接使用的 React 的 state,而是自己在外部维护了一个 store 对象,用发布订阅模式实现了数据更新,脱离了 React 的管理,也就无法依靠 React 自动解决撕裂问题。因此 React 对外提供了这样一个 API。
    目前 React-Redux 8.0 已经基于 useSyncExternalStore 实现。
  • useInsertionEffect
    这个 Hooks 只建议 css-in-js 库来使用。 这个 Hooks 执行时机在 DOM 生成之后,useLayoutEffect 之前,它的工作原理大致和 useLayoutEffect 相同,只是此时无法访问 DOM 节点的引用,一般用于提前注入 < style >脚本。
 const useCSS = rule => {
  useInsertionEffect(() => {
    if (!isInserted.has(rule)) {
      isInserted.add(rule);
      document.head.appendChild(getStyleForRule(rule));
    }
  });
  return rule;
};

const App: React.FC = () => {
  const className = useCSS(rule);
  return <div className={className} />;
};

export default App;

重点 并发模式

  • Concurrent Mode(以下简称 CM)叫做并发模式
  • 在 React 18 中,提供了新的 root api,我们只需要把 render 升级成 createRoot(root).render(< App />) 就可以开启并发模式了
  • 在18中开启了并发模式,并不一定开启了并发更新!
    一句话总结:在 18 中,不再有多种模式,而是以是否使用并发特性作为是否开启并发更新的依据。
  • 可以从架构角度来概括下,当前一共有两种架构:

采用不可中断的递归方式更新的Stack Reconciler(老架构)
采用可中断的遍历方式更新的Fiber Reconciler(新架构)

  • 新架构可以选择是否开启并发更新,所以当前市面上所有 React 版本有四种情况:

老架构(v15及之前版本)
新架构,未开启并发更新,与情况1行为一致(v16、v17 默认属于这种情况)
新架构,未开启并发更新,但是启用了并发模式和一些新功能(比如 Automatic Batching,v18 默认属于这种情况)
新架构,开启并发模式,开启并发更新
React 18 并发模式和并发特性的关系图

并发特性指开启并发模式后才能使用的特性,比如:

useDeferredValue
useTransition

  • useTransition
//React 18
import React, { useState, useEffect, useTransition } from 'react';

const App: React.FC = () => {
  const [list, setList] = useState<any[]>([]);
  const [isPending, startTransition] = useTransition();
  useEffect(() => {
    // 使用了并发特性,开启并发更新
    startTransition(() => {
      setList(new Array(10000).fill(null));
    });
  }, []);
  return (
    <>
      {list.map((_, i) => (
        <div key={i}>{i}</div>
      ))}
    </>
  );
};

export default App;
  • 由于 setList 在 startTransition 的回调函数中执行(使用了并发特性),所以 setList 会触发并发更新。
    startTransition,主要为了能在大量的任务下也能保持 UI 响应。这个新的 API 可以通过将特定更新标记为“过渡”来显著改善用户交互,简单来说,就是被 startTransition 回调包裹的 setState 触发的渲染被标记为不紧急渲染,这些渲染可能被其他紧急渲染所抢占。

  • useDeferredValue

  • 返回一个延迟响应的值,可以让一个state 延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValue 和 startTransition 一样,都是标记了一次非紧急更新。
    从介绍上来看 useDeferredValue 与 useTransition 是否感觉很相似呢?
    相同:useDeferredValue 本质上和内部实现与 useTransition 一样,都是标记成了延迟更新任务。
    不同:useTransition 是把更新任务变成了延迟更新任务,而 useDeferredValue 是产生一个新的值,这个值作为延时状态。(一个用来包装方法,一个用来包装值)

所以,上面 startTransition 的例子,我们也可以用 useDeferredValue 来实现:

import React, { useState, useEffect, useDeferredValue } from 'react';

const App: React.FC = () => {
  const [list, setList] = useState<any[]>([]);
  useEffect(() => {
    setList(new Array(10000).fill(null));
  }, []);
  // 使用了并发特性,开启并发更新
  const deferredList = useDeferredValue(list);
  return (
    <>
      {deferredList.map((_, i) => (
        <div key={i}>{i}</div>
      ))}
    </>
  );
};

export default App;
  • 这种将长任务分拆到每一帧中,像蚂蚁搬家一样一次执行一小段任务的操作,被称为时间切片(time slice)
  • 结论
    并发更新的意义就是交替执行不同的任务,当预留的时间不够用时,React 将线程控制权交还给浏览器,等待下一帧时间到来,然后继续被中断的工作
    并发模式是实现并发更新的基本前提
    时间切片是实现并发更新的具体手段
    上面所有的东西都是基于 fiber 架构实现的,fiber为状态更新提供了可中断的能力
    使用并发更新,此时我们的任务被拆分到每一帧不同的 task 中,浏览器就有剩余时间执行样式布局和样式绘制,减少掉帧的可能性。

fiber

  • 作为架构来说,在旧的架构中,Reconciler(协调器)采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为 Stack Reconciler,stack 就是调用栈;在新的架构中,Reconciler(协调器)是基于fiber实现的,节点数据保存在fiber中,所以被称为 fiber Reconciler。

  • 作为静态数据结构来说,每个fiber对应一个组件,保存了这个组件的类型对应的dom节点信息,这个时候,fiber节点就是我们所说的虚拟DOM。

  • 作为动态工作单元来说,fiber节点保存了该节点需要更新的状态,以及需要执行的副作用。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值