铺垫:*与yield

function *就是Generator创建函数,返回结果就是一个生成器

  1. function* foo(x) {
  2. console.log('start');
  3. let a = yield ++x;
  4. console.log('state 1, a=', a);
  5. let b = yield ++x+2;
  6. console.log('state 2, b=', b);
  7. yield;
  8. console.log('end, x: ', x);
  9. return 33;
  10. }
  11. const result = foo(0) // foo {<suspended>}

调用顺序、传参、结果如下:
image.png

  1. function* foo1() {
  2. yield 1;
  3. yield 2;
  4. return "foo1 end";
  5. }
  6. function* foo2() {
  7. yield 3;
  8. yield 4;
  9. return "foo2 end";
  10. }
  11. function* foo() {
  12. let a = yield* foo1();
  13. console.log(a);
  14. let b = yield* foo2();
  15. console.log(b);
  16. return yield ++testN;
  17. }
  18. let testN = 5;
  19. const iterator = foo();
  20. console.log(iterator.next());// "{ value: 1, done: false }"
  21. console.log(iterator.next());// "{ value: 2, done: false }"
  22. console.log(iterator.next());// "{ value: 3, done: false }"
  23. console.log(iterator.next());// "{ value: 4, done: false }"
  24. console.log(iterator.next());// "{ value: 5, done: false }"
  25. console.log(iterator.next(99));// "{ value: 99, done: true }"

image.png

总结,重要!!

  1. 执行Generator函数(即function*)只是得到一个生成器result(同时它也是迭代器和可迭代对象)。
  2. 上述result/iterator的next方法:
    1. 传递的参数是什么,此次的 yield 表达式 这个整体的值就是多少。注意!第一次next的参数,没法生效!
    2. next返回一个对象,包括value和done属性,value就是刚才执行到的yield后面的表达式的值
    3. 无论怎样,也不管是否移交了控制权!只要调用一次next,就会执行到一个yield 表达式,然后暂停。所以,注意!!把yield 表达式,这个整体的值赋值给a或b这种操作,或者其他操作,压根就还没执行呢。已经暂停了!!
    4. 当函数执行完或者主动return一次,done就会变为true。return的值就是最终value的值
  3. 多个Generator函数移交控制权时,即内嵌 yield* func(xxx);这种的,需注意以下内容:
    1. 一个Generator函数中只有一次return的机会,但是只有最外层的Generator函数return/执行完,done才会变为true
    2. 牢记!无论怎样,不管是否移交了控制权!只要调用next,就会执行到下一个yield 表达式,然后暂停。
      1. 说了无论怎样都会这样,跨Generator函数也这样,注意看上述最后一张图的打印结果
    3. yield* func(); 在func内return的结果,会作为这个整体的值返回给这个整体

      背景

      首先想要更好的理解 Async/Await,需要了解这两个知识点:
  • 同步
  • 异步

    同步

首先,js 是单线程的(重复三遍),所谓单线程,
通俗的讲就是,一根筋(比喻有点过分,哈哈)执行代码是一行一行的往下走(即所谓的同步),
如果上面的没执行完,就痴痴的等着(是不是很像恋爱中在路边等她/他的你,假装 new 了个对象,啊哈哈哈,调皮一下很开心),
还是举个 🌰 吧:

  1. // chrome 81
  2. function test() {
  3. let d = Date.now();
  4. for (let i = 0; i < 1e8; i++) {}
  5. console.log(Date.now() - d); // 62ms-90ms左右
  6. }
  7. function test1() {
  8. let d = Date.now();
  9. console.log(Date.now() - d); // 0
  10. }
  11. test();
  12. test1();

异步

上面仅仅是一个 for 循环,而在实际应用中,会有大量的网络请求,它的响应时间是不确定的,这种情况下也要痴痴的等么?显然是不行的,因而 js 设计了异步,即 发起网络请求(诸如 IO 操作,定时器),由于需要等服务器响应,就先不理会,而是去做其他的事儿,等请求返回了结果的时候再说(即异步)。
那么如何实现异步呢?其实我们平时已经在大量使用了,那就是 callback,例如:

  1. // 网络请求
  2. $.ajax({
  3. url: 'http://xxx',
  4. success: function(res) {
  5. console.log(res);
  6. },
  7. });

success 作为函数传递过去并不会立即执行,而是等请求成功了才执行,即回调函数(callback)

  1. // IO操作
  2. const fs = require('fs');
  3. fs.rename('旧文件.txt', '新文件.txt', err => {
  4. if (err) throw err;
  5. console.log('重命名完成');
  6. });

和网络请求类似,等到 IO 操作有了结果(无论成功与否)才会执行第三个参数:(err)=>{}

从上面我们就可以看出,实现异步的核心就是回调钩子,将 cb 作为参数传递给异步执行函数,当有了结果后在触发 cb。想了解更多,去看看 event-loop 机制吧。

callback hell

至于 async/await 是如何出现的呢,在 es6 之前,大多 js 数项目中会有类似这样的代码:

  1. ajax1(url, () => {
  2. // do something 1
  3. ajax2(url, () => {
  4. // do something 2
  5. ajax3(url, () => {
  6. // do something 3
  7. // ...
  8. });
  9. });
  10. });

这种函数嵌套,大量的回调函数,使代码阅读起来晦涩难懂,不直观,形象的称之为回调地狱(callback hell),所以为了在写法上能更通俗一点,es6+陆续出现了 PromiseGeneratorAsync/await,力求在写法上简洁明了(扁平化),可读性强(更优雅、更简洁)。

前端异步编程发展史

image.png其实Promise之前还有个Thunk,但被淘汰了

========================= 我是分割线 ==========================

以上只是铺垫,下面在进入正题 👇,开始说道说道主角:async/await

========================= 我是分割线 ==========================

async/await 是参照 Generator 封装的一套异步处理方案,可以理解为 Generator 的语法糖,

image.png

所以了解 async/await 就不得不讲一讲 Generator(首次将协程的概念引入 js,是协程的子集,不过由于不能指定让步的协程,只能让步给生成器(迭代器)的调用者,所以也称为非对称协程),

Generator 又返回迭代器Iterator对象,

所以就得先讲一讲 Iterator,

IteratorGenerator 都属于协程,

终于找到源头了:协程

协程

wiki:协程(英语:coroutine)是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。协程更适合于用来实现彼此熟悉的程序组件,如协作式多任务、异常处理、事件循环、迭代器、无限列表和管道 协程可以通过 yield(取其“让步”之义而非“出产”)来调用其它协程,接下来的每次协程被调用时,从协程上次 yield 返回的位置接着执行,通过 yield 方式转移执行权的协程之间不是调用者与被调用者的关系,而是彼此对称、平等的 协程是追求极限性能和优美的代码结构的产物 协程间的调用是逻辑上可控的,时序上确定的

协程是一种比线程更加轻量级的存在,是语言层级的构造,可看作一种形式的控制流,在内存间执行,不像线程间切换的开销。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。

协程概念的提出比较早,单核CPU场景中发展出来的概念,通过提供挂起恢复接口,实现在单个CPU上交叉处理多个任务的并发功能。

那么本质上就是在一个线程的基础上,增加了不同任务栈的切换,通过不同任务栈的挂起和恢复,线程中进行交替运行的代码片段(我暂且叫它 代码分片执行),实现并发的功能。

其实从这里可以看出 「协程间的调用是逻辑上可控的,时序上确定的」

那么如何理解 js 中的协程呢?

  • js 公路只是单行道(主线程),但是有很多车道(辅助线程)都可以汇入车流(异步任务完成后回调进入主线程的任务队列)
  • generator 把 js 公路变成了多车道(协程实现),但是同一时间只有一个车道上的车能开(依然单线程),不过可以自由变道(移交控制权)

协程实现

最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

这里是一个简单的例子证明协程的实用性。假设这样一种生产者-消费者的关系,一个协程生产产品并将它们加入队列,另一个协程从队列中取出产品并消费它们。伪码表示如下:

  1. var q := 新建队列
  2. coroutine 生产者
  3. loop
  4. while q 不满载
  5. 建立某些新产品
  6. q 增加这些产品
  7. yield 给消费者
  8. coroutine 消费者
  9. loop
  10. while q 不空载
  11. q 移除某些产品
  12. 使用这些产品
  13. yield 给生产者

v8 实现源码:js-generatorruntime-generator

编译模拟实现(es5):regenerator(如果时间充足在后面我可以雪微带大家扒一扒源码)

通过以上,我假装你明白什么是协程,下一步开始说一说迭代器 Iterator

Iterator

Iterator 翻译过来就是迭代器(遍历器)让我们先来看看它的遍历过程(类似于单向链表):

  • 创建一个指针对象,指向当前数据结构的起始位置
  • 第一次调用指针对象的 next 方法,将指针指向数据结构的第一个成员
  • 第二次调用指针对象的 next 方法,将指针指向数据结构的第二个成员
  • 不断的调用指针对象的 next 方法,直到它指向数据结构的结束位置

一个对象要变成可迭代的,必须实现 @@iterator 方法,即对象(或它原型链上的某个对象)必须有一个名字是 Symbol.iterator 的属性(原生具有该属性的有:StringArrayTypedArrayMapSet)可通过常量 Symbol.iterator 访问:

属性
[Symbol.iterator]: 返回一个对象的无参函数,被返回对象符合迭代器协议

当一个对象需要被迭代的时候(比如开始用于一个 for..of 循环中),它的 @@iterator 方法被调用并且无参数,然后返回一个用于在迭代中获得值的迭代器

迭代器协议:产生一个有限或无限序列的值,并且当所有的值都已经被迭代后,就会有一个默认的返回值

当一个对象只有满足下述条件才会被认为是一个迭代器:

它实现了一个 next() 的方法,该方法必须返回一个对象,对象有两个必要的属性:

  • done(bool)
    • true:迭代器已经超过了可迭代次数。这种情况下,value 的值可以被省略
    • 如果迭代器可以产生序列中的下一个值,则为 false。这等效于没有指定 done 这个属性
  • value 迭代器返回的任何 JavaScript 值。done 为 true 时可省略

根据上面的规则,咱们来自定义一个简单的迭代器:

  1. const getRawType = (target) => Object.prototype.toString.call(target).slice(8,-1);
  2. const __createArrayIterable = (arr) => {
  3. if (typeof Symbol !== 'function' || !Symbol.iterator) return {};
  4. if(getRawType(arr) !== 'Array') throw new Error('it must be Array');
  5. const iterable = {};
  6. iterable[Symbol.iterator] = () => {
  7. arr.length++;
  8. const iterator = {
  9. next: () => ({ value: arr.shift(), done: arr.length <= 0 })
  10. }
  11. return iterator;
  12. };
  13. return iterable;
  14. };
  15. const itable = __createArrayIterable(['人月', '神话']);
  16. const it = itable[Symbol.iterator]();
  17. console.log(it.next()); // { value: "人月", done: false }
  18. console.log(it.next()); // { value: "神话", done: false }
  19. console.log(it.next()); // { value: undefined, done: true }

我们还可以自定义一个可迭代对象:

  1. Object.prototype[Symbol.iterator] = function () {
  2. const items = Object.entries(this);
  3. items.length++;
  4. return {
  5. next: () => ({ value: items.shift(), done: items.length <= 0 })
  6. }
  7. }
  8. // or
  9. Object.prototype[Symbol.iterator] = function* () {
  10. const items = Object.entries(this);
  11. for (const item of items) {
  12. yield item;
  13. }
  14. }
  15. const obj = { name: 'amap', bu: 'sharetrip'}
  16. for (let value of obj) {
  17. console.log(value);
  18. }
  19. // ["name", "amap"]
  20. // ["bu", "sharetrip"]
  21. // or
  22. console.log([...obj]); // [["name", "amap"], ["bu", "sharetrip"]]

💡 除了 for map forEach 等方法如何用 Iterator 遍历一个数组?

了解了迭代器,下面可以进一步了解生成器了

Generator

Generator:生成器对象是生成器函数(GeneratorFunction)返回的,它符合可迭代协议迭代器协议,既是迭代器也是可迭代对象,可以调用 next 方法,但它不是函数,更不是构造函数

生成器函数(GeneratorFunction):

function* name([param[, param[, … param]]]) { statements }

  • name:函数名
  • param:参数
  • statements:js 语句

调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的迭代器对象,当这个迭代器的 next() 方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现 yield 的位置为止(让执行处于暂停状,挂起),yield 后紧跟迭代器要返回的值。或者如果用的是 yield*(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行),调用 next() (再启动,唤醒)方法时,如果传入了参数,那么这个参数会作为上一条执行的 **yield** 语句的返回值,例如:

  1. function* another() {
  2. yield '人月神话';
  3. }
  4. function* gen() {
  5. yield* another(); // 移交执行权
  6. const a = yield 'hello';
  7. const b = yield a; // a='world' 是 next('world') 传参赋值给了上一个 yidle 'hello' 的左值
  8. yield b; // b=! 是 next('!') 传参赋值给了上一个 yidle a 的左值
  9. }
  10. const g = gen();
  11. g.next(); // {value: "人月神话", done: false}
  12. g.next(); // {value: "hello", done: false}
  13. g.next('world'); // {value: "world", done: false} 将 'world' 赋给上一条 yield 'hello' 的左值,即执行 a='world',
  14. g.next('!'); // {value: "!", done: false} 将 '!' 赋给上一条 yield a 的左值,即执行 b='!',返回 b
  15. g.next(); // {value: undefined, done: true}

看到这里,你可能会问,Generatorcallback 有啥关系,如何处理异步呢?其实二者没有任何关系,我们只是通过一些方式强行的它们产生了关系,才会有 Generator 处理异步

我们来总结一下 Generator 的本质,暂停,它会让程序执行到指定位置先暂停(yield),然后再启动(next),再暂停(yield),再启动(next),而这个暂停就很容易让它和异步操作产生联系,因为我们在处理异步时:开始异步处理(网络求情、IO 操作),然后暂停一下,等处理完了,再该干嘛干嘛。不过值得注意的是,js 是单线程的(又重复了三遍),异步还是异步,callback 还是 callback,不会因为 Generator 而有任何改变

下面来看看,用 Generator + Promise 写一段异步代码:

  1. const gen = function*() {
  2. const res1 = yield Promise.resolve({a: 1});
  3. const res2 = yield Promise.resolve({b: 2});
  4. };
  5. const g = gen();
  6. const g1 = g.next();
  7. console.log('g1:', g1);
  8. g1.value
  9. .then(res1 => {
  10. console.log('res1:', res1);
  11. const g2 = g.next(res1);
  12. console.log('g2:', g2);
  13. g2.value
  14. .then(res2 => {
  15. console.log('res2:', res2);
  16. g.next(res2);
  17. })
  18. .catch(err2 => {
  19. console.log(err2);
  20. });
  21. })
  22. .catch(err1 => {
  23. console.log(err1);
  24. });
  25. // g1: { value: Promise { <pending> }, done: false }
  26. // res1: { "a": 1 }
  27. // g2: { value: Promise { <pending> }, done: false }
  28. // res2: { "b": 2 }

以上代码是 Generatorcallback 结合实现的异步,可以看到,仍然需要手动执行 .then 层层添加回调,但由于 next() 方法返回对象 {value: xxx,done: true/false} 所以我们可以简化它,写一个自动执行器:

  1. function run(gen) {
  2. const g = gen();
  3. function next(data) {
  4. const res = g.next(data);
  5. // 深度递归,只要 `Generator` 函数还没执行到最后一步,`next` 函数就调用自身
  6. if (res.done) return res.value;
  7. res.value.then(function(data) {//等上一个next成功了我才继续next
  8. next(data);
  9. });
  10. }
  11. next();
  12. }
  13. run(function*() {
  14. const res1 = yield Promise.resolve({a: 1});
  15. console.log(res1);
  16. // { "a": 1 }
  17. const res2 = yield Promise.resolve({b: 2});
  18. console.log(res2);
  19. // { "b": 2 }
  20. });

说了这么多,怎么还没有到 async/await,客官别急,马上来了(其实我已经漏了一些内容没说:Promise 和 callback 的关系,thunk 函数,co 库,感兴趣的可以去 google 一下,ruanyifeng 老师讲的es6 入门非常棒,我时不时的都会去看一看)

💡 分析下面 log 输出什么内容?

  1. function* gen() {
  2. const ask1 = yield "2 + 2 = ?";
  3. console.log(ask1);
  4. const ask2 = yield "3 * 3 = ?";
  5. console.log(ask2);
  6. }
  7. const generator = gen();
  8. console.log( generator.next(1).value );//2 + 2 = ?
  9. console.log( generator.next(4).value );//4, 3 * 3 = ?
  10. console.log( generator.next(9).done );//9, true

Async/Await

首先,async/awaitGenerator 的语法糖,上面我是分割线下的第一句已经讲过,先来看一下二者的对比:

  1. // Generator
  2. run(function*() {
  3. const res1 = yield Promise.resolve({a: 1});
  4. console.log(res1);
  5. const res2 = yield Promise.resolve({b: 2});
  6. console.log(res2);
  7. });
  8. // async/await
  9. const aa = async ()=>{
  10. const res1 = await Promise.resolve({a: 1});
  11. console.log(res1);
  12. const res2 = await Promise.resolve({b: 2});
  13. console.log(res2);
  14. return 'done'
  15. }
  16. const res = aa();

可以看到,async function 代替了 function*await 代替了 yield,同时也无需自己手写一个自动执行器 run

现在再来看看async/await 的特点:

  • await 后面跟的是 Promise 对象时,才会异步执行,其它类型的数据会同步执行?错,原始值时会将它包裹成promise并resolve,仍然为异步执行
  • 执行 const res = aa(); 返回的仍然是个 Promise 对象,上面代码中的 return 'done'; 会直接被下面 then 函数接收到
  1. res.then(data => {
  2. console.log(data); // done
  3. });

最后咱们来总结一下:

优点:

  • 内置执行器:自带执行器
  • 更好的语义:比起星号和 yield,语义更清楚了
  • 更广的适用性:await 命令后面,可以跟 Promise 对象和原始类型的值(都是异步操作)

注意点:

  1. await 命令只能用在 async 函数之中,如果用在普通函数,就会报错

    1. (async ()=>{
    2. [].map(()=>{
    3. const list = await getList(); // Uncaught SyntaxError: await is only valid in async function
    4. return list;
    5. })
    6. })();
  2. 多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发(Promise.all) ```javascript (async ()=>{ const list = await getList(); const anotherList = await getAnotherList(); })();

// 推荐写法: (async ()=>{ const [list, anotherList] = await Promise.all([getList(), getAnotherList()]) })();

  1. 3. `await` 命令后面的 `Promise` 对象,运行结果可能是 `rejected`,所以最好把 `await` 命令放在 `try...catch` 代码块中:
  2. ```javascript
  3. async function asyncTask() {
  4. try{
  5. const res1 = await fn1();
  6. if(!res1) throw new CustomerError('No res1 found');
  7. }catch(err){
  8. throw new CustomError('Error occurred while task1');
  9. }
  10. try{
  11. const res2 = await fn1();
  12. if(!res2) throw new CustomerError('No res2 found');
  13. }catch(err){
  14. throw new CustomError('Error occurred while task2');
  15. }
  16. }
  1. 错误捕获:需要捕获多个错误并做不同的处理时(多个 try...catch 很容易导致代码杂乱),可以考虑给 await 后的 promise 对象添加 catch 函数,为此我们需要写一个 helper:
  1. // to.js
  2. export default function to(promise) {
  3. return promise.then(data => {
  4. return [null, data];
  5. })
  6. .catch(err => [err]);
  7. }
  8. /***使用***/
  9. import to from './to';
  10. async function asyncTask() {
  11. const [err1, res1] = await to(fn1);
  12. if(!res1) throw new CustomerError('No res1 found');
  13. const [err2, res2] = await to(fn2);
  14. if(err) throw new CustomError('Error occurred while task2');
  15. }
  1. 在循环中需注意它的使用,尽量在 for/for..of(迭代遍历器) 中使用,永远不要在 forEach/filter 中使用,也尽量不要在 map 中使用
  2. 兼容性(caniusenode.green)不太好,当然一般情况下,可以借助编译工具来打补丁 polyfill(babel)或 es6-shim(转换后即语法糖实现的协程效率低,**generator(内部实现了co) + run** (run中涉及Promise)比 cb 的方式性能差)
  3. 可以在生命周期函数中使用,在线例子: ReactVue

💡 给定一个 URL 数组,如何实现接口的继发和并发?

啊,终于完了,一个 async-await 连带出来这么多知识点,以后在使用它时,希望能够帮助到你

【参考】:

  1. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols#可迭代协议
  2. http://es6.ruanyifeng.com/#docs/iterator
  3. http://es6.ruanyifeng.com/#docs/async