Promise
状态的特点
Promise 异步操作有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。
Promise 对象只有:从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ,状态就不会再变了即 resolved(已定型)。
const p1 = new Promise(function(resolve,reject){
resolve('success1');
resolve('success2');
});
const p2 = new Promise(function(resolve,reject){
resolve('success3');
reject('reject');
});
p1.then(function(value){
console.log(value); // success1
});
p2.then(function(value){
console.log(value); // success3
});
状态的缺点
无法取消 Promise ,一旦新建它就会立即执行,无法中途取消。
如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
then 方法
then 方法接收两个函数作为参数,第一个参数是 Promise 执行成功时的回调,第二个参数是 Promise 执行失败时的回调,该参数可选,两个函数只会有一个被调用。
resolve 与reject的参数将作为then中对应函数的参数,上一then的return 作为下一then中对应函数的参数,
Promise新建后会立即执行, then方法指定的回调函数将在当前脚本所有同步任务执行完成后才会执行,
catch方法
如果catch中的回调函数返回一个值或者没有返回值,那么catch返回的Promise将会成为Resolved状态,并且将返回的值作为Resolved状态的回调函数的参数值。上述代码中的c的状态就是Resolved状态。
如果catch中的回调函数抛出一个错误,那么catch返回的Promise将会成为Rejected状态,并且将抛出的错误作为Rejected状态的回调函数的参数值。
Promise.resolve()
有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。
Promise.resolve(‘foo’)
// 等价于
new Promise(resolve => resolve(‘foo’))
Promise.resolve方法的参数分成四种情况。
(1)参数是一个Promise实例
如果参数是Promise实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
(2)参数是一个thenable对象
thenable对象指的是具有then方法的对象,
Promise.resolve方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法。
(3)参数不是具有then方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为Resolved。
(4)不带有任何参数
Promise.resolve方法允许调用时不带参数,直接返回一个Resolved状态的Promise对象。
两个有用的附加方法
ES6的Promise API提供的方法不是很多,有些有用的方法可以自己部署。下面介绍如何部署两个不在ES6之中、但很有用的方法。
done()
Promise对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为Promise内部的错误不会冒泡到全局)。因此,我们可以提供一个done方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。
它的实现代码相当简单。
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function (reason) {
// 抛出一个全局错误
setTimeout(() => { throw reason }, 0);
});
};
从上面代码可见,done方法的使用,可以像then方法那样用,提供Fulfilled和Rejected状态的回调函数,也可以不提供任何参数。但不管怎样,done都会捕捉到任何可能出现的错误,并向全局抛出。
finally()
finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。它与done方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。
实现代码
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
上面代码中,不管前面的Promise是fulfilled还是rejected,都会执行回调函数callback。
Promise.try()
下面的写法有一个缺点,就是如果f是同步函数,那么它会在下一轮事件循环执行。
const f = () => console.log('now');
Promise.resolve().then(f);
console.log('next');
// next
// now
Promise.try()可以让同步函数同步执行,异步函数异步执行,还可以使promise.catch()捕获所有同步和异步的错误。
Promise.try(database.users.get({id: userId}))
.then(...)
.catch(...)
下一个任务执行前必须先拿到上一个任务的执行结果才能执行,因此下一个任务需要传入一个参数接受上一个任务的执行结果,代码如下:
var a = function (data){
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve(data + "a");
console.log("a");
}, 1000)
})
}
var b = function (){
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve("b");
console.log("b");
}, 2000)
})
}
b().then(function(data){
return a(data);
}).then(function(data){
console.log(data);
})
Generator 函数
next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield语句后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。
yield语句
由于Generator函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield语句就是暂停标志。
遍历器对象的next方法的运行逻辑如下。
(1)遇到yield语句,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句。
(3)如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
next方法的参数
yield句本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。
Generator.prototype.throw()
Generator函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获。
如果Generator函数内部和外部,都没有部署try…catch代码块,那么程序将报错,直接中断执行。
throw方法被捕获以后,会附带执行下一条yield语句。也就是说,会附带执行一次next方法。
只要Generator函数内部部署了try…catch代码块,那么遍历器的throw方法抛出的错误,不影响下一次遍历。
Generator.prototype.return()
Generator函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历Generator函数。
如果return方法调用时,不提供参数,则返回值的value属性为undefined。
如果Generator函数内部有try…finally代码块,那么return方法会推迟到finally代码块执行完再执行。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers()
g.next() // { done: false, value: 1 }
g.next() // { done: false, value: 2 }
g.return(7) // { done: false, value: 4 }
g.next() // { done: false, value: 5 }
g.next() // { done: true, value: 7 }
yield*语句
用来在一个Generator函数里面执行另一个Generator函数。
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"
任何数据结构只要有Iterator接口,就可以被yield*遍历。
如果被代理的Generator函数有return语句,那么就可以向代理它的Generator函数返回数据。
如果一个对象的属性是Generator函数,可以简写成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
}
使用场景之一
实现 Iterator
为不具备 Iterator 接口的对象提供遍历方法。
function* objectEntries(obj) {
const propKeys = Reflect.ownKeys(obj);
for (const propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
const jane = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
Generator函数的this
Generator函数总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,也继承了Generator函数的prototype对象上的方法。
Generator函数也不能跟new命令一起用,会报错。
如下办法可以让Generator函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的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
协程
多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。运行的协程只能有一个,其他协程都处于暂停状态。
Thunk函数
编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做Thunk函数。JavaScript语言是传值调用,它的Thunk函数含义有所不同。在JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。任何函数,只要参数有回调函数,就能写成Thunk函数的形式。
Thunkify模块
生产环境的转换器,建议使用Thunkify模块。安装后引入就能用了
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
// ...
});
Generator函数的自动流程管理需要用到thunk函数
Thunk函数并不是Generator函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制Generator函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。
co模块
也是用于Generator函数的自动执行。
co模块可以让你不用编写Generator函数的执行器。
Generator函数只要传入co函数,就会自动执行。
co函数返回一个Promise对象,因此可以用then方法添加回调函数。
var co = require('co');
co(gen).then(function (){
console.log('Generator 函数执行完成');
});
使用co的前提条件是,Generator函数的yield命令后面,只能是Thunk函数或Promise对象。
async函数
async函数对 Generator 函数的改进,体现在以下四点。
(1)内置执行器。Generator函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。
var result = asyncReadFile();
上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像Generator函数,需要调用next方法,或者用co模块,才能得到真正执行,得到最后结果。
(2)更好的语义。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。 co模块约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
(4)返回值是Promise。async函数的返回值是Promise对象,这比Generator函数的返回值是Iterator对象方便多了。你可以用then方法指定下一步的操作。
进一步说,async函数完全可以看作多个异步操作,包装成的一个Promise对象,而await命令就是内部then命令的语法糖。
1,async函数内部return语句返回的值,会成为then方法回调函数的参数。
2,async函数返回的Promise对象,必须等到内部所有await命令的Promise对象执行完,才会发生状态改变。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
3,正常情况下,await命令后面是一个Promise对象。如果不是,会被转成一个立即resolve的Promise对象。
只要一个await语句后面的Promise变为reject,那么整个async函数都会中断执行。可以用try catch捕获或promise.catch()
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
await针对所跟不同表达式的处理方式:
• Promise 对象:await 会暂停执行,等待 Promise 对象 resolve,然后恢复 async 函数的执行并返回解析值。
• 非 Promise 对象:直接返回对应的值。
其它
Generator想要顺序执行异步操作,不管是promise还是thunk函数,都需要在回调函数中执行下一个next来启动异步请求