本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

前言

在上一篇《React 之 Race Condition》中,咱们最终引入了 Suspense 来处理竞态条件问题,本篇咱们来详细讲解一下 Suspense。

Suspense

React 16.6 新增了 <Suspense> 组件,让你可以“等候”目标代码加载,而且可以直接指定一个加载的界面(像是个 spinner),让它在用户等候的时分显现。

现在,Suspense 仅支持的运用场景是:经过 React.lazy 动态加载组件

const ProfilePage = React.lazy(() => import('./ProfilePage')); // 懒加载
// 在 ProfilePage 组件处于加载阶段时显现一个 spinner
<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>

履行机制

但这并不意味着 Suspense 不可以独自运用,咱们可以写个 Suspense 独自运用的比方,不过现在运用起来会有些麻烦,但相信 React 官方会继续优化这个 API。

let data, promise;
function fetchData() {
  if (data) return data;
  promise = new Promise(resolve => {
    setTimeout(() => {
      data = 'data fetched'
      resolve()
    }, 3000)
  })
  throw promise;
}
function Content() {
  const data = fetchData();
  return <p>{data}</p>
}
function App() {
  return (
    <Suspense fallback={'loading data'}>
      <Content />
    </Suspense>
  )
}

这是一个非常简略的运用示例,但却可以用来解说 Suspense 的履行机制。

最一开始 <Content> 组件会 throw 一个 promise,React 会捕获这个反常,发现是 promise 后,会在这个 promise 上追加一个 then 函数,在 then 函数中履行 Suspense 组件的更新,然后展现 fallback 内容。

等 fetchData 中的 promise resolve 后,会履行追加的 then 函数,触发 Suspense 组件的更新,此时有了 data 数据,由于没有反常,React 会删去 fallback 组件,正常展现 <Content /> 组件。

实践运用

假如咱们每个恳求都这样去写,代码会很冗余,虽然有 react-cache 这个 npm 包,但上次更新已经是 4 年之前了,不过经过检查包源码以及参考 React 官方的示例代码,在实践项目中,咱们可以这样去写:

// 1. 通用的 wrapPromise 函数
function wrapPromise(promise) {
  let status = "pending";
  let result;
  let suspender = promise.then(
    r => {
      status = "success";
      result = r;
    },
    e => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    }
  };
}
// 这儿咱们模拟了恳求进程
const fakeFetch = () => {
  return new Promise(res => {
    setTimeout(() => res('data fetched'), 3000);
  });
};
// 2. 在烘托前发起恳求
const resource = wrapPromise(fakeFetch());
function Content() {
  // 3. 经过 resource.read() 获取接口回来成果
  const data = resource.read();
  return <p>{data}</p>
}
function App() {
  return (
    <Suspense fallback={'loading data'}>
      <Content />
    </Suspense>
  )
}

在这段代码里,咱们声明了一个 wrapPromise 函数,它接收一个 promise,比方 fetch 恳求。函数回来一个带有 read 办法的目标,这是由于封装成办法后,代码可以延迟履行,咱们就可以在 Suspense 组件更新的时分再履行办法,然后获取最新的回来成果。

函数内部记录了三种状况,pendingsuccesserror,根据状况回来不同的内容。

你或许会想,假如咱们还要根据 id 之类的数据点击恳求数据呢?运用 Suspense 该怎么做呢?React 官方文档也给了示例代码:

const fakeFetch = (id) => {
  return new Promise(res => {
    setTimeout(() => res(`${id} data fetched`), 3000);
  });
};
// 1. 依然是直接恳求数据
const initialResource = wrapPromise(fakeFetch(1));
function Content({resource}) {
  // 3. 经过 resource.read() 获取接口回来成果
  const data = resource.read();
  return <p>{data}</p>
}
function App() {
  // 2. 将 wrapPromise 回来的目标作为 props 传递给组件
  const [resource, setResource] = useState(initialResource);
  // 4. 从头恳求
  const handleClick = (id) => () => {
    setResource(wrapPromise(fakeFetch(id)));
  }
  return (
    <Fragment>
      <button onClick={handleClick(1)}>tab 1</button>
      <button onClick={handleClick(2)}>tab 2</button>
      <Suspense fallback={'loading data'}>
        <Content resource={resource} />
      </Suspense>
    </Fragment>
  )
}

优点:恳求前置

运用 Suspense 一个非常大的优点就是恳求是一开始就履行的。回想过往的发送恳求的机遇,咱们都是在 compentDidMount 的时分再恳求的,React 是先烘托的节点再发送的恳求,然而运用 Suspense,咱们是先发送恳求再烘托的节点,这就带来了体会上的提升。

特别当恳求多个接口的时分,凭借 Suspense,咱们可以完成接口并行处理以及提前展现,举个比方:

function fetchData(id) {
  return {
    user: wrapPromise(fakeFetchUser(id)),
    posts: wrapPromise(fakeFetchPosts(id))
  };
}
const fakeFetchUser = (id) => {
  return new Promise(res => {
    setTimeout(() => res(`user ${id} data fetched`), 5000 * Math.random());
  });
};
const fakeFetchPosts = (id) => {
  return new Promise(res => {
    setTimeout(() => res(`posts ${id} data fetched`), 5000 * Math.random());
  });
};
const initialResource = fetchData(1);
function User({resource}) {
  const data = resource.user.read();
  return <p>{data}</p>
}
function Posts({resource}) {
  const data = resource.posts.read();
  return <p>{data}</p>
}
function App() {
  const [resource, setResource] = useState(initialResource);
  const handleClick = (id) => () => {
    setResource(fetchData(id));
  }
  return (
    <Fragment>
      <p><button onClick={handleClick(Math.ceil(Math.random() * 10))}>next user</button></p>
      <Suspense fallback={'loading user'}>
        <User resource={resource} />
        <Suspense fallback={'loading posts'}>
          <Posts resource={resource} />
        </Suspense>
      </Suspense>
    </Fragment>
  )
}

在这个示例代码中,user 和 posts 接口是并行恳求的,假如 posts 接口提前回来,而 user 接口还未回来,会等到 user 接口回来后,再一起展现,但假如 user 接口提前回来,posts 接口后回来,则会先展现 user 信息,然后显现 loading posts,等 posts 接口回来,再展现 posts 内容。

React 之 Suspense

这听起来如同没什么,可是想想假如咱们是曾经会怎么做,咱们或许会用一个 Promise.all 来完成,可是 Promise.all 的问题就在于有必要等候一切接口回来才会履行,而且假如其中有一个 reject 了,都会走向 catch 逻辑。运用 Suspense,咱们可以做到更好的展现效果。

优点:处理竞态条件

运用 Suspense 可以有用的处理 Race Conditions(竞态条件) 的问题,关于 Race Conditions 可以参考《React 之 Race Condition》。

Suspense 之所以可以有用的处理 Race Conditions 问题,就在于传统的完成中,咱们需求考虑 setState 的正确机遇,履行次序是:1. 恳求数据 2. 数据回来 3. setState 数据

而在 Suspense 中,咱们恳求后,马上就设置了 setState,然后就只用等候恳求回来,React 履行 Suspense 的再次更新就好了,履行次序是:1. 恳求数据 2. setState 数据 3. 数据回来 4. Suspense 从头烘托,所以大大降低了出错的概率。

const fakeFetch = person => {
  return new Promise(res => {
    setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
  });
};
function fetchData(userId) {
  return wrapPromise(fakeFetch(userId))
}
const initialResource = fetchData('Nick');
function User({ resource }) {
  const data = resource.read();
  return <p>{ data }</p>
}
const App = () => {
  const [person, setPerson] = useState('Nick');
  const [resource, setResource] = useState(initialResource);
  const handleClick = (name) => () => {
    setPerson(name)
    setResource(fetchData(name));
  }
  return (
    <Fragment>
      <button onClick={handleClick('Nick')}>Nick's Profile</button>
      <button onClick={handleClick('Deb')}>Deb's Profile</button>
	    <button onClick={handleClick('Joe')}>Joe's Profile</button>
      <Fragment>
        <h1>{person}</h1>
        <Suspense fallback={'loading'}>
          <User resource={resource} />
        </Suspense>
      </Fragment>
    </Fragment>
  );
};

错误处理

注意咱们运用的 wrapPromise 函数:

function wrapPromise(promise) {
	// ...
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    }
  };
}

当 status 为 error 的时分,会 throw result 出来,假如 throw 是一个 promise,React 可以处理,但假如仅仅一个 error,React 就处理不了了,这就会导致烘托出现问题,所以咱们有必要针对 status 为 error 的状况进行处理,React 官方文档也供给了办法,那就是界说一个错误边界组件:

// 界说一个错误边界组件
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) {
    return {
      hasError: true,
      error
    };
  }
  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}
function App() {
  // ...
  return (
    <Fragment>
      <button onClick={handleClick(1)}>tab 1</button>
      <button onClick={handleClick(2)}>tab 2</button>
      <ErrorBoundary fallback={<h2>Could not fetch posts.</h2>}>
        <Suspense fallback={'loading data'}>
          <Content resource={resource} />
        </Suspense>
      </ErrorBoundary>
    </Fragment>
  )
}

<Content /> 组件 throw 出 error 的时分,就会被 <ErrorBoundary />组件捕获,然后展现 fallback 的内容。

源码

那 Suspense 的源码呢?咱们检查 React.js 的源码:

import {
  REACT_SUSPENSE_TYPE
} from 'shared/ReactSymbols';
export {
  REACT_SUSPENSE_TYPE as Suspense
};

再看下shared/ReactSymbols的源码:

export const REACT_SUSPENSE_TYPE: symbol = Symbol.for('react.suspense');

所以当咱们写一个 Suspense 组件的时分:

<Suspense fallback={'loading data'}>
  <Content />
</Suspense>
// 被转译为
React.createElement(Suspense, {
  fallback: 'loading data'
}, React.createElement(Content, null));

createElement 传入的 Suspense 就仅仅一个常量罢了,详细的处理逻辑会在以后的文章中慢慢讲解。

React 系列

  1. React 之 createElement 源码解读
  2. React 之元素与组件的差异
  3. React 之 Refs 的运用和 forwardRef 的源码解读
  4. React 之 Context 的变迁与背面完成
  5. React 之 Race Condition

React 系列的预热系列,带大家从源码的角度深化了解 React 的各个 API 和履行进程,全目录不知道多少篇,预计写个 50 篇吧。