Promise,Generator,Async深入,es6笔记
引言:异步编程历史为:原生异步API的回调地狱-> Porrmise -> Generator -> Async,现在一个个先了解使用后深入。本文主体是依照阮一峰es6教程做的笔记。其中也有自己的总结和一些看法。
Promise
概述
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise对象有两个特点:
- 状态一旦改变,就已经凝滞,无法再进行更改。
- 不受外界影响
使用
创建Promise对象
new Promise(excutor);
//excutor 是一个函数,把要执行的操作(同步,异步)放入这个函数中执行。
excutor = (resolve,reject) =>{
//dosometing
resolve(value);
}
Promise对象有三个状态:pending,resolved,rejected。
Promise一创建就是pending状态,当在excutor中调用resolve()状态就会变为resolved,不再更改。当在excutor中调用reject()状态就会变为rejected,不在更改。
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
tip: 这个时候传进去的两个参数resolve,reject两个方法在Promise内部会实现并调用。
resolve = (value)=>{
}
reject = (value)=>{
}
excutor(resolve,reject);
Promise对象创建出来就可以进行消费了。具体方法有then,catch,finally。这些方法都放置在Promise构造函数的原型对象上。
tip: Promise内部是一个Class,其实就是一个构造函数的语法糖,构造函数最终会执行传进去的excutor,所以Promise对象一创建就会执行。new出来的promise实例所以也能用原型对象上的方法进行消费。
then,catch,finally进行消费(原型对象上的方法)
then方法
then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。
//成功回调
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
});
//同样的例子,失败回调
function timeout(ms) {
return new Promise((resolve, reject) => {
//失败了
reject('未能预期的错误发生了!');
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
},(value)=>{
console.log(value);
});
tip: 这里可以发现了then方法里放置的成功回调和失败回调函数一定会等到这个Promise的状态改变了之后才会触发,这是个观察者模式——收集依赖,触发通知,执行依赖。在Promise里,就是then收集依赖,resolve和reject触发依赖,最后执行依赖。
resolve和reject不仅可以带正常值,参数里还可以是promise对象。
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
当resolve(p)时,外层的promise由p的状态决定,上文的p2的回调会等到p1状态改变再触发回调,当然p2也得状态改变。如果把p2的回调放在p1之后也会先等到p2状态改变。
catch方法
catch方法是then的语法糖,相当于then(null,function (){}) 或者then(undefined, function (){})
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
function executor(resolve, reject) {
let rand = Math.random();
console.log(1)
console.log(rand)
if (rand > 0.5)
resolve()
else
reject()
}
var p0 = new Promise(executor);
var p1 = p0.then((value) => {
console.log("succeed-1")
return new Promise(executor)
})
var p3 = p1.then((value) => {
console.log("succeed-2")
return new Promise(executor)
})
var p4 = p3.then((value) => {
console.log("succeed-3")
return new Promise(executor)
})
p4.catch((error) => {
console.log("error")
})
console.log(2)
四个promise的对象任意一个出错都会被p4.catch捕捉到,这就是错误的冒泡。
const promise = new Promise(function (resolve, reject) {
resolve('ok');
setTimeout(function () { throw new Error('test') }, 0)
});
promise.then(function (value) { console.log(value) });
// ok
// Uncaught Error: test
由于抛错误Promise的运行已经结束了,所以这个错误是在Promise函数体外抛出,会冒泡到最外层,成为未捕获的错误。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行会报错,因为y没有声明
y + 2;
}).catch(function(error) {
console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]
catch方法中也可以抛出错误。
finally
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。
promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);
Promise的静态方法
静态方法有all,allSettled,race,any,resolve,reject,try。
根据用法我把这些方法分成了三组:resolve,reject;all,allSettled,race,any;try
先来看第一组:resolve和reject
Promise.resolve和Promise.reject
Promise.resolve(value)
resolve就是将现有对象转成Promise对象的方法。
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
//将jQuery的deferred对象转成promise对象
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
参数有四种情况:
- Promise实例对象
直接返回这个promise对象,不做修改
- thenable对象
thenable对象就是指有then方法的对象
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function (value) {
console.log(value); // 42
});
Promise.resolve()方法会将这个thenable对象先转成Promise对象,然后直接调用这个thenable对象的then方法。
- 不是thenable对象或者不是对象
如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved。
- 不带参数
和3一样,会返回一个已经resolved的promise对象,只不过没有value,then的成功回调自然也没有值。
所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法。
Promise.reject(reason)
返回一个新的rejected状态的实例。
里面的参数会原封不动的作为reject的reason,变成后续回调的参数。
all,race,allSettled,any
Promise.all()
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
参数接收一个可迭代的结构。(像数组,...)里面的每个遍历结果都要是promise实例,如果不是则自动调用Promise.resolve()转成promise对象
Promise.all()返回一个新的promise对象 p, p的状态由里面每个promise实例决定。
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
Promise.race()
与all很像,同样是将多个Promise实例,包装成一个新的Promise实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。
Promise.allSettled()
Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。
有时候,**我们不关心异步操作的结果,只关心这些操作有没有结束。**这时,Promise.allSettled()方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()方法无法做到这一点。
Promise.any()
Promise.any()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。
Promise.try()
实际开发中,经常遇到一种情况:不知道或者不想区分,函数f是同步函数还是异步操作,但是想用 Promise 来处理它。因为这样就可以不管f是否包含异步操作,都用then方法指定下一步流程,用catch方法处理f抛出的错误。一般就会采用下面的写法。
Promise.resolve().then(f)
由于then方法是微任务,所以同步任务也会任务队列末尾执行。
鉴于这是一个很常见的需求,提供Promise.try方法
Promise.try(() => database.users.get({id: userId}))
.then(...)
.catch(...)
database.users.get()返回一个 Promise 对象
总结
Promise是一个类,本质是封装好的构造函数,静态方法有:resolve,reject,all,race,allSettled,any,try。原型方法给创建出的promise实例消费,有:then,catch,finally。
Promise.resolve()将返回promsie实例(4种情况):1 promise直接返回;2 thenable对象会转成promise对象,调用then方法;3 非thenable对象和原始值,返回一个resolved的promise对象,值为传进去的参;4 不带参数,返回一个resolved的promise对象,值为undefined
Promise.reject()返回一个rejected的promise实例:直接把参数原封不动的作为值
实例用then,catch,finally来进行状态变更的相对应的触发函数,promise的使用是一个观察者模式:收集依赖(then),触发通知(resolve,reject),执行依赖。then是把成功和失败回调函数都注册在promise内部,等到状态一改变就进行相应的回调。catch和finally是特殊的then:
//catch相当于
promise.then(null,(reason)=> {
//do something
})
//finally不带回调参数,相当于
promise.then(()=>{
//do samething
},()=>{
//do samething
})
promise的错误有冒泡性,所以一般then第二个回调不传参,最后都用一个catch来捕捉。
all,race,any,allSettled都接受一个iterable结构作为参数,返回一个新的promise实例:all只有都resolved才会resolved,回调的值为value数组。race是最先改变的实例决定,回调值为最先改变的实例值。any是有一个resolved就会返回resolved的promise,都失败就返回失败值数组。allSettled只要所有实例都已经改变状态就触发,回调返回所有值数组。
Generator
概述
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator既是一个状态机,封存了多个内部状态(与协程切换,执行上下文有关),也是一个遍历器生成函数(内部实现next,thorw,return方法),这意味着可以依次遍历这个函数内部的每一个状态。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
Generator函数用 * 声明,其内部的yield就是暂停执行,意味着每次yield都是一个状态。而外部调用next就可以拿到yield的值并切换协程运行。
所以上段代码像我们展示了两个特点:暂停恢复(协程的切换),遍历器的使用(next)
与Iterator接口的关系:任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。 由于Generator就是一个遍历器生成函数,因此也可以直接把Generator函数赋值给Symbol.iterator属性,使对象可迭代。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
使用
yield
Gnenerator内部的关键字,yield单词意思是产出,后面可以带值。在代码中特点是协程中交出控制权(暂停执行)并把yield的结果返回给外部的遍历器,体现在next返回的对象value中。
一般的执行逻辑
- 首先调用Generator(遍历器生成函数)生成遍历器
- 调用后并不会执行内部执行函数,在第一次调用遍历器的next开始执行
- 在每次调用后会一直运行下去,直到遇到yield,并把yield的值包装成value和done的对象返回给遍历器的next返回结果,之后切换协程暂停Generator的运行。这里yield后面的表达式是惰性求值,只有运行到这里才会进行计算。
- Generator的终止条件是遇到return语句为止,把结果返回给对应的next对象返回值(没有return就相当于return undefined)
next方法的参数
next方法可以恢复Generator的执行,里面的参数是替换上一个yield表达式的值,所以第一次用next不可以传参。Generator内部每个yield的返回值都是由下一个next里面的参数替换的,如果没有就是undefined。
function * generator(){
let x= yield 1;
console.log(`x是${x}`);
let y = yield 2;
console.log(`y是${y}`);
}
gen = generator();
console.log(gen.next());
console.log(gen.next(1));
console.log(gen.next(2));
打印x为1,y为2,如果取消传的参数,值都为undefined。
迭代器的消费(for of,...)
由于Generator就是一个遍历器生成函数,所以调用Generator返回的遍历器可以直接拿来给一些能够自动迭代的操作,例如for of,...拓展运算符,解构赋值,Array.from ....
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 扩展运算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解构赋值
let [x, y] = numbers();
x // 1
y // 2
// for...of 循环
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
对象是没有内置Symbol.iterator属性的,也就是没有自己的遍历器生成函数,我们现在可以不用手写遍历器而是直接用Generator生成遍历器。
function* objectEntries() {
let propKeys = Object.keys(this);
for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
next,thorw,return(原型对象上的方法)
共同点:
都是恢复Generator的执行,并且替换yield的表达式:next替换上一个yield的值(第一个没有);throw将yield表达式替换成throw语句;return将yield表达式替换成一个return语句
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
yield * 表达式
ES6 提供了yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。
yield* 后面接受一个可迭代对象,相当于调用 for v of generator() {yield v;}
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo();
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"
如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。
function* foo() {
yield 2;
yield 3;
return "foo";
}
function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
由于可以接受一个可迭代对象,那些原生自带迭代器的数据结构也都可以用yield * 进行yield 遍历
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
read.next().value // "hello"
read.next().value // "h"
作为对象属性的Generator函数
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
//可简写
let obj = {
* myGeneratorMethod() {
···
}
};
关于this
Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。
可以理解Generator调用后生成直接生成实例(和Promise一样),如果在Generator中设置this,这个this会指向空对象,故无法通过this来设置实例属性。也无法new
下面是解决方法:
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F() {
return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
上段代码new 加不加都可以,因为F函数返回了自己的对象。借由gen函数把a,b,c这些属性挂载到了原型对象,所以实例能够访问到。
异步编程的应用
Generator异步任务封装
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();//fetch是一个异步请求函数,返回promise
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
可以看到,由于有yield暂停执行,generator内部几乎可以看成是同步执行代码,很简单,但它的分段执行则有点复杂。
Thunk函数
求值策略和Thunk函数
对于函数的参数中的表达式,func(x+1),在函数调用时有两种策略:
- 传值调用:在调用时直接计算出x+1再赋值给参数,在进入函数体前就计算
- 传名调用:把x+1这个表达式传入函数体,在需要用到再去计算。
编译器实现传名调用原理就是把这个表达式存在一个函数中,然后需要使用这个参数时候直接调用这个函数,返回参数的值,而这个函数就是Thunk函数。
function f(m) {
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk) {
return thunk() * 2;
}
Js中的Thunk函数
与上面提到的Thunk函数有一些不一样,在JS中Thunk是指将多参数函数替换成一个单回调函数参数的函数。
readfile的例子
// Thunk版本的readFile(单参数版本)
var Thunk=function(fileName){
return function(callback){
return fs.readFile(fileName, callback);
};
};
var readFileThunk =Thunk(fileName);
readFileThunk(callback);
上面将readFile的回调和一个文件名参数经过Thunk函数转成只接受一个回调参数的函数。
只要是有回调函数作为参数,就可以用Thunk函数转成一个只接受这个回调函数作为参数的函数:通用(指定调用的函数,传参数,传回调,前面两个都作为闭包给最后Thunk后的函数使用)
// ES5版本
varThunk=function(fn){
returnfunction(){
var args =Array.prototype.slice.call(arguments);
returnfunction(callback){
args.push(callback);
return fn.apply(this, args);
}
};
};
// ES6版本
constThunk=function(fn){
return function(...args){
return function(callback){
return fn.call(this,...args, callback);
}
};
};
Thunk函数的自动流程
Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器。
function run(fn){
var gen = fn();
function next(err, data){
var result = gen.next(data);
if(result.done)return;
result.value(next);
}
next();
}
function* g(){
// ...
}
run(g);
var g =function*(){
var f1 = yield readFileThunk('fileA');
var f2 = yield readFileThunk('fileB');
// ...
var fn = yield readFileThunk('fileN');
};
run(g);
首先看generator中的 readFileThunk函数,它是通过Thunk函数改造后的函数,再次调用(传参为参数)返回一个只有回调函数为参数的函数。通过run函数调用,gen协程开始运行:调用next,执行gen.next,如果没有遍历完则拿到返回对象的value(即最终只传回调函数的函数),调用这个函数,传参为这个next函数,这个时候就开始执行第一个异步函数,回调函数为next,当触发回调时,重复上述流程....
**Thunk 函数并不是 Generator 函数自动执行的唯一方案。**因为自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。
co模块
上文利用Thunk函数构造出了能够自动执行Generator的函数,现在co模块就能够直接实现这个功能。
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
var co = require('co');
//co一调用就会直接运行Generator,遍历完后返回一个promise对象
co(gen).then(function (){
console.log('Generator 函数执行完成');
});
基于Promise对象的自动执行
var fs = require('fs');
//返回promise对象的异步readFile函数
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) return reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);//恢复gen协程
if (result.done) return result.value;
result.value.then(function(data){ //注册回调并在回调中继续递归执行next
next(data);
});
}
next();
}
run(gen);
实现的基本原理
enerator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。
本质上在回调中执行next就能做到(promise内部也算回调)。
co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的**yield命令后面,只能是 Thunk 函数或 Promise 对象。**如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co。
co的源码分析
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
观察整体代码,co函数主要是在promise执行逻辑(所以最后返回一个promise对象),在promise内部:每次恢复generator的执行是执行onFulfilled或者OnRejected,(第一次直接调用onFulfilled),其内部都是调用next函数进行每次异步调用成功的函数注册(成功函数onFulfilled,失败回调onRejected)。上面代码中,next函数的内部代码,一共只有四行命令。
第一行,检查当前是否为 Generator 函数的最后一步,如果是就返回。
第二行,确保每一步的返回值,是 Promise 对象。
第三行,使用then方法,为返回值加上回调函数,然后通过onFulfilled函数再次调用next函数。
第四行,在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为rejected,从而终止执行。
相关深究
Generaror与状态机
Generator是实现状态机的最佳结构。
//一个简单的状态机
var ticking = true;
var clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}
用Generator改写
var clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
可以看到,Generator并没有使用外部变量来保存状态,更安全,这是因为Generator内部就包含状态信息。
协程
协程(coroutine)是一种程序运行的方式,可以理解成**“协作的线程”或“协作的函数”**。
协程与子例程
子例程(subroutine)采用堆栈式执行方式,意味着只要执行完子函数才能回到父函数。但协程在多线程(js是单线程多函数)情况下可以做到并行处理,并只有一个线程(函数)能够运行,其他线程处于暂停状态。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。
从实现上看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。
协程和普通线程
- 线程和协程都可以有自己的上下文,可以共享变量
- 线程可以多个并行处理,协程只能运行一个同一时间
- 线程是抢先式的,由运行环境决定分配的资源,协程的执行权由自己分配
Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。
如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用yield表达式交换控制权。
Gnerator与上下文
之前了解的垃圾回收机制,栈的回收是通过ESP指针来进行覆盖的,那么Generator函数执行会切换执行,当切换并调用其他函数会覆盖执行上下文,状态是如何保持的?
Generator 函数,它执行产生的上下文环境,一旦遇到yield命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。
function* gen() {
yield 1;
return 2;
}
let g = gen();
console.log(
g.next().value,
g.next().value,
);
例如上面yield后,里面的执行上下文会被保存起来,等到调用next继续恢复执行上下文并执行。
总结
Generator是一个遍历器生成函数,调用Generator会返回遍历器。内部关键词有yield,用来暂停Generator内部的执行。返回的这个遍历器是Generator函数的实例,因此可以使用原型对象上的方法(next,thorw,return)。
Gnerator函数在调用后并不会执行,直到实例gen调用next开始执行,遇到yield就切换协程暂停执行,继续调用next继续执行,直到遇到return 语句或抛出错误结束内部执行。
因为生成的遍历器,调用Generator后的遍历器(iterator)可以拿给for of 消费,也可以进行相关各种操作。
next,thorw,return是三个原型方法,其本质都是替换上一个yield的表达式。next,thorw,return 都有一个参数:next的参数用来替换上一个yield的值;thorw 将上一个yield表达式替换成 thorw error ; return 则是替换成 return something;next调用会使得Generator继续执行,而thorw和return则直接终止Generator函数返回最后的对象。
yield * 表达式用于遍历iterator(Generator也可以生成一个iterator),是 for item of iterator{ yield item}语法糖。
由于Generator会直接返回实例,也规定不能new,所以在Generator用this添加实例属性是不可以的,可以通过挂载的原型对象的this来生成实例,F: Generator.call(Generator.prototype), gen = new F();
Generator是用来解决异步编程的一个方案,可以利用yield和next来进行异步执行。简单来说就在需要异步执行的回调函数中执行next,执行next的时机则决定了下一次执行的时间(进行下一个异步任务)
Generator内部的执行顺序已经显而易见,但是执行它的函数则比较复杂繁琐,我们可以手动写好执行它的函数,当然也可以写一个自动执行的函数,而自动执行的逻辑是递归的(执行异步函数,在异步函数执行完毕后触发的回调函数执行next恢复进行,执行下一个异步函数),但是每个异步函数都是不一样的,参数也是不一样的,我们需要用到Thunk函数。
Thunk函数一般来说是来源于函数调用时参数为表达式的求值策略问题:直接计算表达式得到参数值再执行函数还是把表达式存储起来,需要用到参数再执行表达式得到参数的值。Thunk函数就是用来保存并返回参数表达式的函数。JS中的Thunk函数是指把多个参数(包含一个回调函数参数)的函数改造成只有一个回调函数参数的函数。
Thunk函数可以用来实现的Generator的自动执行函数(Generator内部 要求yeild Thunk后的函数(调用返回只有一个回调参数的函数)):
第一次直接调用next(),next具体实现为 调用gen.next() ,如果已经遍历完则直接return,再直接拿到 gen.next().value 即Thunk改造后的单回调参数函数,再进行调用并把next作为回调函数的参数,等到回调函数回调继续重复next()调用。
实现Generator的自动执行本质上只需要做到在异步操作完成之后能交换协程的控制权。不仅仅是Thunk函数可以递归做到,也可以用Promise来递归调用next实现。具体实现与Thunk很像,区别在于next是在promise的then函数调用。
co模块自己实现了Generator的自动执行,直接调用co(generator)就立刻执行。不需要自己再去实现执行函数了。
用co执行完返回的是一个promise对象,Generator中需要yeild promise对象或Thunk函数(Thunk后的函数也会被转成promise对象)。co实现的原理是基于Promise的自动执行,在错误捕捉和回调上做了补充。
Generator是一个状态机,内部储存了yeild的多种状态,Generator是ES6对协程的不完全实现,故Generator函数被称为半协程(只有Generator的调用者才能切换协程)。
Generator由于切换协程,执行上下文是需要进行保存和恢复的工作的,否则就会被垃圾回收机制通过ESP覆盖掉Generator中的执行上下文。
Async
概述
Async函数是Generator和Promise的语法糖。本质上Async还是一个Generator,yield相当于await命令,其中yield后的结果都会被包装成promise对象,利用上文类似co模块的思路进行实现自动执行。
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
相比于generator,async有以下改进:
- 内置自动执行器,执行Async函数就开始运行,不需要自己写generator的执行函数调用next,自动执行也可以用co(generator)来实现
- 更好的语义,await和async代表着等待和异步操作函数,比起 yield(产出)和 * 更好理解
- 适用性更广:使用co模块执行Generator的自动执行中,yield 后面只能接promise对象或者Thunk函数(Thunk函数最后也会被转成promise),在async中,await 后面可以接promise对象或者原始值(原始值被转成resolved后的promise)
- 最终返回的是promise对象: Generator调用后直接返回遍历器,需要自己手动遍历和自动执行,用来异步编程复杂繁琐,async和co模块调用一样最终返回一个promise对象,方便进行后续操作。
使用
基本用法
async函数的使用很简单,经过包装后几乎可以看成是写同步代码:执行所有异步操作前都接上await命令,用变量接收返回值。执行完内部所有操作最终外层返回promise
async function test(){
let result = await new Promise((resolve,reject)=>{
setTimeout(()=>resolve('value'),1000)
})
// .then(res=>console.log('cannot use then!!'));
console.log(result);
}
test().then(()=>{console.log('ok')});
- 可以看到,await后面返回的是一个1s后改变状态的promise对象,resolve的值最终也会在1s后返回
- 不能用then是因为,async的自动执行依赖于改写每个promise对象的then方法,在内部执行函数中每个await后的对象都填充了then方法(向外部填充值和调用next恢复执行)
- 最终返回的promise对象也可以进行then操作
语法
返回Promise对象
async返回一个promise对象,async内部return返回的值,会成为async的promise成功回调的值。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出错了
Promise的状态变化
async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
关于await
await内部是yield关键词的替换,后面要求接上promise对象(如果是其他就会调用Promise.resolve()转成promise对象),所以await 1 也会被注册为一个微任务。
正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
async function test(){
let rejectRes = await Promise.reject('error');
console.log('go next?');
}
test().then(res=>console.log(res)).catch(err=> console.log(err));
如果想让async的异步操作走完,可以用try catch捕捉。
async function test(){
try{let rejectRes = await Promise.reject('error');}
catch (e){
console.log(`catch it ${e}`)
}
console.log('can go next');
return await "ok";
}
test().then(res=>console.log(res)).catch(err=> console.log(err));
也可以用catch捕捉每次await的promise。
async function test(){
let rejectRes = await Promise.reject('error').catch(error=>console.log(`catch it ${error}`))
console.log('can go next');
return "ok";
}
test().then(res=>console.log(res)).catch(err=> console.log(err));
错误处理
如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject。
async function f() {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出错了
上面代码中,async函数f执行后,await后面的 Promise 对象会抛出一个错误对象,导致catch方法的回调函数被调用,它的参数就是抛出的错误对象。
使用try...catch结构,实现多次重复尝试。
const superagent = require('superagent');
const NUM_RETRIES = 3;
async function test() {
let i;
for (i = 0; i < NUM_RETRIES; ++i) {
try {
await superagent.get('http://google.com/this-throws-an-error');
break;
} catch(err) {}
}
console.log(i); // 3
}
test();
相关注意点
- 如果想要多个异步任务同时进行,每次await每个异步任务则不是那么合适
解决方法:
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
上面两种写法都可以让异步任务同时进行,不用再等待。
- 执行上下文会保存
const a = () => {
b().then(() => c());
};
上面代码中,函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()或c()报错,错误堆栈将不包括a()。
现在将这个例子改成async函数。
const a = async () => {
await b();
c();
};
上面b运行时,a交出协程运行权,但是a的执行上下文信息都保存着,直到调用next恢复。
async函数的实现原理
核心是spawn函数的实现(Generator的自动执行)
function spawn(genF){
returnnewPromise(function(resolve, reject){
const gen = genF();
function step(nextF){
let next;
try{
next = nextF();
}catch(e){
return reject(e);
}
if(next.done){
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v){
step(function(){return gen.next(v);});
},function(e){
step(function(){return gen.throw(e);});
});
}
step(function(){return gen.next(undefined);});
});
}
仔细看看可以发现与基于promise的自动执行很类似,这里不做解释。
关于顶层await
解决这种模块异步加载的问题:
//------ library.js ------
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diagonal(x, y) {
return sqrt(square(x) + square(y));
}
//------ middleware.js ------
import { square, diagonal } from './library.js';
console.log('From Middleware');
let squareOutput;
let diagonalOutput;
// IIFE
(async () => {
await delay(1000);
squareOutput = square(13);
diagonalOutput = diagonal(12, 5);
})();
function delay(delayInms) {
return new Promise(resolve => {
setTimeout(() => {
resolve(console.log('❤️'));
}, delayInms);
});
}
export {squareOutput,diagonalOutput};
//------ main.js ------
import { squareOutput, diagonalOutput } from './middleware.js';
console.log(squareOutput); // undefined
console.log(diagonalOutput); // undefined
console.log('From Main');
setTimeout(() => console.log(squareOutput), 2000);
//169
setTimeout(() => console.log(diagonalOutput), 2000);
//13
看到第二个js文件的delay函数在IIFE中调用,由于是延迟执行(await promise对象,需要等到resolve继续async函数的执行),如果当没有resovle前,导出的这两个变量值都为0.
解决方法是导出一个promise模块,用这个模块来连接是否完成了异步操作。
//解决方案
export default (async () => {
await delay(1000);
squareOutput = square(13);
diagonalOutput = diagonal(12, 5);
})();
//------ main.js ------
import promise, { squareOutput, diagonalOutput } from './middleware.js';
promise.then(()=>{
console.log(squareOutput); // 169
console.log(diagonalOutput); // 13
console.log('From Main');
setTimeout(() => console.log(squareOutput), 2000);// 169
setTimeout(() => console.log(diagonalOutput), 2000);// 13
})
上面导出了一个promsie对象,在引用时直接引入这个promise对象,用await完成返回的promise来控制。当然也可以直接把结果放在这个返回的promise设置回调的值。
export default (async () => {
await delay(1000);
squareOutput = square(13);
diagonalOutput = diagonal(12, 5);
return {squareOutput,diagonalOutput};
})();
promise.then(({squareOutput,diagonalOutput})=>{
console.log(squareOutput); // 169
console.log(diagonalOutput); // 13
console.log('From Main');
setTimeout(() => console.log(squareOutput), 2000);// 169
setTimeout(() => console.log(diagonalOutput), 2000);// 13
})
//------ main.js ------
import promise from './middleware.js';
promise.then(({squareOutput,diagonalOutput})=>{
console.log(squareOutput); // 169
console.log(diagonalOutput); // 13
console.log('From Main');
setTimeout(() => console.log(squareOutput), 2000);// 169
setTimeout(() => console.log(diagonalOutput), 2000);// 13
})
但这个方案有其自身的复杂性存在。
根据提案的说法,“这种模式的不良影响在于,它要求对相关数据进行大规模重构以使用动态模式;同时,它将模块的大部分内容放在 .then() 的回调函数中,以使用动态导入。从静态分析、可测试性、工程学以及其它角度来讲,这种做法相比 ES2015 的模块化来说是一种显而易见的倒退”。
顶层的await命令,就是为了解决这个问题。它保证只有异步操作完成,模块才会输出值。
当模块中有不在async函数中使用的await,整个模块会等到异步操作全部结束(即后面的promise改变状态)才会输出值。
对于上面的例子就直接去掉外层async使得await变成顶层await就行。
// IIFE
( () => {
await delay(1000);
squareOutput = square(13);
diagonalOutput = diagonal(12, 5);
})();
至于引入模块则正常import就行,没有区别(不用关心引入的模块是否有没有异步操作)
下面是顶层await的一些使用场景:
// import() 方法加载
const strings = await import(`/i18n/${navigator.language}`);
// 数据库操作
const connection = await dbConnector();
// 依赖回滚
let jQuery;
try{
jQuery = await import('https://cdn-a.com/jQuery');
}catch{
jQuery = await import('https://cdn-b.com/jQuery');
}
总结
Async是Generator和Promise的语法糖,内部关键词有await,用来等待异步操作。由于内部是基于generator实现,async也拥有相关的特点,也是状态机和拥有保存自己执行上下文的能力。
Async函数会返回一个promise对象用来管理内部异步操作:当所有的异步操作执行完毕,也就是Async函数走完使外部promise对象状态改变,return的值为promise成功回调的值。
关于await,后面可以接上原始值和promise对象,在内部都会转成Promise对象,具体规则可以参考Promise.resolve()。如果后面的promise对象如果状态改变成rejected,整个async函数终止运行,并返回外部rejected的promise。可以用try,catch块来包含或者await后的promise对象用catch来接住来使得Async函数不会被中断运行。
在async中如果有不是继发式的异步操作要运行,一行行使用 await不是很合适,这个时候可以先直接把多个异步操作运行并拿到返回的promise,一行行再await或者使用await Promise.all 【】就可以多个异步操作同时进行了。
async函数调用后就开始直接运行,其内部实现原理与co有异曲同工之妙,都实现了generator的自动执行函数。内部自动执行实现为递归调用: 首次调用async函数就开始调用next函数,next函数内部调用gen.next恢复async函数运行并拿到结果,把这个结果封装成promise并把调用next作为它的成功回调函数,失败回调则返回错误终止运行,这样就做到了递归,直到result.done为true。
关于顶层await,是为了解决模块的异步加载问题而出现的:简单来说就是引入模块时,当模块导出的值由于异步执行而没有赋值,这个模块的值为undefined的。最初的解决方法是再导出一个promise模块,用这个模块来控制模块是否异步加载完毕。但是这种解决方法显然不是很好:每次引入都需要清晰的知道这个模块是否异步,如果忘了引入promise控制就可能出错。