React源码解析系列(十二) -- react高级版本的新特性的学习

301 阅读8分钟

上一章讲解了React源码解析系列(十一) -- react生命周期与事件系统的解读,这使得我们对React生命周期,事件系统更加清晰了一些,React作为一个流行的框架,在国内国外都享有盛名,无非归结于几点:

  • React维护团队来自facebook,背景强大且稳定。
  • React是一个构建用户UI界面的js库,jsx语法使得程序组件编写起来更为顺手。
  • 当然还有一些其他的优势,这里也不再复述了。

这一章一起来探讨一下React18的新特性(本章不会深入源码)。

React的发布时间线

  • 0.3.0 (2013 年 05 月 29 日)首次发布
    • 远古时期的React.js
  • 15.0.0(2016 年 04 月 07 日)React15发布
    • 解决document.createElement生成HTML的问题。
    • 解决渲染null节点不再使用<noscript>,而是用注释节点问题。
    • 解决纯文本节点的渲染与注释节点的渲染问题。
    • 功能组件可以返回nullSVG的支持。
  • 16.0.0(2017 年 09 月 26 日)React16发布
    • 解决React浏览器之间的兼容问题,主要是SetMaprequestAnimationFrame问题。
    • 更精准的自子组件根组件的错误的定位问题。
    • 新增ReactDOM.createPortal()方法以及调整了生命周期的一些方法。比如componentWillUnMount会比componentWillMount先执行。
  • 16.8.0(2019 年 02 月 06 日)Hooks版本发布
    • 新增hooks函数,让函数组件能够拥有类组件的功能。
    • 使用Object.is进行浅比较依赖。
  • 17.0.0(2020 年 10 月 20 日)React17发布
    • 新的jsx解析编译环境react/jsx-runtime的支持。
    • 事件委托到root上,而不是document上。
  • 18.0.0(2022 年 03 月 29 日)React18发布
    • 新增createRoot方法,废弃ReactDOM.render方法。
    • 新增useIdstartTransitionuseDeferredValueuseSyncExternalStoreuseInsertionEffect钩子或方法。
    • Automatic batching自动批处理。
    • Stricter Strict Mode严格模式。

Adopting Concurrent Mode

其实在react18里面,createRoot方案并不是意象而生的,其实早在v17版本里面就提出过了。

  • 传统模式(Legacy Mode):  ReactDOM.render(<App />, rootNode) . 这就是React应用程序今天使用的。没有计划在可观察的未来移除传统模式——但它将无法支持这些新功能。
  • 封锁模式(Blocking Mode):  ReactDOM.createBlockingRoot(rootNode).render(<App />)。它目前是实验性的。它旨在作为想要获得并发模式功能子集的应用程序的第一个迁移步骤。
  • 并发模式(Concurrent Mode):  ReactDOM.createRoot(rootNode).render(<App />) . 它目前是实验性的。未来,等它稳定下来后,我们打算让它成为默认的 React 模式。此模式启用所有新功能。
// React 17 with Legacy Mode
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render( 
  <StrictMode>
    <App />
  </StrictMode>,
  document.getElementById('root')
);

// React 18 with Concurrent Mode
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

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

Automatic Batching

  • 首先自动批处理发生在React的框架内部,并不需要用户有过多的操作。
  • React17版本的事件处理程序中,批处理适用于少量事件,如setTimeout等并没有做良好的批处理支持,但是React18默认给所有事件都做了批处理操作,比如
// App.js
import "./styles.css";
import { useState } from "react";

export default function App() {
  const [fristState, setFristStateToUpdate] = useState(0);
  const [secondState, setSecondStateToUpdate] = useState(10);
  
  // 打印一次render
  const handleBatching = () => {
    // 第一次 re-render
    setFristStateToUpdate((fristState) => fristState + 10);
    // 第二次 re-render
    setSecondStateToUpdate((secondState) => secondState + 20);
  };

  // 3000ms之后打印一次render
  const handleBatchingWithSetTimeout = () => {
    setTimeout(() => {
      // 第一次 re-render
      setFristStateToUpdate((fristState) => fristState + 10);
      // 第二次 re-render
      setSecondStateToUpdate((secondState) => secondState + 20);
    }, 3000);
  };

  // 打印一次render
  const handleBatchingWithPromise = () => {
    Promise.resolve().then(() => {
     // 第一次 re-render
    setFristStateToUpdate((fristState) => fristState + 10);
    // 第二次 re-render
    //setSecondStateToUpdate((secondState) => secondState + 20);
    });
    //setFristStateToUpdate((fristState) => fristState + 10);
    // 第二次 re-render
    setSecondStateToUpdate((secondState) => secondState + 20);
  };
  console.log("render");
  return (
    <div className="App">
      <h3>fristState: {fristState}</h3>
      <h3>secondState: {secondState}</h3>
      <button onClick={handleBatchingWithSetTimeout}>按钮</button>
    </div>
  );
}
  • React17版本中,只有在handleBatching做了批处理,render只打印了一次。在handleBatchingWithSetTimeouthandleBatchingWithPromise中都没有做,都是渲染多次。
  • React18版本中,handleBatchinghandleBatchingWithSetTimeouthandleBatchingWithPromise都做了批处理,render只会打印一次。
  • 各位同学可自行去codeSandBox上面去体验,根据不同的依赖版本去修改index.js里面的入口文件,尝试测试一下。

那如果我们有的场景我不想使用批处理,我们该如何做呢?React官方提供了一个flushSync方法。

// 如果不想实现批处理更新,只需要用flushSync包裹当前任务,那么之后的任务就不会被批量处理了
flushSync(()=>{
  setFristStateToUpdate((fristState) => fristState + 10);
})

useId的使用

useId的作用是创建唯一标识,比如:

const App = () => {
  // 创建表单唯一id
  const id = useId();
  // 创建ref
  const ref = createRef(null);
  //获得表单上的id属性
  const getId = () => {
    console.log(ref.current); // <input type="input" name="react" id=":r0:"></input>
    console.log(ref.current.id); // :r0:
  };
  // 打印初始化创建的id
  console.log(id);

  return (
    <>
      <label htmlFor={id}>show id</label>
      <br />
      <input type="input" name="react" id={id} ref={ref} />
      <br />
      <button onClick={getId}>get Element</button>
    </>
  );
}

有什么作用?创建唯一值啊。创建唯一值不是有很多方法吗?比如:

const  id  =  useId();  // :r0:
const id1 = useId(); // :r1:
const id2 = useId(); // :r2: 
const id3 = Math.random(2).toFixed(2); // 0.99
const id4 = Symbol(10); // Symbol(10)
  • useId创建的值,唯一且稳定,每一次都是:r0:
  • 多个useId创建值,则会往下排列。
  • Math.random创建的值不稳定且有局限性
  • Symbol确实唯一,但是它与任何数作比较都是不等的。

另外React提出这个hook的真实用意是为了解决SSR,服务端与客户端渲染的组件id不一致的问题的。

Suspense

Suspense的适用场景

  • 代码拆分:将React应用拆分为多个模块,只有当用户访问到了组件的时候,才会去加载组件对应的模块。
  • 数据加载:在异步请求场景中会因为前后端交互链路耗时比较长、或者因为网络的原因导致前端没有拿到数据,造成白屏的情况。

我们针对这种情况会给组件加一个过渡spin组件,Suspense的作用就在于在根组件上面加一个spin,提高用户体验。

root.render(
  <StrictMode>
    // maxDuration={100}为控制在100ms之后触发
    // fallback={<spin />}为在等待<App />组件渲染完毕之前,显示的组件
    <Suspense fallback={<spin />} maxDuration={100}>
      <App />
    </Suspense>
  </StrictMode>
);

New Strict Mode Behaviors

Concurrent rendering

关于并发渲染有两个关键的点

  • 渲染可中断
    • 非并发模式中,更新的渲染方式与之前版本的React相同——在一个单一的、不间断的、同步的事务中。使用同步渲染,一旦更新开始渲染,在用户可以在屏幕上看到结果之前,没有任何东西可以中断它。
    • 并发模式中,React可能会开始渲染更新,在中间暂停,然后再继续。它甚至可能完全放弃正在进行的渲染。React保证即使渲染被中断,UI也会保持一致。为此,它会等待执行DOM修改操作,直到完成整个树的渲染。有了这个能力,React可以在后台准备新的屏幕而不阻塞主线程。这意味着UI可以立即响应用户输入,即使它处于大型渲染任务的中间,从而创造流畅的用户体验。
  • 状态可重用
    • Concurrent React可以从屏幕上删除部分UI,然后在重用之前的状态时将它们添加回来。例如,当用户从一个屏幕上移开并返回时,React应该能够将前一个屏幕恢复到与之前相同的状态。依赖的是组件。

那么如何开启Concurrent rendering呢?React18提供了useTransition,useDeferredValue两个函数。

useTransition

useTransition能够提供数据更改到ui渲染过程的视觉平滑过度的功能,可适用于频繁地触发setState,或者一次性大量通过setState去更新视图的场景。用法如下:

import { useState, useTransition } from "react";
const App = () => {
  const [state, setState] = useState(0);
  const [isPending, startTransition] = useTransition();

  // 处理函数
  const handleClick = (e) => {
    // 处理卡顿情况
    startTransition(() => {
      setState(e.target.value);
    });
  };
  return (
    <>
      <input onChange={handleClick} />
      {isPending && <span>加载中...</span>}
      {!isPending && (
        <ul>
          {Array(10)
            .fill(state)
            .map((item) => (
              <li>{item}</li>
            ))}
        </ul>
      )}
    </>
  );
};
export default App;

注意startTransition可以单独从react中导出,isPending的作用在于告诉用户,当前还有一批待处理的状态更新,等待执行,在这个阶段你可以通过自定义ui组件进行展示。

useDeferredValue

useDeferredValue的作用在于把一些优先级较低的更新任务(非紧急更新)推迟到紧急渲染任务之后。

import { useState, useDeferredValue } from "react";
const App = () => {
  const [text, setText] = useState("一溪之石");
  const deferredText = useDeferredValue(text);
  const handleChange = (e) => {
    setText(e.target.value);
  };
  return (
    <div className="App">
      <input value={text} onChange={handleChange} />
      <ul>
        {Array(100)
          .fill(deferredText)
          .map((item) => (
            <li>{item}</li>
          ))}
      </ul>
    </div>
  );
};

export default App;

这里讲的是基础的用法,以及理论上的实现效果。如果要谈论到它的使用场景的话,那应该跟防抖的场景差不多。只不过他并不是在规定的时间之后执行的(防抖是在规定时间之后执行回调函数)。

useSyncExternalStore

  • 它通过强制的同步状态更新,使得外部 store 可以支持并发读取。它实现了对外部数据源订阅时不再需要 useEffect,并且推荐用于任何与 React 外部状态集成的库。
  • 注意:这个钩子不是供React用户使用的。

useInsertionEffect

  • useInsertionEffectuseLayoutEffect相似,但是他执行比useLayoutEffect早,那么在这个阶段无法获取domref
  • 注意:这个钩子不是供React用户使用的。

总结

通过查阅文献,学习了React18的新特性。

  • Concurrent Mode
  • Automatic Batching
  • useId的使用
  • Suspense使用
  • New Strict Mode Behaviors
  • Concurrent rendering
  • useSyncExternalStore
  • useInsertionEffect

到目前为止,React18版本的新特性也学习完了,前面的十一章讲了主要的React17.0.2的执行机制,下一章我将会去对前面的这几篇文章做一个系统性的总结以及会针对这些机制整理一些面试题,直通车 >>> React源码解析系列(十三) -- 源码解析终结与展望

参考资料

React严格模式

React18新特性