挑战坚持学习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方法提供参数,返回结果就完全不一样了。上面代码第一次调用b的next方法时,返回x+1的值6;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5,y等于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())
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)
});
第二个 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
- 解锁前端面试体系核心攻略
- 鲨鱼哥面试题总结
结语
志同道合的小伙伴可以加我,一起交流进步,我们坚持每日精进(互相监督思考学习,如果坚持不下来我可以监督你)。我们一起努力鸭! ——>点击这里
备注
按照时间顺序倒叙排列,完结后按时间顺序正序排列方便查看知识点,工作日更新。