Promise/Generator/async 《异步编程的解决方案 》
Promise 基础认识
用 Promise 主要是换种写法而已 (通过callBack的思想把异步编程变成同步的一种写法) 英文是“承诺”的意思,表示其它手段无法改变。
Promise 的原理?(面试题)
一句话总结promise原理:resolve, reject两个回调函数控制promise内部状态,promise.then()根据状态执行不同的逻辑,实现异步。并返回已固定状态的新 promise,完成 then 链。
- Promise 的状态
在Promise的内部,有一个状态管理器的存在,有三种状态:pending、fulfilled、rejected。
(1) promise 对象初始化状态为 pending。
(2) 当调用resolve(成功),会由pending => fulfilled。
(3) 当调用reject(失败),会由pending => rejected。
需要记住的是注意promsie状态 只能由 pending => fulfilled/rejected, 一旦修改就不能再变。 - then 链的实现原理
实例每一次 .then() 之后会重现返回一个状态已固定的 Promise ,所有可以继续使用新的 Promise 原型上的 .then() 方法。 - 哪里可以拿到失败状态的值
.then() 的第二个回调函数的参数,或者 在 .catch() 的第一个回调函数的参数中
(下文有详细说明)
Promise 的私有属性:
[[PromiseState]]: pending
状态
三种状态:pending(准备状态 默认)、fulfilled(成功状态)和 rejected(失败状态),只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是Promise这个名字的由来。
Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected,只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型),如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果,这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
[[PromiseResult]]: undefined
值
默认是未定义,当状态改为成功,存储的是成功的结果,反之是失败的原因
注意:只有 Promise 体内的代码是同步进行的。
如何改变状态
new Promise([executor])
- executor(执行者)必须是一个函数,不传递函数则报错
- executor函数中会有两个形参:resolve & reject
- resolve 和 reject 的值都是个函数
- new Promise 的时候,会把传递进来的executor函数立即执行
- 函数中一般用来管理一个异步编程的代码
executor函数中的形参执行
- resolve(value):把实例状态修改为fulfilled,值改为value
- reject(reason):把实例状态修改为rejected,值改为reason
- executor执行报错,也会把实例的状态改为rejected,值是报错的原因
一但状态被修改为成功或者失败后,则不能再更改它的状态
实例.then(onfulfilled,onrejected)
- 如果此时实例的状态是fulfilled,则把onfulfilled执行,并且把实例的值传递给形参value
- 如果此时实例的状态是rejected,则把onrejected执行,并且把实例的值传递给形参reason
- 如果此时还不知道实例的状态{状态还是pending},则先把onfulfilled、onrejected都存储起来,等到后期实例的状态被更改后,再拿出对应的方法执行!!
- 同时一个实例可以多次调用then方法,当实例状态确定后,会把每一次基于then传递的onfulfilled或者onrejected都去执行「这种写法不常用」
- then链机制:每一次执行then方法,都会返回一个全新的promise实例「p2/p3」
- then的“穿透/顺延”机制:如果onfulfilled或者onrejected没有传递,则Promise内部会自动补全对应的方法,实现穿透的效果 “顺延到下一个then相同状态应该执行的方法上”
创建promise实例的办法
- new Promise([executor])
实例的状态和值由executor函数执行是否报错 & resolve/reject函数执行 来决定 - .then(onfulfilled,onrejected)
实例的状态和值和onfulfilled或者onrejected的方法执行有关系「不论哪个方法执行,都是按照以下规则来影响新返回实例的状态和值」
@1 看方法执行是否会报错,如果报错了,则新实例的状态是rejected,值是报错的原因
@2 如果没有报错,则再看方法执行的返回值,是否为一个新的promsie实例「@NEW」,如果是,则@NEW的状态和值,直接决定了.then返回的P2实例的状态和值
@3 如果方法返回值不是新的实例,则P2的状态就是fulfilled,值就是函数的返回值 - Promise.resolve([value]):直接创建一个状态是成功,值是[value]的实例
- Promise.reject([reason]):直接创建一个状态是失败,值是[reason]的实例
- Promise.all([promise1,promise2,...]):监听传递进来的数组中,每一个promise实例的状态,当所有实状态是成功的时候,整体返回实例状态也是成功,值是一个数组,包含所有实例的结果;如果其中有一个实例是失的,整体返回的实例就是失败的,值是失败的那个promise的值!!
- Promise.race([p1, p2, ...])
- ...
一. new Promise((resolve,reject)=>{...})
let str = 10;
// p是 <promise> 对象 可以进行链式调用,链式调用的返回值是 this (这里指向p,所以可以一直使用 .then( ) 的方法)
let p = new Promise(function (resolve, reject) {
setTimeout(function () {
str = 20;
resolve(str)
}, 100);
});
p.then((str) => { //这里拿到的 str 是 new 里面的 resolve(str) 里的 str 相当于这里的函数,在上面被调用,并传实参 str
console.log(str) //20
return 5; // 虽然写了 return,但返回的依然是一个新的 promise 对象,而这里的 return 是给下一个.then 传参。
}).then((a) => {
console.log(a) //5
});
p.then((xxx) => { //这里拿到的 xxx 是 new 里面的 resolve(str) 里的 str 相当于这里的函数,在上面被调用,并传实参 xxx
console.log(xxx) //20
})
// 先打印 11行的 20, 再输出 18行的 20, 最后输出 14行的 5, 因为 11行 和 18行, 都是最先被执行的 promise, 而14行是一个新的 promise,属于新的微任务,进入异步队列
二. 哪里可以拿到 reject 中的内容
1. reject后的东西,一定会进入then中的第二个回调,如果then中没有写第二个回调,则进入catch,没有 then 也会进入 catch
var p1=new Promise((resolve,reject) => {
console.log('没有resolve')
reject('失败了') // 或者 throw new Error('手动返回错误')
});
/*
p1的状态是成功fulfilled还是失败rejected,决定了本次.then
是执行onfulfilled(第一个回调函数)还是onrejected(第二个回调函数)
*/
p1.then(data =>{
// 因为p1状态是rejected,所有本次.then的onfulfilled(第一个回调函数)不会执行
console.log('data::',data);
}, err=> {
console.log('err::',err); // 本次.then的onrejected(第二个回调函数)被执行并拿到失败的原因
return 'err2';
}).then(data =>{
/*
1.上一个.then被执行的那个回调函数不管return了什么,都会返回一个新的Promise,而这个新的Promise
的状态,又决定了本次.then是执行onfulfilled(第一个回调函数)还是onrejected(第二个回调函数)
2.如果上一个.then被执行的那个回调函数没有return一个失败状态的Promise或者代码报错,
就执行本次.then的第一个回调函数,值是return的内容。
*/
console.log('data2::',data);
throw new Error('手动返回错误');
// return Promise.reject("给catch的值");
}, err=> {
//本次的onrejected不会执行,因为上一个.then被执行的那个回调函数返回的新Promise状态是fulfilled(成功状态)
console.log('err2::',err)
}).catch(
/*
如果.then的onfulfilled或onrejected方法执行了,并且在被执行的方法中return一个失败状态的Promise或者代码报错,
那么.catch就会执行其onrejected,值是'手动返回错误'或"给catch的值"。
*/
res => {
console.log('catch data::', res)
});
/*
————————运行结果————————
没有resolve
err:: 失败了
data2:: err2
catch data:: Error: 手动返回错误
*/
.then中没有第二个回调的情况
var p1=new Promise((resolve,reject) => {
console.log('没有resolve')
// throw new Error('手动返回错误')
reject('失败了')
})
p1.then(data =>{ // 不会被执行
console.log('data::',data);
}).then(value => { // 不会被执行
console.log('请求成功:', value);
throw 'xxx';
}).catch(res => { // 失败状态顺延到.catch的onrejected函数,并最终执行
console.log('catch data::', res)
})
/*
————————运行结果————————
没有resolve
catch data:: 失败了
同理:resolve的东西,一定会进入then的第一个回调,肯定不会进入catch
*/
p.then(null, onrejected) 等价于 p.catch(onrejected)
2. 用 try{}catch(error){}
**注意写法,需要使用 async await **
通常 try catch 是无法捕捉异步报错的,所以用它直接包裹 promise 是无法捕捉到错误信息的。
let fn = async () => {
try {
let p = await new Promise((res, rej) => {
rej('失败了')
})
} catch (err) {
console.log("try catch 捕获到 promise 的失败信息:", err);
};
};
fn();
三. Promise.all ( [ p1 , p2 , ...... ] )
Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。(all会将传入的数组中的所有promise全部决议以后,将决议值以数组的形式传入到观察回调中,任何一个promise决议为拒绝,那么就会调用拒绝回调。)
let p1 = new Promise((resolve, reject) => {
resolve('成功了')
});
let p2 = new Promise((resolve, reject) => {
resolve('success')
});
let p3 = new Promise((resolve, reject) => {
reject('失败')
});
Promise.all([p1, p2]).then((result) => {
console.log(result); // ['成功了', 'success']
}).catch((error) => {
console.log(error);
});
Promise.all([p1,p3,p2]).then((result) => {
console.log(result);
}).catch((error) => {
console.log(error); // 失败了,打出 '失败'
});
Promse.all在处理多个异步处理时非常有用,比如说一个页面上需要等两个或多个ajax的数据回来以后才正常显示,在此之前只显示loading图标。
代码模拟:
let wake = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`${time / 1000}秒后醒来`)
}, time)
})
}
let p1 = wake(3000)
let p2 = wake(2000)
Promise.all([p1, p2]).then((result) => {
console.log(result) // [ '3秒后醒来', '2秒后醒来' ]
}).catch((error) => {
console.log(error)
})
需要特别注意的是,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,即p1的结果在前,即便p1的结果获取的比p2要晚。
这带来了一个绝大的好处:在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用Promise.all毫无疑问可以解决这个问题。
四. Promise.race的使用
顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
},1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('failed')
}, 500)
})
Promise.race([p1, p2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 打开的是 'failed'
})
五. Promise是如何捕获异常的?与传统的try/catch相比有什么优势?
传统的 try / catch 捕获异常方式是无法捕获异步的异常的。
而对于 Promise 对象来说,构造 Promise 实例时的代码如果出错,则会被认为是一个拒绝的决议,并会向观察回调中传递异常信息。所以即使是一个异步的请求,Promise 也是可以捕获异常的。此外,Promise 还可以通过 catch 回调来捕获回调中的异常。
六.如果向Promise.all()和Promise.race()传递空数组,运行结果会有什么不同?
all会立即决议,决议结果是fullfilled,值是undefined
race会永远都不决议,程序卡住……
七. 扩展一个 Promise.finally()
扩展一个 Promise.finally(),finally方法用于指定不管Promise对象最后状态如何,都会执行的操作.
它与done方法的最大区别在于,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。
//添加finally方法
Promise.prototype.finally=function (callback) {
var p=this.constructor;
return this.then(//只要是promise对象就可以调用then方法
value => p.resolve(callback()).then(() => value),
reason => p.resolve(callback()).then(() => {throw reason})
);
}
对finally方法的理解:
- p.resolve(callback())这句函数callback已经执行
- finally方法return的是一个promise对象,所以还可以继续链式调用其他方法
- 对于Promise.resolve方法:
Promise.resolve('foo')
等价于new Promise(resolve => resolve('foo'));
所以可以通过then方法的回调函数 接受 实例对象返回的参数。比如:Promise.resolve(function(){console.log(2);}).then(function(cb){cb()})
p.resolve(callback()).then(() => value)
调用then的目的是给promise实例即this添加成功和失败的回调函数
八. 扩展一个 Promise.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都会捕捉到任何可能出现的错误,并向全局抛出。
基于 Promise 之后又有 Generator ,和它门的语法糖:async
Generator
Generator 函数是协程在ES6的实现,最大的特点就是可以交出函数的执行权(即暂停执行),交给 yield 后面的 语句,等此语句执行完,再执行下面的代码。
Generator 有两个区分于普通函数的部分:
- 一是在 function 后面,函数名之前有个 * ;
- 函数内部有 yield 表达式。
协程:
第一步:协程 A 开始执行。
第二步:协程 A 执行到一半,进入暂停,执行权转移到协程 B。
第三步:(一段时间后)协程 B 交还执行权
第四步:协程 A 恢复执行。
也就是说 Promise 和 Generator 一个明显不同的特点是,前者只在Promise体内进行异步代码的同步执行,(不会阻断主任务)
而后者是阻断(主任务)的执行,等到异步的代码执行完毕之后再执行被阻断的(主任务)代码。
例子一:
function* func(){
console.log("one");
yield '1';
console.log("two");
yield '2';
console.log("three");
return '3';
};
let f = func();
f.next();
// one
// {value: "1", done: false}
f.next();
// two
// {value: "2", done: false}
f.next();
// three
// {value: "3", done: true}
f.next();
// {value: undefined, done: true}
例子二:
一般情况下,next 方法不传入参数的时候,yield 表达式的返回值是 undefined 。当 next 传入参数的时候,该参数会作为上一步yield的返回值。
function* sendParameter() {
console.log("start");
var x = yield '2';
console.log("one:" + x);
var y = yield '3';
console.log("two:" + y);
};
// next()不传参
var sendp1 = sendParameter();
sendp1.next();
// start
// {value: "2", done: false}
sendp1.next();
// one:undefined
// {value: "3", done: false}
sendp1.next();
// two:undefined
// {value: undefined, done: true}
// next()传参
var sendp2 = sendParameter();
sendp2.next();
// start
// {value: "2", done: false}
sendp2.next(10);
// one:10
// {value: "3", done: false}
sendp2.next(20);
// two:20
// {value: undefined, done: true}
例子三:练习一下
function* gen(x){
//这个函数体内任何地方console.log(y)都是undefined
var y = yield x + 2 ;
var z = yield x + y ;
};
let y = gen(5);
// y.next() => {value: 7, done: false},value的值是第一个yield后面的 x + 2
y.next(y.next().value) // next中可以传入数据(传入的数据为上一次yield表达式的返回值(就是这里的y)),为了下一步操作的运算。
async(**现在最常用)
async & await:是promise+generator的语法糖,简化promsie的代码编写
async:用来修饰函数的,让函数执行的返回值是一个promsie实例「不常用的」
+ 函数执行报错,返回的实例状态是rejected,值是报错原因
+ 如果没报错,看函数执行是否自己返回了promise实例,如果返回了,则以自己返回的为主
+ 如果没返回,则默认返回一个状态是fulfilled、值是返回值的实例
真实项目中,使用async修饰函数,一般都是因为函数中想要使用await「因为:函数中想使用await,则当前函数必须基于async修饰」
await:等待promise处理的结果;当实例状态是成功,把实例的值赋值给value,同时再执行当前上下文await下面的代码;如果实例的状态是失败的,则下面代码不会去执行!!let value = await promise;
await下面的代码何时执行?
- await 后面如果是微任务(一个promise),它下面的代码就会等待这个微任务执行完,紧跟着执行。
- await 后面如果是主任务(比如一个函数或一个表达式),其下面的代码就会等待这一批主任务执行完,最后执行。
function fn2(){
return new Promise(function(resolve){
setTimeout(function(){
resolve(50);
}, 1000);
})
};
async function fn(){
let value = await fn2();
console.log(value);
};
fn();// 调用后,输出 50 。要 等待 fn2执行完(将异步转成同步的感觉)