前言
手写实现Promise是一道经典的前端面试题。比如在美团,Promise几乎就是必考题,在其它互联网一线大厂也是高频题目。
Promise的实现相对比较复杂,考虑的逻辑也比较多,下面带着大家逐一分析,找到其关键点、以及如何手写一个Promise。
(本文实现遵循 promise/A+规范)
1. Promise 基本结构
我们知道实例化Promise对象时传入一个函数作为执行器,有两个参数(resolve和reject)分别将结果变为成功态和失败态。
据此我们可以写出基本结构:
function Promise(excutor){ let self = this; self.status = 'pending'; // 状态 等待 => 成功或失败 self.value = null; // 成功结果 self.reason = null; // 失败原因 function resolve(value){ }; function reject(reason){ }; }
2. then方法
每一个Promise实例都有一个then方法,它用来处理异步返回的结果,它是定义在原型上的方法。
// 一个成功 一个失败 Promise.prototype.then = function(onFulfilled, onRejected){ }
当我们自己实例化一个Promise时,其执行器函数(executor)会立即执行,这是一定的:
let demo = new Promise((resolve, reject)=>{ console.log("在座的各位彭于晏,大家好"); })
因此,当实例化Promise时,构造函数中就要马上调用传入的excutor函数执行,为了防止出错,加入try,catch:
// 此段代码放在Promise方法里 try { excutor(resolve, reject); } catch(err) { reject(err) }
3. 状态管理
已经是成功态或是失败态不可再更新状态,Promise 规范中规定:当Promise对象已经由pending状态改变为了成功态(resolved)或是失败态(rejected)就不能再次更改状态。
因此我们在更新状态时要判断,如果当前状态是pending(等待态)才可更新。
function resolve(value){ if (self.status === 'pending') { self.value = value; //保存成功结果 self.status = 'fulfilled'; } } function reject(reason){ if (self.status === 'pending') { self.reason = reason; self.status = 'rejected'; } }
以上可以看到:
1. 在resolve和reject函数中分别加入了判断
2. 只有当前状态是pending才可进行操作
3. 将成功的结果和失败的原因都保存到对应的属性上。之后将state属性置为更新后的状态
4. then方法完善
当Promise的状态发生了改变,不论是成功或是失败都会调用then方法。所以,then方法的实现也很简单,根据state状态来调用不同的回调函数即可。
Promise.prototype.then = function(onFulfilled, onRejected){ onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (data) { resolve(data) } onRejected = typeof onRejected === 'function' ? onRejected : function (err) { throw err } }
代码写到这里似乎基本功能都实现了,可是还有一个很大的问题:目前此Promise还不支持异步代码,如果Promise中封装的是异步操作,then方法无能为力,例:
let demo = new Promise((resolve, reject)=>{ console.log("在座的各位彭于晏大家好"); setTimeout(()=>{ resolve(1); }, 500) }) // 没有打印 demo.then(data => console.log(data));
第一步是正常打印的,但是 setTimeout 里面的代码不会执行,最后一行也不会打印 “1”。
问题所在:原因是 setTimeout 函数使得 resolve 是异步执行的,有延迟,当调用then方法的时候,此时此刻的状态还是等待态(pending),因此then方法即没有调用 onFulfilled 也没有调用 onRejected。
5. 发布订阅模式登场
这个问题如何解决?我们可以参照发布订阅模式,在执行 then 方法时如果还在等待态(pending),就把回调函数临时寄存到一个数组里,当状态发生改变时依次从数组中取出执行就好,是不是很Nice!
清楚这个思路我们开始实现它,首先在类上新增两个Array类型的数组,用于存放回调函数:
function Promise(excutor){ // 其它代码略 self.onFulfilledCallbacks = []; self.onRejectedCallbacks = []; }
在then方法里添加进去:
Promise.prototype.then = function(onFulfilled, onRejected){ // 其它代码略 let self = this; if(self.status === "pending"){ self.onFulfilledCallbacks.push(onFulfilled); self.onRejectedCallbacks.push(onRejected); } }
在resolve() 与reject() 里逐一遍历:
function resolve(value){ if (self.status === 'pending') { // 其它代码略 self.onFulfilledCallbacks.forEach(item => item(value)); } } function reject(reason){ if (self.status === 'pending') { // 其它代码略 self.onRejectedCallbacks.forEach(item => item(reason)); } }
到这一步,异步也搞定了!再是如何实现链式调用。
6. 经典的链式调用实现
then方法接收的两个函数中,可以通过return把值传给下一个步,也可以返回一个新的Promise把值传给下一步,then方法执行的时候有个特点:
为了保证链式调用,上一次then中不管你是成功还是失败都会把参数作为下一个then中成功时回调的参数
话不多说,开干(只需要再new一个Promise即可):
Promise.prototype.then = function(onFulfilled, onRejected){ onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (data) { resolve(data) } onRejected = typeof onRejected === 'function' ? onRejected : function (err) { throw err } let self = this; if(self.status === "fulfilled"){ return new Promise((resolve, reject) => { try{ let x = onFulfilled(self.value); x instanceof Promise ? x.then(resolve, reject) : resolve(x); } catch (err){ reject(err) } }) } if(self.status === "rejected"){ return new Promise((resolve, reject) => { try{ let x = onRejected(self.reason); x instanceof Promise ? x.then(resolve, reject) : resolve(x); } catch (err){ reject(err) } }) } if(self.status === "pending"){ return new Promise((resolve, reject) => { self.onFulfilledCallbacks.push(()=>{ let x = onFulfilled(self.value); x instanceof Promise ? x.then(resolve, reject) : resolve(x); }) self.onRejectedCallbacks.push(()=>{ let x = onRejected(self.reason); x instanceof Promise ? x.then(resolve, reject) : resolve(x); }) }) } } }
7. 最后的catch方法
其实catch方法就是then方法的简写
Promise.prototype.catch = function(fn){ return this.then(null, fn); }
现在,一个完整的Promise就实现了,大家可以去试试。有啥不对的可以在评论区指出,感谢。
考虑到一些新手基础可能差点,本篇文章可以说讲解的非常非常详细了,拆分了多个小步骤,一步一步来。相信大家跟着思路走都是能搞定的。排版啥的也都尽量搞得好看些,提高各位客官的用户体验。