Skip to content
目录

异步传染

ECMA Script 2016中提出了一个Generator语法糖async/await,用于帮助我们书写更清晰的异步代码

文档地址

要想使用await获取异步函数结果,必须满足一个特殊的条件:

  • async关键字声明的函数中
JavaScript
async function getUser() {
  return await getUserRequest();
}
1
2
3

如果想要对getUser()函数再使用await获取其返回值,也需要修改使用await的函数为async函数

JavaScript
async function request1() {
  return await getUser();
}
async function request2() {
  return await request1();
}
async function request3() {
  return await request2();
}
async function request4() {
  return await request3();
}
async function main() {
  const ans = await request4();
  console.log(ans);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

所以我们称async具有“传染性


我们想要异步获取一些数据,但不是所有的情况下我们都希望使用async声明函数,原因也是async具有传染性,一处使用async所有相关调用都要使用async,破坏了原函数的同步特性,大有牵一发而动全身的意思


此时我们希望的调用方式是这样:

JavaScript
function getName() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 3000, "luowei");
  });
}

function main() {
  const name = getName();
  console.log(name);
  // luowei
}
1
2
3
4
5
6
7
8
9
10
11

这里我将引用React 技术揭秘中的虚构语法做演示

虚构一个try...handle语法和两个操作符performresume

JavaScript
function getName() {
  return perform new Promise((resolve, reject) => {
    setTimeout(resolve, 3000, "luowei");
  })
}

function main() {
  const name = getName();
  console.log(name);
  // luowei
}

try{
    main();
} handle(value) {
    if(value instanceof Promise){
        value.then(name => {
            resume with name;
        })
    }else{
        resume with value;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

perform操作符行为与throw相似

handle操作符与catch相似


最大的区别在于resume操作符,它可以无视回调嵌套返回异步结果,也就是说:

JavaScript
if(value instanceof Promise){
    value.then(name => {
        resume with name;
    })
}
1
2
3
4
5

这里resume with name后会将值回传main函数中继续执行,resume with name语句会将name回传给perform返回

通过这样一个虚构语法我们能够实现同步代码中获取异步返回值的过程


try...catchtry...handle语法相似

JavaScript
function getName() {
  return throw new Promise((resolve, reject) => {
    setTimeout(resolve, 3000, "luowei");
  })
}

function main() {
  const name = getName();
  console.log(name);
}

try{
    main();
} catch(value) {
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

使用try...catch与使用try...handle的最大区别在于——函数执行上下文是否被销毁


try...catch:

main函数中抛出错误->被catch捕获

此时执行main的函数执行上下文已经被弹出执行上下文栈


try...handle:

main函数中perform一个值->被handle获取->处理结束后返回给perform操作符->继续执行main函数剩余部分

此时在handle获取期间,main的函数执行上下文仍被保留在执行上下文栈中


总结下来,如果能够让try...catch中的执行上下文得到保留,则近似得到了try...handle的效果

经过分析,在函数执行上下文的词法环境和变量环境中,存在两种类型的值

  • 同步返回的值
  • 异步返回的值

保存函数执行上下文当然是不现实的,但如果将异步返回的值全部转变为同步返回的值,再重新执行一次main函数,是否也能够得到类似的效果?

为此我们需要一个cache数组,来缓存异步返回的值,在后续调用时优先从cache数组中读取内容

  1. 创建run函数,用于执行main函数和构建缓存
JavaScript
function getName() {
  return throw new Promise((resolve, reject) => {
    setTimeout(resolve, 3000, "luowei");
  })
}

function main() {
  const name = getName();
  console.log(name);
}

function run() {
  const cache = [];
  let index = 0;
  try {
    main();
  } catch (error) {

  }
}

run();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  1. 我们需要对异步返回的值做一点修改,具体来讲是重写异步返回值的获取函数

策略如下:

  • 判断是否存在缓存,如果命中缓存则直接返回
  • 如果没有命中缓存,则抛出一个promise,并将promise的返回值存入缓存
JavaScript
const _originGetName = getName;
getName = () => {
    // 判断是否已经存在缓存值
    if (cache[index] && cache[index].status === "fulfilled") {
      return [cache[index].data, cache[index].err];
    }
    if (cache[index] && cache[index].status === "rejected") {
      return [cache[index].data, cache[index].err];
    }
    // 构建缓存值
    const promiseResult = {
      status: "pending",
      data: null,
      err: null,
    };
    // 存入cache数组
    cache[index++] = promiseResult;
    // 抛出源getName().then(),在其中修改promiseResult状态
    throw _originGetName().then(
      value => {
        promiseResult.status = "fulfilled";
        promiseResult.data = value;
      },
      reason => {
        promiseResult.status = "rejected";
        promiseResult.data = reason;
      }
    );
  };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  1. catch中判断是否是promise类型,如果是,在promise兑现后重新执行main函数
JavaScript
  try {
    main();
  } catch (error) {
    if (error instanceof Promise) {
      error.finally(() => {
        // 下标重置为0
        index = 0;
        main();
      });
    }
  }
1
2
3
4
5
6
7
8
9
10
11

完整代码如下:

JavaScript
function run() {
  const cache = [];
  let index = 0;
  const _originGetName = getName;
  getName = () => {
    if (cache[index] && cache[index].status === "fulfilled") {
      return [cache[index].data, cache[index].err];
    }
    if (cache[index] && cache[index].status === "rejected") {
      return [cache[index].data, cache[index].err];
    }
    const promiseResult = {
      status: "pending",
      data: null,
      err: null,
    };
    cache[index++] = promiseResult;
    throw _originGetName("luowei").then(
      value => {
        promiseResult.status = "fulfilled";
        promiseResult.data = value;
      },
      reason => {
        promiseResult.status = "rejected";
        promiseResult.data = reason;
      }
    );
  };
  try {
    main();
  } catch (error) {
    if (error instanceof Promise) {
      error.finally(() => {
        index = 0;
        main();
      });
    }
  }
}

run();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

执行流程图:

img

类似

TypeScript
const name = getName()
1

这种,我们不关心getName()实现,只在乎获取到name并返回结果的过程叫做代数效应

React Hooks就是代数效应的最佳实践

TypeScript
const [num, updateNum] = useState(0);
// 只需要假设useState返回的是我们想要的state
1
2

更加明显的体现是Suspense组件

Suspense`组件会在子组件挂起状态时渲染`fallback component
1

使用lazy进行懒加载的组件就是返回一个promise,当promise状态为pending时渲染fallbackpromise兑现后渲染对应组件

img


Suspense官方示例

参考资料:

React设计原理[卡颂著]——电子工业出版社

React 技术揭秘

React 18 新特性(二):Suspense & SuspenseList - 掘金 (juejin.cn)

Released under the MIT License.