前言
在上篇中完成了一个最基本的能处理同步状态的promise,没有看过的话可以点击下面的链接。
这里将通过promise 1.5完成异步处理,promise 2.0完成链式调用和处理then返回值的resolvePromise
最后的总结部分是真正精华所在,是至少看了五遍视频,review了无数次自己的代码之后的我对promise原理的文字描述。
myPromise 1.5 过渡
在进入promise 2.0
之前,我认为可能需要一个1.5作为过渡,专门讲一下异步的处理,其实promise
处理异步就是发布订阅模式。
这部分比较简单,直接上代码,所有相较于1.0有增加的地方都有注释,关键在理解思想。
const RESOLVED = 'RESOLVED'; // 成功
const REJECTED = 'REJECTED'; // 失败
const PENDING = 'PENDING'; // 等待态
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
// 专门用来存放成功的回调
this.onResolvedCallbacks = [];
// 因为promise可能多次.then接收多个回调函数,所以采用栈的形式
this.onRejectedCallbacks = [];
let resolve = (value) => {
if(this.status === PENDING){
this.value = value;
this.status = RESOLVED;
// executor调用resolve时执行栈中保存的回调
this.onResolvedCallbacks.forEach(fn=>fn());
}
}
let reject = (reason) => {
if(this.status === PENDING){
this.reason = reason;
this.status = REJECTED;
this.onRejectedCallbacks.forEach(fn=>fn());
}
}
try{
executor(resolve,reject);
}catch(e){
reject(e);
}
}
then(onFulfilled,onRejected){
if(this.status === RESOLVED){
onFulfilled(this.value);
}
if(this.status === REJECTED ){
onRejected(this.reason)
}
if(this.status === PENDING){//执行到then时promise状态还是pending,说明executor异步
this.onResolvedCallbacks.push(()=>{//异步时,将用户传入的onFulfilled函数保存起来(闭包)
//在回调调用用户传入的onFulfilled前会做一些操作(下一版加入)
onFulfilled(this.value);
});
//这里不能直接this.onResolvedCallbacks.push(onFulfilled())这样onFulfilled会立即执行
//也没有采用this.onResolvedCallbacks.push(onFulfilled)这样虽然不会直接执行但是,不方便扩展
this.onRejectedCallbacks.push(()=>{
onRejected(this.reason);
});
}
}
}
module.exports = Promise
总结一下
异步的情况下,promise
本身(或者说promise内用户传入的executor)是立即执行的,但executor函数的结果是异步返回的。
而then
方法是同步执行,因此then
永远比executor
先执行完,于是then
中传入的方法先保存起来,供executor
回调调用。
换句话说,异步情况下,不确定promise
状态,先执行完then
,储存好then
中的onFulfilled/onRejected
函数,再供给executor,executor根据异步返回的结果,确定状态后调用。
这里注意到,'mypromise1.0'中是先执行完executor,确定promise状态,再执行then,根据状态决定执行哪个用户传入的函数。与1.5中处理异步的情况正好相反。
myPromise 2.0 介绍
myPromise 2.0
版本解决了实现promise
的两大难题:
-
链式调用
-
then三种返回值的处理
和上期一样,下面先举几个例子,看一看node
中的promise
这两个特性。
then
的返回值不管是onFulfilled
还是onRejected
的返回值都会传给下一个.then
的onFulfilled
。也就是链式调用
read('./na1me.txt').then((data) => {// 这里故意写错进入第一层的reject
//第一层resolve中正常的return会被下一层的then的resolve接受
return 'then1 [resolve] return.'
},(err) => {
//第一层reject中正常的return也会被下一层的then的resolve接受(这里要记住,容易搞混)
return 'then1 [reject] return.'
}).then((data) => {
console.log(data,'then2 [resolve] worked.');//结果是这里被调用
},(err) => {err
console.log(err,'then2 [reject] worked.');
})
- then中回调的返回值有三种情况,正常值(情况同上↑,传给下个
then
的onFulfilled
),出错值(传给下个then
的onRejected
),promise
(先执行这个promise
再根据这个promise
的返回值决定调用下个then
的onFulfilled
还是onRejected
)要分别进行处理。实现resolvePromise函数进行处理
2.1 返回出错值
read('./na1me.txt').then((data) => {// 这里故意写错进入reject
throw new Error('then1 [resolve] throw error.')//第一层resolve中的报错会被下一层的then的reject接受
},(err) => {
throw new Error('then1 [reject] throw error.')//第一层reject中的报错也会被下一层的then的reject接受
}).then((data) => {
console.log(data,'\n---then2 [resolve] worked.');
},(err) => {
console.log(err,'\n---then2 [reject] worked.');//结果是这里被调用
})
2.2 返回 promise
read('./name.txt').then((data) => {//name.txt里的内容是age.txt
return read(data)
}, (err) => {})
.then((data) => {//第二层执行哪个由第一层返回的promise执行完的状态判断
console.log(data,'\n---then2 [resolve] worked.'); //这里执行并输出age.txt中的内容
}, (err) => {
console.log(err,'\n---then2 [reject] worked.');
})
- 错误没有被捕获的话,可以在后面任意一层被捕获,被捕获后这个错误将不会再向后传递
read('./na1me.txt').then((data) => {//这里故意写错进入reject,而这层then没有reject
return read(data) //这里写不写都一样,因为并没有走第一层then的resolve
})
.then((data) => {
console.log(data,'\n---then2 [resolve] worked.');
}, (err) => {
console.log(err,'\n---then2 [reject] worked.'); //被这里捕获,这里输出 no file na1me.txt的错
})
.then((data) => {
console.log(data,'\n---then3 [resolve] worked.'); //然后这里被调用,因为错误在第二层被捕获了
}, (err) => {
console.log(err,'\n---then3 [reject] worked.');
// 补充,如果第二层then代码运行出现了错误或throw new error 就会执行这里捕获第二层而不是第一层的错误。如果第二层没有传入reject方法,则第一层的错误会被这里第三层then的reject捕获
})// 如果错误从头到尾都没有被捕获则代码运行报错
- 每次执行完promise.then方法后返回的都是一个新的'promise',这也是为什么可以一直.then
read('./name.txt').then((data) => {
return 111
})// 即使这里返回的是111也会被包装成promise,第二层的then其实是这个新promise的then
.then((data) => {
console.log(data)
})
mypromise 2.0 源码解析
话不多说,开始上代码,话都在注释里。
const RESOLVED = 'RESOLVED';
const REJECTED = 'REJECTED';
const PENDING = 'PENDING';
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = (value) => {
if(this.status === PENDING){
this.value = value;
this.status = RESOLVED;
this.onResolvedCallbacks.forEach(fn=>fn());
}
}
let reject = (reason) => {
if(this.status === PENDING){
this.reason = reason;
this.status = REJECTED;
this.onRejectedCallbacks.forEach(fn=>fn());
}
}
try{
executor(resolve,reject);
}catch(e){
reject(e);
}
}
then(onFulfilled,onRejected){// 为了实现链式调用,把整个then函数也变成返回一个promise,这样返回值就有.then方法了
let promise2 = new Promise((resolve, reject) => {//new promise 传入executor,立即执行
if(this.status ===RESOLVED){
//难点,注意这里为什么要加settimeout
//因为下面的resolvePromise中要传入promise2自己本身
//而在去掉setTimeout的情况下promise2的new操作里的构造函数里的executor都还没执行完成
//所以此时promise2还没有初始化完成,相当于还没有promise2无法传给resolvePromise,会显示promise2 undefined。
//所以这里用setTimeout,这样写会将resolvePromise放在下一个事件循环执行,事件循环的知识这里不讲了,可以认为和异步一样在同步代码执行完之后执行。
//在node自己实现的promise中不是用settimeout的,而是用更底层的方法,但是使用settimeout方法是A+规范里建议方法之一,所以不用担心。
setTimeout(() => {
try {
let x = onFulfilled(this.value); //这里获取到用户resolve回调函数运行的返回值,也就是then函数会获取到resolve中return的值
//补充,这里本来不用担心onFulfilled也就是用户传入的resolve执行抛错,因为这里属于promise2的executor报错会被promise2的try/catch捕获
// 但是由于外层包了settimeout,变成了异步,而try/catch无法捕获异步错误,所以得再包一层try/catch
resolvePromise(promise2,x,resolve,reject) //这里x有三种情况(正常值、错误值、promise),这个函数根据x决定调用resolve还是reject
} catch (e) {// 抛错就直接让promise2失败
reject(e)
}
},0)
}//下面改动的原因和这里一样
if(this.status ===REJECTED ){
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2,x,resolve,reject)
} catch (e) {
reject(e)
}
},0)
}
if(this.status === PENDING){
this.onResolvedCallbacks.push(()=>{
setTimeout(() => {//这里完全可以不用加setTimeout,因为本身就是异步的,不需要settimeout。加了是为了保持代码一致(好看)
try {
let x = onFulfilled(this.value);
resolvePromise(promise2,x,resolve,reject)
} catch (e) {
reject(e)
}
},0)
});
this.onRejectedCallbacks.push(()=>{
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2,x,resolve,reject)
} catch (e) {
reject(e)
}
},0)
});
}
})
}
}
// resolvePromise 所有的promise都要坚持 bluebird q es6-promise
const resolvePromise = (promise2, x, resolve, reject) => {
// 1.循环引用 自己等待自己完成
// let p = new Promise((resolve,reject) => {
// resolve(1)
// })
// let p2 = p.then(data => {
// return p2// 报错,因为这里promise p2的返回值还是p2,而resolvePromise方法内,
// //当onFulfilled/onRejected的返回值X是promise时,会执行返回的这个promise,再根据这个promise的返回值决定p2的状态
// //而这里返回值还是p2,p2的状态又由内部的p2决定,无限循环。所以当用户这样使用promise时会报错
// //所以我们自己写resolvePromise时,要针对返回值还是这个promise实例自己时,要识别并报错。
// })
// p2.then(() => {},() => {}) // 报错TypeError: Chaining cycle detected for promise #<Promise>
if (promise2 === x) { // 自己等另一个自己完成,另一个自己完成的条件又是等另另一个自己 而调用reject抛错
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
// 后续的条件要严格判断 保证代码能和别的库一起使用
let called;
if ((typeof x === 'object' && x != null) || typeof x === 'function') { //判断是不是promise
try {
let then = x.then;
if (typeof then === 'function') { // 判断是不是promise
// ↑ A+规范判断到这里就认为是promise了,别的promise实现库再继续搞事就属于报错的范围
// ↓ 相当于x.then(y=>{},e=>{}),这里用call主要是x.then又会取一次then,怕别的promise库写成第二次取then的时候才会报错
then.call(x, y => { // 根据promise的状态决定是成功还是失败
if (called) return;//这两句是防止别人的promise库可以多次调用resolve/reject
called = true;//这里调用一次就把called标记为true,之后发现标记了就直接return
resolvePromise(promise2, y, resolve, reject); // 递归解析的过程
}, e => {
if (called) return;
called = true;
reject(e);
});
} else { // 1**(不是promise就直接成功)
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e); // 取值出错
}
} else { // 2**(不是promise就直接成功)
resolve(x);
}
//注意两个**(不是promise就直接成功)的地方,这里为什么不把promise判断条件写一起,把else的resolve写一起呢?
//同时注意这里的try/catch并不是捕获then.call执行的报错,then内部本身就try/catch没必要再包一层,那为什么要包裹try/catch?
//因为let then = x.then;这里可能会报错,所以这里包一层try/catch,所以不能和其他promise判断条件写一起
//至于为什么获取then可能会报错,是怕别人的promise库搞事情,所以A+规范里规定了这一条,获取then可能报错,要加try/catch。
//说到这里可能还是不好理解别人的库怎么搞事情,毕竟很少遇到获取参数赋值还报错,这里姜老师姜哥具体提了一条。
//Object.defineProperty(x,'then',{get(){throw new Error()}})
}
module.exports = Promise
mypromise 2.0 源码无注释版
补一个没有注释的版本,方便大家切换着看结构以及逻辑,注释太多了我都觉得乱
所以绝不是为了水字数 :)
但是觉得拆开一块块讲很麻烦,也不方便大家复制运行,索性写一起了。
去掉注释,其实代码也不多,可以看到主要分为三大块executor、then、resolvePromise
const RESOLVED = 'RESOLVED';
const REJECTED = 'REJECTED';
const PENDING = 'PENDING';
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = (value) => {
if(this.status === PENDING){
this.value = value;
this.status = RESOLVED;
this.onResolvedCallbacks.forEach(fn=>fn());
}
}
let reject = (reason) => {
if(this.status === PENDING){
this.reason = reason;
this.status = REJECTED;
this.onRejectedCallbacks.forEach(fn=>fn());
}
}
try{
executor(resolve,reject);
}catch(e){
reject(e);
}
}
then(onFulfilled,onRejected){
let promise2 = new Promise((resolve, reject) => {
if(this.status ===RESOLVED){
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2,x,resolve,reject)
} catch (e) {
reject(e)
}
},0)
}
if(this.status ===REJECTED ){
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2,x,resolve,reject)
} catch (e) {
reject(e)
}
},0)
}
if(this.status === PENDING){
this.onResolvedCallbacks.push(()=>{
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2,x,resolve,reject)
} catch (e) {
reject(e)
}
},0)
});
this.onRejectedCallbacks.push(()=>{
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2,x,resolve,reject)
} catch (e) {
reject(e)
}
},0)
});
}
})
}
}
const resolvePromise = (promise2, x, resolve, reject) => {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
let called;
if ((typeof x === 'object' && x != null) || typeof x === 'function') {
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, e => {
if (called) return;
called = true;
reject(e);
});
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
module.exports = Promise
总结
先上一段代码,后以p1,p2,p3指代三个先后产生的promise
let p1 = new Promise((resolve,reject) => {
resolve('p1')
})
p1
.then((data) => {//产生一个新promise p2
resolve(data + ' + p2')
}, (err) => {
console.log(err);
})
//实际上,下面是第二个promise p2的.then方法,与p1无关
.then((data) => {//又产生一个新promise p3
console.log(data + ' + p3');
}, (err) => {
console.log(err);
})
1.将then函数本身变成了一个promise,并且执行,从而实现了能不停.then的链式调用
实现链式调用其实就是把then函数也写成返回promise,于是then就有了状态,有自己的.then方法,p1调用then产生p2,并将数据传给p2或者说then,可以认为then就是p2
then执行就是p2执行,p2立即执行,就是onFulfilled或onRejected立即执行,执行结果代表p2的状态为成功还是失败,
根据p2的状态才执行了第二个.then方法,实现了链式调用,这就是 同步状态下 promise实现链式调用的原理!!!!
(异步状态差不多,只不过绕了一点弯,回头总结补上)
p1的改变状态很简单易懂,执行器executor中调用了resolve p1就是resolve状态,调用了reject p1就是reject状态,
用户自己会判断并在执行器中写好p1什么情况调用哪个,决定好p1的状态,从而决定第一个.then该调用onFulfilled还是onRejected
而第二个.then该调用onFulfilled还是onRejected,要根据第一个.then 或者说 p2的状态 决定
p2的状态由onFulfilled/onRejected的返回结果x决定,x是什么,这又是第二个难点了。
2.实现resolvePromise
大部分情况下,不管是执行onFulfilled还是onRejected,正常运行都会调用resolve。
但是还有有另外两种情况,一种是用户写的onFulfilled/onRejected代码执行抛错,将调用reject
另一种是nFulfilled/onRejected的返回值是promise,将执行这个promise,根据这个promise的状态决定p2的状态。
上手试试(待完成)
上班的地方没有运行环境,两三天内补全使用测试展示效果
以及检查代码跑一遍官方测试(应该是没有问题的毕竟是老师的代码)
额外
感谢你的耐心阅读,文中可能出现一些没有检查到的错误还望轻喷,后续会不停修改和补充。
再次强调这是一篇笔记及心得小记,有什么错误和问题欢迎留言,一定及时回复并且改正!!
中篇已经几乎写完了promise,下篇写一些race all catch方法。
写完promise篇之后有计划写一篇js引擎执行顺序,会继续我的风格,文字写的非常详细(同时应该不会配图,因为太懒)。但是不再是笔记啦!。
我们建了一个前端菜鸟小群,邀请年龄以及水平差不多的菜鸟加入。
唯一的要求和建立群的初衷是每个星期必须输出一篇文章,周二清人,以此督促群内每个人学习。