React18新特性

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2022-06-18

plugin-react-refresh

支持react组件热更新的插件

import {defineConfig} from 'vite';
import reactRefresh from '@vitejs/plugin-react-refresh';
export default defineConfig({
  plugins:[reactRefresh()]
});

并发更新

// 旧模式 渲染是同步的
// ReactDOM.render(
//   <Router>
//     <ul>
//       <li>
//         <Link to="/BatchState">BatchState</Link>
//       </li>
//       <Routes>
//         <Route path="/BatchState" element={<BatchState />} />
//       </Routes>
//     </ul>
//   </Router>, document.getElementById('root'));


// 新模式 启用是并发渲染模式 会将优先级一样的更改合并到一起
const root = createRoot(document.getElementById('root')!);
root.render(
  <Router>
    <ul>
      <li>
        <Link to="/BatchState">BatchState</Link>
      </li>
      <Routes>
        <Route path="/BatchState" element={<BatchState />} />
      </Routes>
    </ul>
  </Router>
);

其表现就是

import React from 'react';

export default class extends React.Component {
  state = {
    number: 0,
  }

  // 开启了并发更新的打印 0 0  1 1 
  // 没有开启的打印 0 0 2 3
  handleClick = () => {
    this.setState({ number: this.state.number + 1 })
    console.log(this.state);
    this.setState({ number: this.state.number + 1 })
    console.log(this.state);
    
    setTimeout(() => {
      this.setState({ number: this.state.number + 1 })
      console.log(this.state);
      this.setState({ number: this.state.number + 1 })
      console.log(this.state);
    })
  }

  render() {
    return (
      <div>
        <p>
          {this.state.number}
        </p>
        <button onClick={this.handleClick}>点击</button>
      </div>
    )
  }
}

打印 0 0 2 3 的原因是在React18以前有这样一个机制,其表现如下:

let isBatchingUpdate = false;
let updateQueue = [];
let state = 0;

function setState(newState) {
  if (isBatchingUpdate) {
    updateQueue.push(newState);
  } else {
    state = newState;
  }
}

function update() {
  isBatchingUpdate = true;
  setState(state+1);
  setState(state+1);

  setTimeout(() => {
    setState(state+1);
    setState(state+1);
  })
  isBatchingUpdate = false;
}

update();
state = updateQueue.pop();

console.log(state); // 1

setTimeout(() => {
  console.log(state); // 3
}, 2000)

即同一个宏任务中的更新被合并到一起,以后面的覆盖前面的,下一个宏任务开始时,update执行完成了,但是对应这个update的更新任务,还有setTimeout中的两个,此时isBatchingUpdate已翻转,则直接更新。

Suspense

使用

import React, { Suspense } from 'react';

interface CommonResponse {
  success: boolean;
  data: any;
}

function fetchUser(id: number): Promise<CommonResponse> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ success: true, data: { id, name: '姓名' + id }})
      // reject({ success: false, error: '数据出错了'})
    }, 1000)
  })
}

function createResource(promise: Promise<any>) {
  let status = 'pending';
  let result: any;
  return {
    read() {
      if (status === 'success' || status === 'error') {
        return result
      } else {
        throw promise.then((data: any) => {
          status = 'success';
          result = data;
        }, (err: any) => {
          status = 'error';
          result = err;  
        })
      }
    }
  }
}

let userResource = createResource(fetchUser(1));

function User() {
  let result = userResource.read();
  if (result.success) {
    let user = result.data;
    return <div>{user.id} {user.name}</div>
  }
  throw result.error;
}

export default class extends React.Component {
  render() {
    return (
      <Suspense fallback={<div>加载中...</div>}>
        <User />
      </Suspense>
    )
  }
}

大概原理:

  • 在render函数中,我们可以写入一个异步请求,请求数据
  • react会从我们缓存中读取这个缓存
  • 如果有缓存了,直接进行正常的render
  • 如果没有缓存,那么会抛出一个异常,这个异常是一个promise,并被Suspense捕获到,然后翻转内部维持的一个变量,进行 loading ? fallback : children,即在loading时显示占位的fallback否则显示Suspense的子组件。
  • 当这个promise完成后(请求数据完成),react会继续回到原来的render中(实际上是重新执行一遍render),把数据render出来。
  • 完全同步写法,没有任何异步callback之类的东西

实现大致如下:

// MySuspense.tsx
import React from 'react';

interface Props {
  fallback: React.ReactNode;
}

export default class extends React.Component<Props> {
  state = { loading : false }
  
  // render() 函数抛出错误,该函数可以捕捉到错误信息
  // 子组件加载异常后也会被这个钩子捕获到
  componentDidCatch(error: any) {
    if (typeof error.then === 'function') { // 抛出的异常是Promise 置为true显示fallback
      this.setState({ loading: true });

      error.then(() => {
        this.setState({ loading: false }) // 置为false后 显示对应的子组件
      })
    }
  }

  render() {
    const { children, fallback } = this.props;
    const { loading } = this.state;
    return loading ? fallback : children;
  }
}

SuspenseList

import React, { Suspense, SuspenseList } from 'react';

function fetchUser(id: number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ success: true, data: { id, name: '姓名' + id } });
    }, 1000*id);
  });
}

// React.lazy
function createResource(promise: Promise<any>) {
  let status = 'pending';//等待中,未知态
  let result: any;
  return {
    read() {
      if (status === 'success' || status === 'error') {
        return result;
      } else {
        throw promise.then((data: any) => {
          status = 'success';
          result = data;
        }, (error: any) => {
          status = 'error';
          result = error;
        });
      }
    }
  }
}

let user1Resource = createResource(fetchUser(1));
let user2Resource = createResource(fetchUser(2));
let user3Resource = createResource(fetchUser(3));

let userResourceMap:any = {
  1:user1Resource,
  2:user2Resource,
  3:user3Resource
}


interface UserProps{
  id:number
}
/**
 * User组件它会依赖一个异步加载的数据
 */
function User(props:UserProps) {
  let result: any = userResourceMap[props.id].read();
  if (result.success) {
    let user = result.data;
    return <div>{user.id} {user.name}</div>
  } else {
    return null;
  }
}

/**
 * revealOrder 定义了SuspenseList子组件应该显示的顺序,默认是哪个先相应就先显示哪个
 *  together 在所有的子组件都准备好了的时候显示它们
 *  forwards 从前往后显示
 *  backwards 从后往前显示
 * tail 指定如何显示SuspenseList尚未加载的项目
 *  默认情况下,显示列表中所有fallback
 *  collapsed 仅显示列表中下一个fallback
 *  hidden 未加载的项目不显示任何信息
 */
export default class extends React.Component {
  render() {
    return (
      <SuspenseList revealOrder="backwards" tail="collapsed">
        <Suspense fallback={<div>3加载中....</div>}>
          <User id={3} />
        </Suspense>
        <Suspense fallback={<div>2加载中....</div>}>
          <User id={2} />
        </Suspense>
        <Suspense fallback={<div>1加载中....</div>}>
          <User id={1} />
        </Suspense>
      </SuspenseList>
    )
  }
}

ErrorBoundary

如果上面的User组件加载失败了(异步操作出错了)而不是暂时状态没更新,可以使用一个ErrorBoundary组件借助getDerivedStateFromError来进行一个友好的展示,因此我们可以将User中return null 改为 throw new Error('error')来模拟这种场景,ErrorBoundary的实现如下:

import React from 'react';

interface Props {
  fallback:React.ReactNode
}

export default class extends React.Component<Props>{
  state = { 
    hasError: false,
    error: null
  }

  // 能从异步渲染中捕获错误 
  // 返回一个state,重置当前状态,并引发render重新渲染
  static getDerivedStateFromError(error:any){
    return {
      hasError:true,
      error
    }
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }

    return this.props.children;
  }
}

使用

export default class extends React.Component {
  render() {
    return (
      <ErrorBoundary fallback={<div>出错了...</div>}>
        <Suspense fallback={<div>加载中...</div>}>
          <User />
        </Suspense>
      </ErrorBoundary>
    )
  }
}

startTransition

下面实现的组件就是一个输入框,进行输入的时候,对应Suggestions组件根据输入框展示一个列表。

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

interface Props {
  keyword: string;
}

function getWords(keyword:string){
  let words = new Array(10000).fill(0).map((item:number,index:number)=> keyword+index);
  return Promise.resolve(words);
}


function Suggestions(props: Props) {
  let [words, setWords] = useState<Array<string>>([]);

  useEffect(()=>{  
    getWords(props.keyword).then((words:Array<string>)=>{
      // 开启渐变更新 本质就是低优先级的更新
      startTransition(()=>setWords(words));
    });
  },[props.keyword]);

  return (
    <ul>
      {
        words.map((word: string) => <li key={word}>{word}</li>)
      }
    </ul>
  )
}


export default function () {
  const [keyword, setKeyword] = useState<string>('');
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setKeyword(event.target.value);
  }

  return (
    <div>
      关键字 <input value={keyword} onChange={handleChange} />
      <Suggestions keyword={keyword} />
    </div>
  )
}

其本质是每次更新会产生一个update任务,而现在给update加上了优先级,优先级最高的最先执行。startTransition即将update标注为最低优先级的更新。

官网的更新日志是这么说的:

startTransition 和 useTransition 让您将一些状态更新标记为不紧急。 默认情况下,其他状态更新被认为是紧急的。 React 将允许紧急状态更新(例如,更新文本输入)以中断非紧急状态更新(例如,呈现搜索结果列表)。

解决的是,当搜索结果有10000条,此时要渲染是需要花费时间的,而此时接着继续输入,会被“卡”住,因为10000条还没渲染完,此时用户得不到文本框的输入反馈。而使用这两个API后将可以做到优先处理用户的输入反馈,滞后处理过长搜索结果列表的反馈。

useTransition

useTransition 允许组件在切换到下一个界面之前等待内容加载,从而避免不必要的加载状态。

它还允许组件将速度较慢的数据获取更新推迟到随后渲染,以便能立即渲染更重要的更新。

useTransition hook返回两个值的数组

  • isPending 是一个布尔值,这是React通知我们是否正在等待的过度的完成的方式
  • startTransition 是一个接收回调的函数,我们用它来告诉React需要推迟的state

如果某个state更新导致组件挂起,那么state更新应包装在transition中。

import React, { Suspense ,useState, useTransition} from 'react';

function fetchUser(id: number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ success: true, data: { id, name: '姓名' + id } });
      //reject({ success: false, error: '数据加载失败' });
    }, 5000);
  });
}

//React.lazy
function createResource(promise: Promise<any>) {
  let status = 'pending'; // 等待中,未知态
  let result: any;
  return {
    read() {
      if (status === 'success' || status === 'error') {
        return result;
      } else {
        throw promise.then((data: any) => {
          status = 'success';
          result = data;
        }, (error: any) => {
          status = 'error';
          result = error;
        });
      }
    }
  }
}

interface Props {
  resource:any
}

// User第一次渲染的时候,调用read会抛出错误,被Suspense捕获到,从而展示fallback。
// 而同时这个错误是一个promise,当promise.then时代表异步处理完成,这时改变Suspense中的flag变量loading,翻转状态。
// 状态更新,组件重新渲染,此时调用read返回的result.success就是true了,即展示正常的User状态。
// 这个initialResource可以看出是一个将异步请求封装成能在加载前抛出一个promise错误的对象以便被Suspense捕获
function User(props:Props) {
  let result: any = props.resource.read();
  if (result.success) {
    let user = result.data;
    return <div>{user.id} {user.name}</div>
  } else {
    return <div>{result.error}</div>;
  }
}

const initialResource = createResource(fetchUser(1));

export default function(){
  const [resource, setResource] = useState(initialResource);
  // 而使用useTransition能做到一个缓冲的效果
  // 当用户点击按钮的时候,initialResource发生了改变
  // User重新更新,此时又会抛出一个promise的错误对象,从而导致Suspense显示其fallback(加载中...),等加载完成之后再显示变化后的User信息
  // useTransition则能优化这一部分,将处理数据变化的函数使用返回的startTransition包裹,也是低优先级更新的另一种展现,
  // 即滞后展现时状态已经完成了,传递给User的resource一个是成功状态的了。所以不会有加载中那个展示的过程了。
  // 数据变化,需要重新加载时,不会展示fallback(加载中...),而是等到状态完成之后,直接显示变化后的User信息
  const [isPending, startTransition] = useTransition();

  return (
    <div>
      <Suspense fallback={<div>加载中.....</div>}>
        <User resource={resource}/>
      </Suspense>

      {/* isPending 用来处理如果等待状态完成的这个时间比较长的话,用户点击之后会感受到好像没有反应 */}
      {/* 此时可以使用isPending的状态来给用户一个反馈,如果异步请求比较长的话 */}
      {isPending ? <div>加载中....</div> : null }

      <button 
        onClick={
          ()=>{
            startTransition(()=>{
              setResource(createResource(fetchUser(2)));
            })
          }
        }
      >
        下一个用户
      </button>
    </div>
  )
}

useDeferredValue

与startTransition相同的例子,只是将传递给Suggestions的keyword使用useDeferredValue包裹了一下,它的作用是内部会调用useState并触发一次更新,但此时更新的优先级很低,所以但用户不断输入的时候,deferredText永远是“滞后”,类似于防抖效果,即Suggestions不会实时接收到最新的值,而是输入值没有更新之后,“滞后”接收到的那个值。

useDeferredValue 允许您推迟重新渲染树的非紧急部分。它类似于去抖动,但与之相比有一些优点。没有固定的时间延迟,因此 React 将在第一次渲染反映在屏幕上后立即尝试延迟渲染。 延迟渲染是可中断的,不会阻止用户输入。

export default function () {
  const [keyword, setKeyword] = useState<string>('');
  const deferredText = useDeferredValue(keyword); // 不需要使用 startTransition 了
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setKeyword(event.target.value);
  }

  return (
    <div>
      关键字 <input value={keyword} onChange={handleChange} />
      <Suggestions keyword={deferredText} />
    </div>
  )
}