使用async需要先知道的ES6异步函数

84 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

关于async await 这俩关键字,每次使用的时候都是面向百度,没有深入的去了解,导致使用的时候运行和预期不一样,今天2022.12.01号,我实在是忍不了了,作为一个程序员掌握不了薪资的涨幅,难搞还不能掌控自己的代码吗╰(‵□′)╯


async 函数是语法糖

让我们打开阮一峰老师编写的ES6标准入门教程 第三版 第20章 以下引入教材原文

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。 async 函数是什么?一句话,它就是 Generator 函数的语法糖。

嗯,async可以让异步操作更方便,这我知道;然后它是Genetator的语法糖;桥豆麻袋我项目里面有用过这个吗?什么机?发电机?好吧复制粘贴打开有道。 image.png 为了让我显得更专业一点就统一叫“生成器”好了。 让我们继续,知识嘛就和俄罗斯套娃一样,只有学的深入了,才不会是虚有其表的假大空。

Generator函数

ok,我们来到了教程的第18章

基本概念

Generator是ES6提供的一种异步编辑的解决方案,和传统函数不同。形式上与普通函数不同:

function* helloWorldGenerator() { //function关键字与函数名之间有一个星号
    yield 'hello'; //函数体内部使用yield表达式
    yield 'world';
    return 'ending';
}
var hw = helloWorldGenerator();

调用的方法与普通函数一样,也是函数名后面加圆括号,不一样的是调用这个函数并不会执行返回的也不是函数运行结果,而是指向一个内部状态的指针对象,这个对象是Iterator Object,此次先挖个Iterator的坑,等有时间再研究。让编辑器运行以下代码

console.log(hw.next());
console.log(hw.next());
console.log(hw.next());
console.log(hw.next());
console.log(hw.next());

那我们看看执行的结果

image.png

教程写到Genetator函数是一个状态机,内部封装了多个状态,每次执行Genetator函数都会返回一个遍历器对象。Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

一共执行了5次,其中valuenext方法返回的yield表达式的值,done则表示遍历是否结束。前两次的done为false是因为函数还没有执行完毕,第三次结果是函数执行到了return语句,done为true,当没有return语句时,就执行到函数结束,后两次的undefined是因为函数已经运行结束,value为undefined,node为true。

ennn...不难理解,继续

yield 表达式

上面我们了解了generator的基本使用,知道了yield是generator的表达式,也知道了只有调用next方法才会遍历下一个内部状态,所以其实是提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

遍历器对象的next方法运行逻辑如下:

  1. 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
  2. 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
  3. 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
  4. 如果该函数没有return语句,则返回的对象的value属性值为undefined

ok,yield是一个暂停标志

在generator函数里yieldreturn语句有相同也有不同,相同在于每次执行返回语句后面的那个表达式的值,不同在于遇到yield,函数暂停,下次从这个位置继续向后面执行,return语句则直接结束函数运行。

yield的使用

function* demo() {
    console.log('Hello' + yield); // SyntaxError
    console.log('Hello' + yield 123); // SyntaxError
    console.log('Hello' + (yield)); // OK
    console.log('Hello' + (yield 123)); // OK
}
function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}

了解,yield可以多个,而return只能有一个

Generator与状态机

Generator是实现状态机的最佳结构,这句话也是教程里面的,下面还是引用老师的代码

// ES5
var ticking = true;
var clock = function(){
    if(ticking){
        console.log('tick!')
    }else{
        console.log('tock!')
    }
    ticking = !ticking;
}
// ES6
var clock = function* (){
    while(true){
        console.log('tick!');
        yield;
        console.log('tock!');
        yield;
    }
}

上面的代码不管是从数量还是结构上Generator都比ES5要优雅,简洁,因Generator本身包含状态信息,所以不需要外部变量来保存状态。

next方法的参数

yield表达式本身是没有返回值的,没有返回值就总是返回undefined,当next方法有带参数时,这个参数就是yield的返回值

function* f() {
  for(var i = 0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}
console.log(g.next());//{value: 0, done: false}
console.log(g.next());//{value: 1, done: false}
console.log(g.next());//{value: 2, done: false}
console.log(g.next());//{value: 3, done: false}
console.log(g.next(true));//{value: 0, done: false}
console.log(g.next());//{value: 1, done: false}
console.log(g.next());//{value: 2, done: false}
console.log(g.next());//{value: 3, done: false}

这个案例先是定义了一个无限运行的Generator函数f,由于前4次next方法没有传递参数,所以变量reset总是undefined,第五次调用next时传递了true参数,使得reset变量为true,执行i = -1,所以下一次next方法从-1开始。

与for...of的使用

for...of循环自动遍历Generator函数生成的Iterator对象,不需要调用next方法

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}
for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

next方法返回对象的done属性为true时,for...of循环就会终止,这也就是为什么return语句没有在循环中返回的原因。 除了使用for--of循环外,扩张运算符...、解构赋值和Array.from方法内部调用的,都是遍历器接口,也就意味它们都可以将着Generator函数返回的Iterator对象作为参数。

function* numbers(){
    yield 1
    yield 2
    return 3
    yield 4
}
// 扩展运算符
[...numbers()] //[1,2]
//Array.from 方法
Array.form((numbers())//[1,2]
//解构赋值
let [x,y] = numbers()
x //1
y //2
//for...of循环
for(let n of numbers){
    console.log(n)
}
//1
//2

Generator.prototype.throw()

throw可以在函数体外抛出错误然后在Generator函数体内捕获。

let g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('内部捕获', e);
  }
};
let i = g();
console.log(i.next());

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
//内部捕获 a
//外部捕获 b

上面遍历器对象i连续抛出两个错误,一个错位被Generator函数体内的catch捕获,i第二次抛出的错误,由于Generator函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了Generator函数体,被函数体外的catch语句捕获。

需要注意的是throw方法抛出的错误要被内部捕获的前提是必须先执行一次next方法

function* gen() {
  try {
    yield 1;
  } catch (e) {
    console.log('内部捕获');
  }
}
let g = gen();
g.throw(1);//这样写会导致整个程序报错

next方法一次都没执行过,等于没有启动Generator函数的内部代码,这时throw方法只能函数外部抛出错误,导致程序出错。

throw方法被捕获后,还是会执行下一条的yield表达式。

let gen = function* gen(){
  try {
    yield 'a';
  } catch(d) {
    console.log('内部捕获',d);
  }
  yield console.log('b');
  yield console.log('c');
}
var g = gen();
console.log(g.next());// {value: 'a', done: false}
console.log(g.throw('d'));
// 内部捕获 d
// b
console.log(g.next());// c

g.throw被捕获以后,还自动执行了一次next方法,所以后面紧跟着打印了一个b,由此可以总结出Generator函数内try...catch,遍历器throw抛出错误也不影响下一次遍历。

Generator.prototype.return()

Generator函数返回编辑器对象还有一个return方法,可以返回给定的值,并终结遍历。

function* gen():any {
  yield 1;
  yield 2;
  yield 3;
}
var g = gen();
console.log(g.next());// { value: 1, done: false }
console.log(g.return('foo'));// { value: "foo", done: true }
console.log(g.next());//{value: undefined, done: true}

遍历器调用return方法后,返donetrue,意味着Generator函数遍历被终结,后面的next方法value将一直为undefined

next() throw() return()的共同点

next()

yield表达式替换为一个值

function* g(x:number, y:number) {
  let result = yield x + y;
  return result;
};
const gen = g(3, 2);
console.log(gen.next());// Object {value: 5, done: false}
console.log(gen.next(1));// Object {value: 1, done: true}

next(1)yield表达式替换为一个值1;如果next没有参数则相当于替换为undefined。

throw()

yield表达式替换成一个throw语句

console.log(gen.throw(new Error('出错了')))//这一行将会抛出一个函数外的错误
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));

return()

yield表达式替换为一个return语句

gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;

yield*

yield*表达是ES6为了解决Generator函数嵌套而提供的表达式,具体使用方法如下

// ES6 使用yield*
function* foo() {
  yield 'a';
  yield 'b';
}
function* bar() {
  yield 'x';
  yield* foo();// 使用yield*表达,优雅实在是优雅
  yield 'y';
}
for (let v of bar()){
  console.log(v);
}

// ES5 还没有yield*
function* foo() {
  yield 'a';
  yield 'b';
}
function* bar() {
  yield 'x';
  // 需手动遍历
  for (let i of foo()) {
    console.log(i);
  }
  yield 'y';
}
for (let v of bar()){
  console.log(v);
}

需要注意,任何数据结构,只要有Iterator接口,都可以被yield*遍历

function* gen(){
  yield* ["a", "b", "c"];
}
let g = gen()
console.log(g.next());// {value: 'a', done: false}
console.log(g.next());// {value: 'b', done: false}
console.log(g.next());// {value: 'c', done: false}
console.log(g.next());// {value: undefined, done: true}

for(let v of gen()){
  console.log(v);
}
// a
// b
// c
let read = (function* () {
  yield 'hello';
  yield* 'hello';
})();
console.log(read.next().value)// hello
console.log(read.next().value)// h
console.log(read.next().value)// e
console.log(read.next().value)// l
console.log(read.next().value)// l
console.log(read.next().value)// o
console.log(read.next().value)// undefined

当Generator函数作为对象属性时的写法

// 简写
let obj ={
    *myGeneratorMethod(){...}
}
// 完整形式
let obj ={
    myGeneratorMethod: function* (){...}
}

Generator函数的this

Generator函数总是返回一个遍历器,ES6规定这个遍历器就是Generator函数的实例,也就同时继承了prototype对象上的方法。

function* g(){}
g.prototype.hello = function(){return 'hi!'}
let obj = g()
console.log(obj instanceof g)//true
console.log(obj.hello())//hi!

在Generato函数里直接使用this并不会报错

function* g(){this.a=11}
let obj = g()
//在ts编译下从this.a就开始报错了
obj.next()
obj.a

解决办法

function* F():any {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj:any = {};
//使用call方法绑定Generator函数内部的this
var f = F.call(obj);
console.log(f.next());// Object {value: 2, done: false}
console.log(f.next());// Object {value: 3, done: false}
console.log(f.next());// Object {value: undefined, done: true}

console.log(obj.a);// 1
console.log(obj.b);// 2
console.log(obj.c);// 3

首先在F内部的this对象上绑定obj对象,然后调用它,返回一个Iterator对象。这个对象执行三次next方法,当完成F内部所有的代码运行;所有内部属性都绑定在obj对象上了,因此obj对象也就成了F的实例。 上面的代码,执行的是遍历器对象f,生成的对象实例是obj,教程中给出的解决办法是:将obj换成F.prototype

function* F():any {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj:any = {};
var f = F.call(F.prototype);
console.log(f.next());// Object {value: 2, done: false}
console.log(f.next());// Object {value: 3, done: false}
console.log(f.next());// Object {value: undefined, done: true}

console.log(f.a);// 1
console.log(f.b);// 2
console.log(f.c);// 3

结语

至此,async的语法糖Generator函数学习暂时告一段落,这篇文章从12月1号开始编辑,到今天13号;期间有加班有摆烂有放弃的想法,看着自己收藏夹和草稿箱;为了证明自己的学习没有浪费光阴,谨以此记录时间的印记。

预告下期学习笔记————Generator的异步应用,学习资源还是阮一峰老师的教程

另外祝各位同行都能有可控的选择!