JavaScript Promise
YinJie… 人气:21. 初始结构
我们先来回顾下 js 里 promise 实例是如何创建的:
let promsie = new Promise((resolve, reject) => { resolve('完成了') })
那这样我们就知道了,在我们创建的 promise 类中的构造函数要传入的是一个函数,而且我们在传入参数的时候,这个函数参数会被自动执行,接下来这个函数参数也有自己的参数,也就是 resolve 和 reject 这两个参数,那我们也得让我们手写的 myPromise 支持这两个参数:
class myPromise { constructor(func) { func(resolve,reject) } }
这么写显然是有问题的,因为我们不知道在哪里调用 resolve 和 reject 这两个参数,因为我们还没有定义这两个对象,这两个对象还是函数,因此下面就要用类方法的形式创建这两个函数:
class myPromise { constructor(func) { func(resolve,reject) } resolve() {} reject() {} }
这样是对的了么?
错啦,因为我们得用 this 才能调用自身的方法,所以要在两个参数前加上 this:
class myPromise { constructor(func) { func(this.resolve,this.reject) } resolve() {} reject() {} }
2. 定义状态
promise 里有三种状态,分别是 pending,fulfilled 和 rejected,要注意状态一经改变就不能再改变了且只能由 pending 转化为 fulfilled 或者 pending 转化为 rejected。所以我们可以提前把这些状态定义好,可以用 const 来创建外部的固定变量,然后将 pending 设置为默认状态,这样在每一个实例被创建以后就会有自身的状态属性可以进行判断和变动了:
class myPromise { static PENDING = '待定' static FULFILLED = '成功' static REJECTED = '拒绝' constructor(func) { this.status = myPromise.PENDING func(this.resolve,this.reject) } resolve() { if (this.status === myPromise.PENDING) { this.status = myPromise.FULFILLED } } reject() { if (this.status === myPromise.PENDING) { this.status = myPromise.REJECTED } } }
当执行 reolve 的时候,如果现在的状态是待定,就让他变为成功, reject 同理。
并且,在执行 resolve 或者 reject 的时候都是可以传入一个参数的,这样后面就能使用这个参数了:
class myPromise { static PENDING = '待定' static FULFILLED = '成功' static REJECTED = '拒绝' constructor(func) { this.status = myPromise.PENDING this.result = null func(this.resolve,this.reject) } resolve(result) { if (this.status === myPromise.PENDING) { this.status = myPromise.FULFILLED this.result = result } } reject(result) { if (this.status === myPromise.PENDING) { this.status = myPromise.REJECTED this.result = result } } }
那现在我们 new 一个 myPromise 的实例,大家觉得会不会成功呢?
3. this指向
承接上文,我们 new 了一个实例对象:
let promise = new myPromise((resolve,reject) => { resolve('小杰学前端') })
下面是输出结果:
控制台不出意外的报错了,其实问题就出在这里:
我们在 resolve 中拿不到 status ,也就是 this 已经跟丢了,这是为什么呢?
因为我们在 new 一个新实例的时候,执行的是 constructor 里的内容,也就是 constructoe 里的 this 指向的是实例对象,但是现在我们是在实例对象被创建后再在外部环境下执行 resolve 方法的,这里的 resolve 看着像是和实例一起执行的,其实不然。解决 class 的 this 指向问题一般会用箭头函数, bind 或者 proxy,这里我们就用 bind 来绑定
class myPromise { static PENDING = '待定' static FULFILLED = '成功' static REJECTED = '拒绝' constructor(func) { this.status = myPromise.PENDING this.result = null func(this.resolve.bind(this),this.reject.bind(this)) } resolve(result) { if (this.status === myPromise.PENDING) { this.status = myPromise.FULFILLED this.result = result } } reject(result) { if (this.status === myPromise.PENDING) { this.status = myPromise.REJECTED this.result = result } } }
这里就是给实例的 resolve 和 reject 方法绑定这个 this 为当前的实例对象
或者我们也可以这样解决:
class myPromise { static PENDING = '待定' static FULFILLED = '成功' static REJECTED = '拒绝' constructor(func) { this.status = myPromise.PENDING this.result = null let resolve = (result) => { if (this.status === myPromise.PENDING) { this.status = myPromise.FULFILLED this.result = result } } let reject = (result) => { if (this.status === myPromise.PENDING) { this.status = myPromise.REJECTED this.result = result } } func(resolve, reject) } }
4. then 方法
我们先回顾一下原生的 then 是怎么写的:
let promise = new Promise((resolve, reject) => { resolve('完成了') reject('失败了') }) promise.then( res => console.log(res), result => console.log(result))
那我们就开始写我们的 then ,首先知道的是要传递两个参数,他们都是函数类型,一个表示状态成功时的回调,一个表示状态失败时的回调:
class myPromise { static PENDING = '待定' static FULFILLED = '成功' static REJECTED = '拒绝' constructor(func) { this.status = myPromise.PENDING this.result = null func(this.resolve.bind(this),this.reject.bind(this)) } resolve(result) { if (this.status === myPromise.PENDING) { this.status = myPromise.FULFILLED this.result = result } } reject(result) { if (this.status === myPromise.PENDING) { this.status = myPromise.REJECTED this.result = result } } then(onFulfilled, onRejected) { if (this.status === myPromise.FULFILLED) { onFulfilled(this.result) } if (this.status === myPromise.REJECTED) { onRejected(this.result) } } }
这里 then 写的也是浅显易懂,我们测试一下能否正常输出:
let promise = new myPromise((resolve,reject) => { resolve('成功了') reject('失败了') } promise.then(res => console.log(res), result => console.log(result)) // 成功了
5. 执行异常
如果我们在 new Promise 的时候,执行函数里面我们抛出错误,是会触发拒绝方法,可以把错误的信息作为内容输出出来:
let promise = new Promise((resolve, reject) => { throw new Error('抛出错误') }) promise.then( res => console.log(res), result => console.log(result)) //Error: 抛出错误
但是如果我们在 myPromise 这里写上同样的代码则会报错,于是我们就可以在执行 resolve 和 reject 之前进行判断,可以用 try 和 catch 来该写代码,如果没有报错就正常执行,有报错就调用 reject 方法:
constructor(func) { this.status = myPromise.PENDING this.result = null try { func(this.resolve.bind(this),this.reject.bind(this)) } catch (error) { this.reject(error) } }
注意,在 catch 这里不需要用 bind ,因为这是立即调用的,只在内部执行的。
那到了这里我们的异常处理是不是和原生的差不多了呢,其实还没有,我们知道在 then 里传递的是两个函数参数,那如果我们故意在原生代码里不传入函数作为参数呢:
let promise = new Promise((resolve, reject) => { resolve('成功了') }) promise.then( undefined, result => console.log(result))
实际上,这里什么也没有输出,但是如果我们以同样的方法应用在 myPromise 里会发生什么呢?
答案是会报一堆错误,这不是我们想要的,那我们只需要在执行 then 里的参数前进行类型判断,如果它是函数就把原函数参数赋给它,否则就赋给它空函数:
then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => {} onRejected = typeof onRejected === 'function' ? onRejected : () => {} if (this.status === myPromise.FULFILLED) { onFulfilled(this.result) } if (this.status === myPromise.REJECTED) { onRejected(this.result) } }
现在再运行就不会报错了。
6. 支持异步(重头戏1)
现在我们的手写代码里面还没有植入异步功能,首先我们先了解一下原生 Promise 的一些运行顺序规则:
console.log(111) let promise = new Promise((resolve, reject) => { console.log(222) resolve('成功了') }) console.log(333) promise.then( res => console.log(res), result => console.log(result))
熟悉 JS 执行机制和 prmoise 的同学一定能够正确的判断输出的顺序:
我们再看一下手写的 myPromise 是如何输出的:
console.log(111) let promise = new myPromise((resolve,reject) => { console.log(222) resolve('成功了') }) promise.then(res => console.log(res), result => console.log(result)) // 成功了 console.log(333)
执行代码,查看输出:
那我们已经找到问题的所在了,那就是 resolve 和 reject 里面都没有设置异步执行,二话不说直接套上定时器:
then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => {} onRejected = typeof onRejected === 'function' ? onRejected : () => {} if (this.status === myPromise.FULFILLED) { setTimeout(() => { onFulfilled(this.result) }, 0); } if (this.status === myPromise.REJECTED) { setTimeout(() => { onRejected(this.result) }, 0); } }
现在我们的 then 里面就是异步执行的了,再次运行代码查看输出:
可以看到我们成功输出了,但是现在异步的问题真的解决了吗,现在又进入另一个非常!非常!重要的重点了,我们先看一下原生 Promise 这段代码:
console.log(111) let promise = new Promise((resolve, reject) => { console.log(222) setTimeout(() => { resolve('成功了') console.log(444) }, 0); }) console.log(333) promise.then( res => console.log(res), result => console.log(result))
这段代码的输出如下:
在 setTimeout 中会先执行输出444,再执行 resolve ,我们再看一下同样的代码放在 myPromise 中的输出结果:
并没有输出 “ 成功了 ”,这是为什么呢?
没有输出很可能的原因是 then 方法没有被执行,我们再各个位置输出一下当前的状态,进行判断:
console.log(111) let promise = new myPromise((resolve,reject) => { console.log(222) setTimeout(() => { console.log(myPromise.status) resolve('成功了') console.log(myPromise.status) console.log(444) }, 0); }) promise.then(res => { console.log(res); console.log(myPromise.status) }, result => console.log(result)) // 成功了 console.log(333)
看一下控制台输出:
可以看到在 setTimeout 里面的两个状态都被成功输出了只有then 里的状态并没有成功输出出来,那基本就可以判断是 then 里的判断出了问题,我们先分析一下,在执行完 111,222,333后就开始处理异步,这里肯定是因为我们先执行了 then ,然后在 then 里会判断当前状态是成功就执行成功的回调,是失败就执行失败的回调,但是如果当前状态是 pending 呢?这不就出问题了吗,在本题中 setTimeout 是宏任务,要晚于微任务 then 执行,所以 then 执行的时候还没有改变状态,自然就不会输出,那我们就在 then 里添加待定状态的条件就可以了。
7. 回调保存(重头戏2)
因为在函数体中执行异步操作时,then 会先执行,所以我们要在 then 里状态为 pending ,也就是碰到异步操作的时候让 then 里的函数稍后再执行,等到状态改变了再去执行,那我们就定义两个数组来保存回调
constructor(func) { this.status = myPromise.PENDING this.result = null this.resolveCallbacks = [] this.rejectCallbacks = [] try { func(this.resolve.bind(this),this.reject.bind(this)) } catch (error) { this.reject(error) } }
然后在 then 里状态为 pending 时将函数保存:
then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => {} onRejected = typeof onRejected === 'function' ? onRejected : () => {} if (this.status === myPromise.PENDING) { this.resolveCallbacks.push(() => { onFulfilled(this.result) }) this.rejectCallbacks.push(() => { onRejected(this.result) }) } if (this.status === myPromise.FULFILLED) { setTimeout(() => { onFulfilled(this.result) }, 0); } if (this.status === myPromise.REJECTED) { setTimeout(() => { onRejected(this.result) }, 0); } }
然后在执行 resolve 和 reject 的时候,看看回调数组里有没有待执行的函数,有的话就执行:
resolve(result) { if (this.status === myPromise.PENDING) { this.status = myPromise.FULFILLED this.result = result this.resolveCallbacks.forEach(callback => { callback(result) }) } } reject(result) { if (this.status === myPromise.PENDING) { this.status = myPromise.REJECTED this.result = result this.rejectCallbacks.forEach(callback => { callback(result) }) } }
现在我们再运行一下原来的测试代码:
console.log(111) let promise = new myPromise((resolve,reject) => { console.log(222) setTimeout(() => { resolve('成功了') console.log(444) }, 0); }) promise.then(res => { console.log(res) }, result => console.log(result)) console.log(333)
现在输出的是:
这个输出结果也不是我们预期的呀,我们虽然成功输出了执行成功的回调,但是应该是在444后面输出才对,其实这里还有个小坑,就是 resolve 和 reject 也得让他俩变成异步执行:
resolve(result) { setTimeout(() => { if (this.status === myPromise.PENDING) { this.status = myPromise.FULFILLED this.result = result this.resolveCallbacks.forEach(callback => { callback(result) }) } }, 0); } reject(result) { setTimeout(() => { if (this.status === myPromise.PENDING) { this.status = myPromise.REJECTED this.result = result this.rejectCallbacks.forEach(callback => { callback(result) }) } }, 0); }
执行结果:
因为我们知道在 promise 的函数体内是同步执行的,promise.then 是微任务,会异步执行,所以在函数体内碰到 resolve 时虽然状态改变了但是并不会立刻输出 result ,但是因为现在我们的函数体内有异步代码,此时我们执行 then 只是把他的回调保存起来了,等到执行回调的时候还得恢复原来的逻辑,也就是等函数体内同步代码输出完了再执行 then 输出 resolve 里传递的值,所以要给 resolve 和 reject 加 setTimeout 让他变成异步,不这样的话在执行 resolve 状态改变了就会立刻输出结果,这显然不是我们期待的。
8. 重难点解读
如果你之前还是云里雾里没有懂的话,我们再次来解读一下这段代码:
console.log(111) let promise = new myPromise((resolve,reject) => { console.log(222) setTimeout(() => { resolve('成功了') console.log(444) }, 0); }) promise.then(res => { console.log(res) }, result => console.log(result)) console.log(333)
- 第一步,执行 console.log(111),输出 ”111“
- 第二步,创建 promise,同步执行函数体内的代码,执行 console.log(222),输出 “222”
- 第三步,遇到 setTimeout,这是宏任务,将他放到宏任务队列,等待执行
- 第四步,遇到 promise.then ,这是微任务,将它放到微任务队列,等待执行
- 第五步,执行 console.log(333) ,输出 “333”
- 第六步,当前执行栈已经清空,先执行微任务队列的任务 promise.then ,发现当前状态还是待定,所以没有输出,没有输出的原因是改变状态的 resolve 函数在 setTimeout 里,而我们目前还没有执行,然后将 then 的回调保存起来。
- 第七步,微任务队列已经清空,开始执行宏任务 setTimeout,因为 resolve 也是异步执行的,所以放到异步队列中,先执行同步任务 console.log(444),输出 “444”
- 第八步,现在没有要执行的同步任务了,再执行 resolve ,改变当前状态为成功,然后发现回调数组里有待执行的函数,执行回调函数,输出结果 “成功了”
9. 链式功能(重头戏3)
我们先了解一下 then 方法的特点,then方法中的成功回调或者失败回调,如果返回的是一个promise ,那么这个 promise 成功或者失败的值就会传递给下一个 then 中。如果返回的是一个普通的不是 promise 的值,那就把这个值传给下一个 then 。如果执行时出现错误那就会执行下一个 then 中的失败的回调。
注意是参数函数的返回值,而不是then
本身的返回值,then
本身肯定是返回promise
的。
那么首先我们现在要做的就是拿到上一个回调中返回的值:
then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => {} onRejected = typeof onRejected === 'function' ? onRejected : () => {} let newPromise = new myPromise((resolve, reject) => { if (this.status === myPromise.PENDING) { this.resolveCallbacks.push(() => { let res = onFulfilled() }) this.rejectCallbacks.push(() => { let res = onRejected() }) } if (this.status === myPromise.FULFILLED) { setTimeout(() => { let res = onFulfilled(this.result) }, 0); } if (this.status === myPromise.REJECTED) { setTimeout(() => { let res = onRejected(this.result) }, 0); } }) return newPromise }
then
能执行的前提是,对应的promise
执行了resolve
,这样能拿到resolve
的值。
所以后面的then
能拿到的前提是:前面的then
(返回值是promise
) 将参数函数返回值resolve
了。这里面略绕,得好好想想。所以我们现在需要执行 resolve 并且把上一个回调传进来的值作为参数:
then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => {} onRejected = typeof onRejected === 'function' ? onRejected : () => {} let newPromise = new myPromise((resolve, reject) => { if (this.status === myPromise.PENDING) { this.resolveCallbacks.push(() => { let res = onFulfilled(this.result) resolve(res) }) this.rejectCallbacks.push(() => { let res = onRejected(this.result) resolve(res) }) } if (this.status === myPromise.FULFILLED) { setTimeout(() => { let res = onFulfilled(this.result) resolve(res) }, 0); } if (this.status === myPromise.REJECTED) { setTimeout(() => { let res = onRejected(this.result) resolve(res) }, 0); } }) return newPromise }
现在我们的工作完成了么?还没有,准确的说核心逻辑完成了一半,我们还没有处理返回值是 promise 对象的情况,下面我们直接把这块的逻辑抽象成一个函数:
then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => {} onRejected = typeof onRejected === 'function' ? onRejected : () => {} let newPromise = new myPromise((resolve, reject) => { if (this.status === myPromise.PENDING) { this.resolveCallbacks.push(() => { let res = onFulfilled(this.result) this.resolvePromise(res,resolve,reject) }) this.rejectCallbacks.push(() => { let res = onRejected(this.result) this.resolvePromise(res,resolve,reject) }) } if (this.status === myPromise.FULFILLED) { setTimeout(() => { let res = onFulfilled(this.result) this.resolvePromise(res,resolve,reject) }, 0); } if (this.status === myPromise.REJECTED) { setTimeout(() => { let res = onRejected(this.result) this.resolvePromise(res,resolve,reject) }, 0); } }) return newPromise } resolvePromise(res, resolve, reject) { if(res instanceof myPromise) { res.then(resolve, reject) } else{ // 普通值 resolve(res) } }
那么现在我们通过实例看一下能否实现链式调用,当返回值是 promise 的时候:
let promise = new myPromise((resolve,reject) => { resolve(11) }) const a = new Promise((resolve,reject) => { resolve('ok') }) promise.then(res => { return a }).then(res => console.log(res))
运行结果:
当返回值是普通值的时候:
let promise = new myPromise((resolve,reject) => { resolve(11) }) promise.then(res => { return res }).then(res => console.log(res)) //11
可以看到现在我们的链式调用已经基本实现了功能,其实这里面还有很多边界情况没有做处理,还实现的很粗糙,但是我们研究源码要做的就是抽丝剥茧,关注最核心的逻辑就行,当然能把边界情况都处理了是最好的。
加载全部内容