JS之详细理解异步编程下——前端之JavaScript高级之七【Day64-Day68】

307 阅读32分钟

挑战坚持学习1024天——前端之JavaScript高级

js基础部分可到我文章专栏去看 ---点击这里

Day64【2022年9月26日】

学习重点:生成器(Generator)

之前介绍了迭代器,现在介绍一下生成器,实际上生成器是一种特殊的迭代器

1.什么是生成器?

生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。

生成器函数也是一个函数,但是和普通的函数有一些区别:

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。生成器函数的返回值是一个Iterator(可迭代对象):生成器事实上是一种特殊的迭代器。 Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

生成器函数 虽然自定义的迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此需要谨慎地创建。生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态。 生成器函数使用 function*语法编写。 最初调用时,生成器函数不执行任何代码,而是返回一种称为 Generator 的迭代器。 通过调用生成器的下一个方法消耗值时,Generator 函数将执行,直到遇到 yield 关键字。可以根据需要多次调用该函数,并且每次都返回一个新的 Generator,但每个 Generator 只能迭代一次。 MDN:Instead, they return a special type of iterator, called a Generator.

生成器和普通函数的区别 Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。

2.生成器基本形式

生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要是可以定义函数的地方,就可以定义生成器。

// 生成器函数声明
function* generatorFn() {} 
// 生成器函数表达式
let generatorFn = function* () {} 
// 作为对象字面量方法的生成器函数
let foo = { 
 * generatorFn() {} 
} 
// 作为类实例方法的生成器函数
class Foo { 
 * generatorFn() {} 
} 
// 作为类静态方法的生成器函数
class Bar { 
 static * generatorFn() {} 
}
//标识生成器函数的星号不受两侧空格的影响:

// 等价的生成器函数: 
function* generatorFnA() {} 
function *generatorFnB() {} 
function * generatorFnC() {} 
// 等价的生成器方法:
class Foo { 
 *generatorFnD() {} 
 * generatorFnE() {} 
} 

箭头函数不能用来定义生成器函数。

调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行(suspended)的状态。与 迭代器相似,生成器对象也实现了 Iterator 接口,因此具有 next()方法。调用这个方法会让生成器开始或恢复执行。next()方法的返回值类似于迭代器,有一个 done 属性和一个 value (yield 后面一个的值)属性。函数体为空的生成器函数中间不会停留,调用一次 next()就会让生成器到达 done: true 状态。(注意是函数体为空的时候),调用next()才执行

实例

function* generatorFn() {} 
let generatorObject = generatorFn(); 
console.log(generatorObject); // generatorFn {<suspended>} 
console.log(generatorObject.next); // f next() { [native code] }
console.log(generatorObject.next()); // { done: true, value: undefined } 

value 属性是生成器函数的返回值,默认值为 undefined,可以通过生成器函数的返回值指定:

function* generatorFn() { 
 return 'foo'; 
} 
let generatorObject = generatorFn(); 
console.log(generatorObject); // generatorFn {<suspended>} 
console.log(generatorObject.next()); // { done: true, value: 'foo' } 
// 生成器函数只会在初次调用 next()方法后开始执行,如下所示:
function* generatorFn1() { 
 console.log('foobar'); 
} 
// 初次调用生成器函数并不会打印日志
let generatorObject1 = generatorFn1(); 
generatorObject1.next(); //  foobar
//浏览器返回
//{value: undefined, done: true}

生成器对象实现了 Iterable 接口,它们默认的迭代器是自引用的:

function* generatorFn() {} 
console.log(generatorFn); 
// f* generatorFn() {} 
console.log(generatorFn()[Symbol.iterator]); 
// f [Symbol.iterator]() {native code} 
console.log(generatorFn()); 
// generatorFn {<suspended>} 
console.log(generatorFn()[Symbol.iterator]()); 
// generatorFn {<suspended>} 
const g = generatorFn(); 
console.log(g === g[Symbol.iterator]()); 
// true

由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

因此可以遍历

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]

上面代码中,Generator 函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了 Iterator 接口,可以被...运算符遍历了。

3.yield

3.1.基本性质

Generator 一个有利于异步的特性是,它可以在执行中被中断、然后等待一段时间再被我们唤醒。通过这个“中断后唤醒”的机制,我们可以把 Generator看作是异步任务的容器,利用 yield 关键字,实现对异步任务的等待。 yield 关键字可以让生成器停止和开始执行,也是生成器最有用的地方。生成器函数在遇到 yield 关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 next()方法来恢复执行:

此时的yield 关键字有点像函数的中间返回语句,它生成的值会出现在 next()方法返回的对象里。通过 yield 关键字退出的生成器函数会处在 done: false 状态;通过 return 关键字退出的生成器函数会处于 done: true 状态。 yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

实例

function* generatorFn() {
    yield 'foo';
    yield 'bar';
    yield undefined;
    yield;
    yield null;
    return 'baz';
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // { done: false, value: 'foo' } 
console.log(generatorObject.next()); // { done: false, value: 'bar' } 
console.log(generatorObject.next()); // { done: false, value: undefined }
console.log(generatorObject.next()); // { done: false, value: undefined }
console.log(generatorObject.next()); // { done: false, value: null }
console.log(generatorObject.next()); // { done: true, value: 'baz' }
console.log(undefined === null); // false

另外需要注意,yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。 yield 关键字必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会抛出语法错误。


// 有效
function* validGeneratorFn() {
    yield;
}
// 无效
function* invalidGeneratorFnA() {
    function a() {
        yield;
    }
}
// 无效
function* invalidGeneratorFnB() {
    const b = () => {
        yield;
    }
}
// 无效
function* invalidGeneratorFnC() {
    (() => {
        yield;
    })();
}
//无效
(function () {
    yield 1;
})()
  // SyntaxError: Unexpected number

3.2.生成器对象作为可迭代对象

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

上面代码使用for...of循环,依次显示 5 个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

3.3.使用 yield 实现输入和输出(next传参)

除了可以作为函数的中间返回语句使用,yield 关键字还可以作为函数的中间参数使用。上一次让生成器函数暂停的 yield 关键字会接收到传给 next()方法的第一个值。yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。

实例 yield 关键字可以同时用于输入和输出

  function* generatorFn() { 
    return yield 'foo'; 
   } 
   let generatorObject = generatorFn(); 
   console.log(generatorObject.next()); // { done: false, value: 'foo' } 
   console.log(generatorObject.next('bar')); // { done: true, value: 'bar' }

因为函数必须对整个表达式求值才能确定要返回的值,所以它在遇到 yield 关键字时暂停执行并计算出要产生的值:"foo"。下一次调用 next()传入了"bar",作为交给同一个 yield 的值。然后这个值被确定为本次生成器函数要返回的值。

yield也可以被使用多次

   function* nTimes(n) { 
    for (let i = 0; i < n; ++i) { 
    yield i; 
    } 
   } 
   for (let x of nTimes(3)) { 
    console.log(x); 
   } 
   // 0 
   // 1 
   // 2

综合实例

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

上面代码中,第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。第三次运行Next方法的时候不带参数,所以z等于undefined,返回对象的value属性等于5 + NaN + undefined,即NaN

如果向next方法提供参数,返回结果就完全不一样了。上面代码第一次调用bnext方法时,返回x+1的值6;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5y等于24,所以return语句的值等于42

3.4.yield*

可以使用星号增强 yield 的行为,让它能够迭代一个可迭代对象,从而一次产出一个值

   // 等价的 generatorFn: 
function* generatorFn() { 
for (const x of [1, 2, 3]) { 
yield x; 
} 
} 
// function* generatorFn() { 
//     yield* [1, 2, 3]; 
//    } 
//    let generatorObject = generatorFn(); 
   for (const x of generatorFn()) { 
    console.log(x); 
   } 
   // 1 
   // 2 
   // 3

与生成器函数的星号类似,yield 星号两侧的空格不影响其行为:

//等价的
yield* [1, 2];
yield *[3, 4];
yield * [5, 6];

在一个 Generator 函数里面执行另一个 Generator 函数

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"

实现递归(重要作用)


   function* nTimes(n) { 
    if (n > 0) { 
    yield* nTimes(n - 1); 
    yield n - 1; 
    } 
   } 
   for (const x of nTimes(3)) { 
    console.log(x); 
   } 
   // 0 
   // 1 
   // 2

在这个例子中,每个生成器首先都会从新创建的生成器对象产出每个值,然后再产出一个整数。结果就是生成器函数会递归地减少计数器值,并实例化另一个生成器对象。从最顶层来看,这就相当于创建一个可迭代对象并返回递增的整数。

4.终止生成器的方法

next()、throw()、return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

  function* generatorFn() {} 
const g = generatorFn(); 
console.log(g); // generatorFn {<suspended>} 
console.log(g.next); // f next() { [native code] } 
console.log(g.return); // f return() { [native code] } 
console.log(g.throw); // f throw() { [native code] } 
//return()和 throw()方法都可以用于强制生成器进入关闭状态。

4.1.next

next()是将yield表达式替换成一个值。

  const g = function* (x, y) {
    let result = yield x + y;
    return result;
  };
  
  const gen = g(1, 2);
  console.log(gen.next());// Object {value: 3, done: false}
  console.log(gen.next(1));  // Object {value: 1, done: true}
  // 相当于将 let result = yield x + y
  // 替换成 let result = 1;

上面代码中,第二个next(1)方法就相当于将yield表达式替换成一个值1。如果next方法没有参数,就相当于替换成undefined。

4.2.return

return()是将yield表达式替换成一个return语句。

   const g = function* (x, y) {
    let result = yield x + y;
    return result;
  };
  
  const gen = g(1, 2);
  console.log(g); // generatorFn {<suspended>}
  console.log(gen.return(2)); // Object {value: 2, done: true}
  console.log(gen);//g {<closed>}
  // 相当于将 let result = yield x + y
  // 替换成 let result = return 2;

与迭代器不同,所有生成器对象都有 return()方法,只要通过它进入关闭状态,就无法恢复了。 后续调用 next()会显示 done: true 状态,而提供的任何返回值都不会被存储或传播:

function* generatorFn() { 
    for (const x of [1, 2, 3]) { 
    yield x; 
    } 
   } 
   const g = generatorFn(); 
   console.log(g.next()); // { done: false, value: 1 } 
   console.log(g.return(4)); // { done: true, value: 4 } 
   console.log(g.next()); // { done: true, value: undefined } 
   console.log(g.next()); // { done: true, value: undefined } 
   console.log(g.next()); // { done: true, value: undefined }

4.3.throw()

throw()是将yield表达式替换成一个throw语句,throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭:

  const g = function* (x, y) {
    let result = yield x + y;
    return result;
  };
  
  const gen = g(1, 2);
  console.log(gen.throw(new Error('报错'))); // Uncaught Error: 报错
  console.log(gen); // generatorFn {<suspended>} 
  try { 
   gen.throw('foo'); 
  } catch (e) { 
   console.log(e); // foo 
  } 
  console.log(gen); // generatorFn {<closed>}
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));

不过,假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对应的 yield,因此在这个例子中会跳过一个值。比如:

function* generatorFn() { 
    for (const x of [1, 2, 3]) { 
    try { 
    yield x; 
    } catch(e) {} 
    } 
   } 
   const g = generatorFn(); 
   console.log(g.next()); // { done: false, value: 1} 
   g.throw('foo'); 
   console.log(g.next()); // { done: false, value: 3}

在这个例子中,生成器在 try/catch 块中的 yield 关键字处暂停执行。在暂停期间,throw()方 法向生成器对象内部注入了一个错误:字符串"foo"。这个错误会被 yield 关键字抛出。因为错误是在 生成器的 try/catch 块中抛出的,所以仍然在生成器内部被捕获。可是,由于 yield 抛出了那个错误, 生成器就不会再产出值 2。此时,生成器函数继续执行,在下一次迭代再次遇到 yield 关键字时产出了 值 3。

总结

迭代是一种所有编程语言中都可以看到的模式。ECMAScript 6 正式支持迭代模式并引入了两个新的 语言特性:迭代器和生成器。

迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现 Iterable 接口的对象都有一个 Symbol.iterator 属性,这个属性引用默认迭代器。默认迭代器就像一个迭代器 工厂,也就是一个函数,调用之后会产生一个实现 Iterator 接口的对象。

迭代器必须通过连续调用 next()方法才能连续取得值,这个方法返回一个 IteratorObject。这 个对象包含一个 done 属性和一个 value 属性。前者是一个布尔值,表示是否还有更多值可以访问;后 者包含迭代器返回的当前值。这个接口可以通过手动反复调用 next()方法来消费,也可以通过原生消 费者,比如 for-of 循环来自动消费。

生成器是一种特殊的函数,调用之后会返回一个生成器对象。生成器对象实现了 Iterable 接口, 因此可用在任何消费可迭代对象的地方。生成器的独特之处在于支持 yield 关键字,这个关键字能够 暂停执行生成器函数。使用 yield 关键字还可以通过 next()方法接收输入和产生输出。在加上星号之 后,yield 关键字可以将跟在它后面的可迭代对象序列化为一连串值。--js高级程序设计

补充 Generator异步执行中为了更好的异步代码提出了co模块。 co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。 co,看作是一个加强版的 runGenerator。我们只需要在代码里引入 co 库,然后把写好的 generator 传进去,就可以轻松地实现 generator 异步了。 这里就不作过多补充。

5.今日精进

要明白自身现阶段在公司的定位,并通过一系列正向的努力提高在公司的价值。找准定位,明确方向,精准提升。个人的职业发展规划,亦是如此。

Day65【2022年9月27日】

学习重点:async/await

1.asyac函数是什么?

async关键字用于声明一个异步函数:async是asynchronous单词的缩写,异步、非同步;sync是synchronous单词的缩写,同步、同时;一句话一句话,它就是 Generator 函数的语法糖。async和await关键字让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用promise。async函数返回一个 Promise 对象。

1.1async的基本用法

语法

async function name([param[, param[, ... param]]]) {
    statements
}

参数 name函数名称。

param要传递给函数的参数的名称。

statements包含函数主体的表达式。可以使用await机制。

返回值 一个Promise,这个 promise 要么会通过一个由 async 函数返回的值被解决,要么会通过一个从 async 函数中抛出的(或其中没有被捕获到的)异常被拒绝。 返回值实例

async function func1() {
    return 1
}
console.log(func1())

1664299070114.png func1的运行结果其实就是一个Promise对象。因此也可以使用then来处理后续逻辑。

func1().then(res => {
    console.log(res);  // 30
})
//1

async 函数可能包含 0 个或者多个await表达式。await 表达式会暂停整个 async 函数的执行进程并出让其控制权,只有当其等待的基于 promise 的异步操作被兑现或被拒绝之后才会恢复进程。promise 的解决值会被当作该 await 表达式的返回值。使用async / await关键字就可以在异步代码中使用普通的try / catch代码块。

备注: await关键字只在 async 函数内有效。如果你在 async 函数体之外使用它,就会抛出语法错误 SyntaxError 。async/await的目的为了简化使用基于 promise 的 API 时所需的语法。async/await的行为就好像搭配使用了生成器和 promise。

async 关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上,由async不一定要跟一个await,后面也可没有await。 async 函数一定会返回一个 promise 对象。如果一个 async 函数的返回值看起来不是 promise,那么它将会被隐式地包装在一个 promise 中。 实例

//一些写法
async function foo() {} 
let bar = async function() {}; 
let baz = async () => {}; 
class Qux { 
 async qux() {} 
}
//以下两种写法等价
async function foo() {
    return 1
}

function foo() {
    Promise.resolve(1);
}

await 不能出现在箭头函数中

async函数本身在执行过程中是同步执行的 由下面这个实例说明

async function foo() {
    // await console.log(1);
    console.log(2);
}
foo();
console.log(11);
//2
//11

1.2跟Generator 函数进行对比

引用以下例子说明二者的关系

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

上面代码的函数gen可以写成async函数,就是下面这样。

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

async函数对 Generator 函数的改进,体现在以下四点。

(1)内置执行器。

Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

asyncReadFile(); 上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

(2)更好的语义。

async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

(3)更广的适用性。

co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

(4)返回值是 Promise。

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

2.await

await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。可以理解为,是让出了线程,跳出了 async 函数体。

如果接受一个promise reject 则后面的代码都不会执行

async function foo() { 
 console.log(1); 
 await Promise.reject(3); 
 console.log(4); // 这行代码不会执行
} 
// 给返回的期约添加一个拒绝处理程序 如果不处理则会报错
foo().catch(console.log); 
console.log(2); 
// 1 
// 2 
// 3

要完全理解 await 关键字,必须知道它并非只是等待一个值可用那么简单。JavaScript 运行时在碰 到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息 队列中推送一个任务,这个任务会恢复异步函数的执行。 因此,即使 await 后面跟着一个立即可用的值,函数的其余部分也会被异步求值。下面的例子演 示了这一点:

async function foo() { 
    console.log(2); 
    await console.log(5); //相当于 console.log(5); await null(相当于 await Promise.resolve(null));
    console.log(6);
    await setTimeout(() => { console.log(7); }, 1000); //相当于 setTimeout(() => { console.log(7); }, 1000); await null(相当于 await Promise.resolve(null));
    console.log(4); 
    console.log(await Promise.resolve(8)); 
   } 
   console.log(1); 
   foo(); 
   console.log(3); 
   // 1 2 3   5 6  4 8 7

执行情况如下: (1) 打印 1;

(2) 调用异步函数 foo();

(3)(在 foo()中)打印 2,5;

(4)(在 foo()中)await 关键字暂停执行,为立即可用的值 null 向消息队列中添加一个任务,将setTimeout推进宏任务队列;

(5) foo()退出;

(6) 打印 3;

(7) 同步线程的代码执行完毕;

(8) JavaScript 运行时从消息队列中取出任务,恢复异步函数执行;

(9)(在 foo()中)恢复执行,await 取得 null 值(这里并没有使用);

(10)(在 foo()中)打印 6,4将await 8推进微任务队列输出8最后打印7;

(11) foo()返回。

如果是以下情况:

async function foo() { 
    console.log(2); 
    console.log(await Promise.resolve(8)); 
    console.log(9); 
   } 
   async function bar() { 
    console.log(4); 
    console.log(await 6); 
    console.log(7); 
   } 
   console.log(1); 
   foo(); 
   console.log(3); 
   bar(); 
   console.log(5);
   //123458967

(1) 打印 1;

(2) 调用异步函数 foo();

(3)(在 foo()中)打印 2;

(4)(在 foo()中)await 关键字暂停执行,向消息队列中添加一个期约在落定之后执行的任务;

(5) 期约立即落定,把给 await 提供值的任务添加到消息队列;

(6) foo()退出;

(7) 打印 3;

(8) 调用异步函数 bar();

(9)(在 bar()中)打印 4;

(10)(在 bar()中)await 关键字暂停执行,为立即可用的值 6 向消息队列中添加一个任务;

(11) bar()退出;

(12) 打印 5;

(13) 顶级线程执行完毕;

(14) JavaScript 运行时从消息队列中取出解决 await 期约的处理程序,并将解决的值 8 提供给它;

(15) JavaScript 运行时向消息队列中添加一个恢复执行 foo()函数的任务;

(16) 异步任务完成,JavaScript 从消息队列中取出恢复执行 foo()的任务及值 8;

(TC39-ECMAScript对 await 后面是期约的情况如何处理做过一次修改。修改后,本例中的 Promise.resolve(8)只会生成一个异步任务。因此在新版浏览器中,这个示例的输出结果为 123458967。实际开发中,对于并行的异步操作我们通常更关注结果,而不依赖执行顺序。)

(17)(在 foo()中)打印 8;

(18)(在 foo()中)打印 9;

(19) foo()返回;

(20) JavaScript 运行时从消息队列中取出恢复执行 bar()的任务及值 6;

(21)(在 bar()中)恢复执行,await 取得值 6;

(22)(在 bar()中)打印 6;

(23)(在 bar()中)打印 7;

(24) bar()返回。

2.1 await需要注意的一些事项

await 关键字必须在异步函数中使用,不能在顶级上下文如script标签或模块中使用。 不过, 定义并立即调用异步函数是没问题的。下面两段代码实际是相同的:

async function foo() { 
 console.log(await Promise.resolve(3)); 
} 
foo(); 
// 3 
// 立即调用的异步函数表达式
(async function() { 
 console.log(await Promise.resolve(3)); 
})(); 
// 3 

此外,异步函数的特质不会扩展到嵌套函数。 因此,await 关键字也只能直接出现在异步函数的定 义中。在同步函数内部使用 await 会抛出 SyntaxError。 下面展示了一些会出错的例子:

// 不允许:await 出现在了箭头函数中
function foo() { 
 const syncFn = () => { 
 return await Promise.resolve('foo'); 
 }; 
 console.log(syncFn()); 
} 
// 不允许:await 出现在了同步函数声明中
function bar() { 
 function syncFn() { 
 return await Promise.resolve('bar'); 
 } 
 console.log(syncFn()); 
} 
// 不允许:await 出现在了同步函数表达式中
function baz() { 
 const syncFn = function() { 
 return await Promise.resolve('baz'); 
 }; 
 console.log(syncFn()); 
} 
// 不允许:IIFE 使用同步函数表达式或箭头函数
function qux() { 
 (function () { console.log(await Promise.resolve('qux')); })(); 
 (() => console.log(await Promise.resolve('qux')))();}

3.结合时间循环机制

async function async1(){
   console.log('async1 start');
    await async2();
    console.log('async1 end')
}
async function async2(){
    console.log('async2')
}
console.log('script start');
async1();
console.log('script end');
// 输出顺序:script start->async1 start->async2->script end->async1 end
//一个宏任务(script下同步)->一对微任务->一个宏任务... 循环
//由上面可看下从script start->async1 start->async2->script end都是同步任务

async/await 和 generator 方案,相较于 Promise 而言,有一个重要的优势:Promise 的错误需要通过回调函数捕获,try catch 是行不通的。而 async/await 和 generator 允许 try/catch。这也是一个可以作为命题点细节,大家留心把握。

4.async/await对比Promise的优势

  • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
  • Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余
  • 调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。

async捕获异常

async function fn(){
    try{
        let a = await Promise.reject('error')
    }catch(error){
        console.log(error)
    }
}

Day66【2022年9月28日】

学习重点:primose面试题及手写promise

常见考察的promise面试题有事件循环,及一些基本原理简单的问题前面以及介绍了,现介绍一下结合事件循环的考察模式。

1.事件循环实例

const promise = new Promise((resolve, reject) => {
    console.log(1);
    resolve(5);
    console.log(2);
    reject(6);
});

promise.then((res) => {
    console.log(3);
    console.log(res);
}).catch((res) => { //不会执行
    console.log(res);
});

console.log(4);
//1 2 4 3 5

Promise 中的处理函数是异步任务then 方法中传入的任务是一个异步任务且promise的状态一旦改变后就不可更改。

执行顺序如下:输出promise构造函数下立即执行的同步代码->输出同步代码4->输出异步代码3符合执行原则(script下宏任务-微任务)一个宏任务->一对微任务。

2.Promise 值穿透问题

   Promise.resolve(1)
    .then(Promise.resolve(2))
    .then(3)
    .then()
    .then(console.log) // 1
    .then(() => { console.log(44) })  // 44
    .then(() => { console.log(33) })  // 33
    .then(() => { console.log(77) })  // 77
// 1

//等价于

let p1 = new Promise((resolve, reject) => {
    console.log(111); // 111
    resolve(1);
});

p1.then(Promise.resolve(2))
    .then(3)
    .then()
    .then( () => { console.log(4) }) // 4
    .then((res) => { console.log(res) })  // undefined
    .then(() => { console.log(55) })    // 55
    .then(() => { console.log(66) })    // 66
//
111
1
4
44
undefined
33
55
77
66

可以看出为两个promise为交错执行,可以理解为执行完一个状态的promise后会暂时性的跳出执行另一个promise由此循环。then 只有遇到里面是函数的时候才会执行下去否则会一直穿透下去。且遇到一个执行完的then后不会再传给后面了。resolve 出来那个值,穿越了一个又一个无效的 then 调用,就好像是这些 then 调用都是透明的、不存在的一样,因此这种情形我们也形象地称它是 Promise 的“值穿透”。

Day67【2022年9月29日】

学习重点:手写promise

先来一个简单版本peomise的书写 一个简单版的 Promise 应该具备的最基本的特征, 可以接收一个 executor 作为入参,具备 pending、resolved 和 rejected 这三种状态。

function CutePromise(executor) {
    // value 记录异步任务成功的执行结果
    this.value = null;
    // reason 记录异步任务失败的原因
    this.reason = null;
    // status 记录当前状态,初始化是 pending
    this.status = 'pending';
     
    // 把 this 存下来,后面会用到
    //this指向永远指向当前的CutePromise实例,防止随着函数执行环境的改变而改变
    var self = this;
  
    // 定义 resolve 函数
    function resolve(value) {
        // 异步任务成功,把结果赋值给 value
        self.value = value;
        // 当前状态切换为 resolved
        self.status = 'resolved'; 
    }
    
    // 定义 reject 函数
    function reject(reason) {
        // 异步任务失败,把结果赋值给 value
        self.reason = reason; 
        // 当前状态切换为 rejected
        self.status = 'rejected';
    }
  
    // 把 resolve 和 reject 能力赋予执行器
    executor(resolve, reject);
}

写then方法

// then 方法接收两个函数作为入参(可选)
CutePromise.prototype.then = function(onResolved, onRejected) {
  
    // 注意,onResolved 和 onRejected必须是函数;如果不是,我们此处用一个透传来兜底
    if (typeof onResolved !== 'function') {
        onResolved = function(x) {return x};
    }
    if (typeof onRejected !== 'function') {
        onRejected = function(e) {throw e};
    }

    // 依然是保存 this
    var self = this;
    // 判断是否是 resolved 状态
    if (self.status === 'resolved') {
        // 如果是 执行对应的处理方法
        onResolved(self.value);
    } else if (self.status === 'rejected') {
        // 若是 rejected 状态,则执行 rejected 对应方法
        onRejected(self.reason);
    }
};

new CutePromise(function(resolve, reject){
    resolve('成了!');
}).then(function(value){
    console.log(value);
}, function(reason){
    console.log(reason);
});

// 输出 “成了!”

new CutePromise(function(resolve, reject){
    reject('错了!');
}).then(function(value){
    console.log(value);
}, function(reason){
    console.log(reason);
});

// 输出“错了!”

一个简单版本的promise实现了接下来实现链式调用。 主要加入链式调用要考虑以下几点

  • then方法中应该直接把 this 给 return 出去(链式调用常规操作);
  • 链式调用允许我们多次调用 then,多个 then 中传入的 onResolved(也叫onFulFilled) 和 onRejected 任务,我们需要把它们维护在一个队列里;
  • 要想办法确保 then 方法执行的时机,务必在 onResolved 队列 和 onRejected 队列批量执行前。不然队列任务批量执行的时候,任务本身都还没收集完,就乌龙了。一个比较容易想到的办法就是把批量执行这个动作包装成异步任务,这样就能确保它一定可以在同步代码之后执行了。

function CutePromise(executor) {
    // value 记录异步任务成功的执行结果
    this.value = null;
    // reason 记录异步任务失败的原因
    this.reason = null;
    // status 记录当前状态,初始化是 pending
    this.status = 'pending';
  
    // 缓存两个队列,维护 resolved 和 rejected 各自对应的处理函数
    this.onResolvedQueue = [];
    this.onRejectedQueue = [];
         
    // 把 this 存下来,后面会用到
    var self = this;
  
    // 定义 resolve 函数
    function resolve(value) {
        // 如果不是 pending 状态,直接返回
        if (self.status !== 'pending') {
            return;
        }
        // 异步任务成功,把结果赋值给 value
        self.value = value;
        // 当前状态切换为 resolved
        self.status = 'resolved'; 
        // 用 setTimeout 延迟队列任务的执行
        setTimeout(function(){
            // 批量执行 resolved 队列里的任务
            self.onResolvedQueue.forEach(resolved => resolved(self.value)); 
        });
    }
        
    // 定义 reject 函数
    function reject(reason) {
        // 如果不是 pending 状态,直接返回
        if (self.status !== 'pending') {
            return;
        }
        // 异步任务失败,把结果赋值给 value
        self.reason = reason; 
        // 当前状态切换为 rejected
        self.status = 'rejected';
        // 用 setTimeout 延迟队列任务的执行
        setTimeout(function(){
            // 批量执行 rejected 队列里的任务
            self.onRejectedQueue.forEach(rejected => rejected(self.reason));
        });
    }
  
    // 把 resolve 和 reject 能力赋予执行器
    executor(resolve, reject);
}

在then中加入对padding状态的处理


// then 方法接收两个函数作为入参(可选)
CutePromise.prototype.then = function(onResolved, onRejected) {
  
    // 注意,onResolved 和 onRejected必须是函数;如果不是,我们此处用一个透传来兜底
    if (typeof onResolved !== 'function') {
        onResolved = function(x) {return x};
    }
    if (typeof onRejected !== 'function') {
        onRejected = function(e) {throw e};
    }
 
    // 依然是保存 this
    var self = this;
    // 判断是否是 resolved 状态
    if (self.status === 'resolved') {
        // 如果是 执行对应的处理方法
        onResolved(self.value);
    } else if (self.status === 'rejected') {
        // 若是 rejected 状态,则执行 rejected 对应方法
        onRejected(self.reason);
    } else if (self.status === 'pending') {
        // 若是 pending 状态,则只对任务做入队处理
        self.onResolvedQueue.push(onResolved);
        self.onRejectedQueue.push(onRejected);
    }
    return this
};

const cutePromise = new CutePromise(function (resolve, reject) {
    resolve('成了!');
});
cutePromise.then((value) => {
    console.log(value)
    console.log('我是第 1 个任务')
}).then(value => {
    console.log('我是第 2 个任务')
}).then(value => {
    console.log('我是第 3 个任务')
})
// 成了!
// 我是第 1 个任务
// 我是第 2 个任务
// 我是第 3 个任务

Day68【2022年9月30日】

学习重点:手写promise(完善版)

const cutePromise = new CutePromise(function (resolve, reject) {
    resolve('成了!');
});

cutePromise.then((value) => {
    console.log(value)
    console.log('我是第 1 个任务')
    return '第 1 个任务的结果'
}).then(value => {
    // 此处 value 期望输出 '第 1 个任务的结果'
    console.log('第二个任务尝试拿到第 1 个任务的结果是:',value)
});

image.png 第二个 then 好像无视了第一个 then 的结果,仍然获取到的是我们在 Promise 执行器中 resolve 出的那个最初的值——这显然是不合理的。

事实上,除了这个最明显的缺陷,我们现在实现出来这个 Promise 还有很多能力上的问题,比如说 thenable 对象的特殊处理缺失、比如异常处理缺失等等,这些问题可以用一句话来归纳 —— 对 then 方法的处理过于粗糙。

上面的方法会有一个问题后面的then拿不到前面一个then的结果可从下面方面进行改造 构造函数改造 构造函数侧的改造无需太多,我们主要是把 setTimeout 给拿掉。这是因为后续我们会把异步处理放到 then 方法中的 resolveByStatus/ rejectByStatus 里面来做。

function CutePromise(executor) {
    // value 记录异步任务成功的执行结果
    this.value = null;
    // reason 记录异步任务失败的原因
    this.reason = null;
    // status 记录当前状态,初始化是 pending
    this.status = 'pending';
  
    // 缓存两个队列,维护 resolved 和 rejected 各自对应的处理函数
    this.onResolvedQueue = [];
    this.onRejectedQueue = [];
     
    // 把 this 存下来,后面会用到
    var self = this;
  
    // 定义 resolve 函数
    function resolve(value) {
        // 如果是 pending 状态,直接返回
        if (self.status !== 'pending') {
            return;
        }
        // 异步任务成功,把结果赋值给 value
        self.value = value;
        // 当前状态切换为 resolved
        self.status = 'resolved'; 
        // 批量执行 resolved 队列里的任务
        self.onResolvedQueue.forEach(resolved => resolved(self.value)); 
    }
    
    // 定义 reject 函数
    function reject(reason) {
        // 如果是 pending 状态,直接返回
        if (self.status !== 'pending') {
            return;
        }
        // 异步任务失败,把结果赋值给 value
        self.reason = reason; 
        // 当前状态切换为 rejected
        self.status = 'rejected';
        // 用 setTimeout 延迟队列任务的执行
        // 批量执行 rejected 队列里的任务
        self.onRejectedQueue.forEach(rejected => rejected(self.reason));
    }
  
    // 把 resolve 和 reject 能力赋予执行器
    executor(resolve, reject);
}

改造决议程序resolutionProcedure


function resolutionProcedure(promise2, x, resolve, reject) {
    // 这里 hasCalled 这个标识,是为了确保 resolve、reject 不要被重复执行
    let hasCalled;
    if (x === promise2) {
        // 决议程序规范:如果 resolve 结果和 promise2相同则reject,这是为了避免死循环
        return reject(new TypeError('为避免死循环,此处抛错'));
    } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        // 决议程序规范:如果x是一个对象或者函数,则需要额外处理下
        try {
            // 首先是看它有没有 then 方法(是不是 thenable 对象)
            let then = x.then;
            // 如果是 thenable 对象,则将promise的then方法指向x.then。
            if (typeof then === 'function') {
                // 如果 then 是是一个函数,那么用x为this来调用它,第一个参数为 resolvePromise,第二个参数为rejectPromise
                then.call(x, y => {
                    // 如果已经被 resolve/reject 过了,那么直接 return
                    if (hasCalled) return;
                    hasCalled = true;
                    // 进入决议程序(递归调用自身)
                    resolutionProcedure(promise2, y, resolve, reject);
                }, err => {
                    // 这里 hascalled 用法和上面意思一样
                    if (hasCalled) return;
                    hasCalled = true;
                    reject(err);
                });
            } else {
                // 如果then不是function,用x为参数执行promise
                resolve(x);
            }
        } catch (e) {
            if (hasCalled) return;
            hasCalled = true;
            reject(e);
        }
    } else {
        // 如果x不是一个object或者function,用x为参数执行promise
        resolve(x);
    }
}

设置then


// then 方法接收两个函数作为入参(可选)
CutePromise.prototype.then = function(onResolved, onRejected) {
  
    // 注意,onResolved 和 onRejected必须是函数;如果不是,我们此处用一个透传来兜底
    if (typeof onResolved !== 'function') {
        onResolved = function(x) {return x};
    }
    if (typeof onRejected !== 'function') {
        onRejected = function(e) {throw e};
    }
  
    // 依然是保存 this
    var self = this;
    // 这个变量用来存返回值 x
    let x
    
    // resolve态的处理函数
    function resolveByStatus(resolve, reject) {
        // 包装成异步任务,确保决议程序在 then 后执行
        setTimeout(function() {
            try { 
                // 返回值赋值给 x
                x = onResolved(self.value);
                // 进入决议程序
                resolutionProcedure(promise2, x, resolve, reject);
            } catch (e) {
                // 如果onResolved或者onRejected抛出异常error,则promise2必须被rejected,用error做reason
                reject(e);
            }
        });
    }
  
    // reject态的处理函数
    function rejectByStatus(resolve, reject) {
        // 包装成异步任务,确保决议程序在 then 后执行
        setTimeout(function() {
            try {
                // 返回值赋值给 x
                x = onRejected(self.reason);
                // 进入决议程序
                resolutionProcedure(promise2, x, resolve, reject);
            } catch (e) {
                reject(e);
            }
        });
    }

    
    // 注意,这里我们不能再简单粗暴 return this 了,需要 return 一个符合规范的 Promise 对象
    var promise2 = new CutePromise(function(resolve, reject) {
        // 判断状态,分配对应的处理函数
        if (self.status === 'resolved') {
            // resolve 处理函数
            resolveByStatus(resolve, reject);
        } else if (self.status === 'rejected') {
            // reject 处理函数
            rejectByStatus(resolve, reject);
        } else if (self.status === 'pending') {
            // 若是 pending ,则将任务推入对应队列
            self.onResolvedQueue.push(function() {
                 resolveByStatus(resolve, reject);
            });
            self.onRejectedQueue.push(function() {
                rejectByStatus(resolve, reject);
            });
        }
    });

    // 把包装好的 promise2 return 掉
    return promise2;
};

调用

const cutePromise = new CutePromise(function (resolve, reject) {
    resolve('成了!');
});

cutePromise.then((value) => {
    console.log(value)
    console.log('我是第 1 个任务')
    return '第 1 个任务的结果'
}).then(value => {
    // 此处 value 期望输出 '第 1 个任务的结果'
    console.log('第二个任务尝试拿到第 1 个任务的结果是:',value)
});


//成了!
//我是第 1 个任务
//第二个任务尝试拿到第 1 个任务的结果是: 第 1 个任务的结果

参考资料

  • JavaScript高级程序设计(第4版)
  • MDN
  • 解锁前端面试体系核心攻略
  • 鲨鱼哥面试题总结

结语

志同道合的小伙伴可以加我,一起交流进步,我们坚持每日精进(互相监督思考学习,如果坚持不下来我可以监督你)。我们一起努力鸭! ——>点击这里

备注

按照时间顺序倒叙排列,完结后按时间顺序正序排列方便查看知识点,工作日更新。