前言

最近在阅读 React 源码,发现了一个很有意思的现象,就是 React.Suspense 的原理,它使得异步函数不再具有传染性,即异步函数不再需要层层传递,而是直接在顶层进行控制。

异步函数的传染性

在 JavaScript 中,异步函数通常是通过回调函数来实现的,例如:

function fetchData(callback) {
  setTimeout(() => {
    callback("data");
  }, 1000);
}

fetchData((data) => {
  console.log(data);
});

在上面的例子中,fetchData 函数是一个异步函数,它接受一个回调函数作为参数,当数据获取完成后,调用回调函数将数据传递出去。

但是,如果需要在一个异步函数中调用另一个异步函数,那么就需要将回调函数传递下去,例如:

function fetchData(callback) {
  setTimeout(() => {
    callback("data");
  }, 1000);
}

function fetchData2(callback) {
  fetchData((data) => {
    callback(data + "2");
  });
}

fetchData2((data) => {
  console.log(data);
});

在上面的例子中,fetchData2 函数是一个异步函数,它调用了 fetchData 函数,并将回调函数传递给了 fetchData 函数。这样,当 fetchData 函数获取到数据后,就会调用回调函数将数据传递给 fetchData2 函数,然后 fetchData2 函数再调用回调函数将数据传递给顶层函数。

但是,如果需要在一个异步函数中调用多个异步函数,那么就需要将回调函数层层传递下去,例如:

function fetchData(callback) {
  setTimeout(() => {
    callback("data");
  }, 1000);
}

function fetchData2(callback) {
  fetchData((data) => {
    callback(data + "2");
  });
}

function fetchData3(callback) {
  fetchData2((data) => {
    callback(data + "3");
  });
}

fetchData3((data) => {
  console.log(data);
});

在上面的例子中,fetchData3 函数是一个异步函数,它调用了 fetchData2 函数,并将回调函数传递给了 fetchData2 函数。然后 fetchData2 函数再调用了 fetchData 函数,并将回调函数传递给了 fetchData 函数。这样,当 fetchData 函数获取到数据后,就会调用回调函数将数据传递给 fetchData2 函数,然后 fetchData2 函数再调用回调函数将数据传递给 fetchData3 函数,最后 fetchData3 函数再调用回调函数将数据传递给顶层函数。

可以看到,异步函数的传染性会导致代码变得非常复杂,难以维护。因此,我们需要一种更好的方式来处理异步函数,使得异步函数不再具有传染性。

async/await

async/await 是 JavaScript 中处理异步函数的一种方式,它使得异步函数不再具有传染性,而是直接在顶层进行控制。例如:

async function fetchData() {
  const data = await new Promise((resolve) => {
    setTimeout(() => {
      resolve("data");
    }, 1000);
  });
  console.log(data);
}

fetchData();

在上面的例子中,fetchData 函数是一个异步函数,它使用 await 关键字来等待 Promise 对象的解析,然后将解析结果赋值给 data 变量。这样,当 Promise 对象解析完成后,fetchData 函数就会继续执行,将 data 变量的值打印出来。

可以看到,使用 async/await 可以使得异步函数不再具有传染性,而是直接在顶层进行控制。这样,代码就变得更加简洁,易于维护。

React.Suspense

React.Suspense 是 React 中处理异步组件的一种方式,它使得异步组件不再具有传染性,而是直接在顶层进行控制。例如:

import React, { Suspense } from "react";
import { unstable_createResource } from "react-cache";

const resource = unstable_createResource(fetchData);

function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("data");
    }, 1000);
  });
}

function MyComponent() {
  const data = resource.read();
  return <div>{data}</div>;
}

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <MyComponent />
    </Suspense>
  );
}

export default App;

在上面的例子中,MyComponent 函数是一个异步组件,它使用 unstable_createResource 函数创建了一个资源对象 resource,然后使用 resource.read() 方法来获取数据。这样,当数据获取完成后,MyComponent 函数就会继续执行,将数据的值渲染到页面上。 React.Suspense 使用了 unstable_createResource 函数来创建资源对象,这个函数接受一个异步函数作为参数,当异步函数执行完成后,就会将解析结果赋值给资源对象。然后,React.Suspense 使用资源对象的 read() 方法来获取数据,当数据获取完成后,React.Suspense 就会继续执行,将数据的值渲染到页面上。

可以看到,React.Suspense 使得异步组件不再具有传染性,而是直接在顶层进行控制。这样,代码就变得更加简洁,易于维护。

仿照 React.Suspense 的原理 解读一道面试题

async function getUser() {
  return await fetch("./1.json");
}

async function m1() {
  const user = await getUser();
  return user;
}

async function m2() {
  const user = await m1();
  return user;
}

async function m3() {
  const user = await m2();
  return user;
}

async function main() {
  const user = await m3();
  console.log(user);
}

main();

面临的问题

因为上述都是一个个简单的纯函数,但是因为涉及到异步调用,就产生了副作用,那现在希望全部可以正常同步执行,而且又可以正常返回结果,该怎么做呢? 于是改造的结果如下,变成同步执行

function getUser() {
  return fetch("./1.json");
}

function m1() {
  const user = getUser();
  return user;
}

function m2() {
  const user = m1();
  return user;
}

function m3() {
  const user = m2();
  return user;
}

function main() {
  const user = m3();
  console.log(user);
}

main();
我们希望可以同步调用该如何实现呢

图解1 图解2

解决方案

function run(func) {
  //保存旧的fetch
  const oldFetch = window.fetch;

  //重写fetch

  const cache = {
    status: "pending", //pending, fullfilled, rejected
    value: null,
  };

  function newFetch(...args) {
    //有缓存 交付缓存
    if (cache.status === "fulfilled") {
      return cache.value;
    } else if (cache.status === "rejected") {
      throw cache.value;
    }

    //无缓存
    const p = oldFetch(...args)
      .then((res) => res.json())
      .then((data) => {
        cache.status = "fulfilled";
        cache.value = data;
      })
      .catch((err) => {
        cache.status = "rejected";
        cache.value = err;
      });

    // 立即抛出错误
    throw p;
  }

  window.fecth = newFetch;

  //执行函数
  try {
    func();
  } catch (e) {
    if (e instanceof Promise) {
      e.finally(() => {
        window.fetch = newFecth;
        func();
        window.fetch = oldFetch;
      });
    }
  }
  //恢复旧的fetch
  window.fetch = oldFetch;

  //返回结果
}

run(main);

总结

异步函数的传染性会导致代码变得非常复杂,难以维护。因此,我们需要一种更好的方式来处理异步函数,使得异步函数不再具有传染性。async/await 和 React.Suspense 都可以使得异步函数不再具有传染性,而是直接在顶层进行控制。这样,代码就变得更加简洁,易于维护。