方法 手写promise_JS探索-手写Promise

无意间在知乎上刷到Monad这个概念,去了解了一下,前端的Promise就是一种Monad模式,所以试着学习一下手写一个Promise.

本文内容主要参考于

只会用?一起来手写一个合乎规范的Promise​www.jianshu.com
b2cc9056b372c3f86512b8c6a44055ba.png

Promise是什么

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise是处理异步编码的一个解决方案,在Promise出现以前,异步代码的编写都是通过回调函数来处理的,回调函数本身没有任何问题,只是当多次异步回调有逻辑关系时就会变得复杂:

const fs = require('fs');
fs.readFile('1.txt', (err,data) => {
    fs.readFile('2.txt', (err,data) => {
        fs.readFile('3.txt', (err,data) => {
            //可能还有后续代码
        });
    });
});

上面读取了3个文件,它们是层层递进的关系,可以看到多个异步代码套在一起不是纵向发展的,而是横向,不论是从语法上还是从排错上都不好,于是Promise的出现可以解决这一痛点。
上述代码如果改写成Promise版是这样:

const util = require('util');
const fs = require('fs');
const readFile = util.promisify(fs.readFile);

readFile('1.txt')
    .then(data => {
        return readFile('2.txt');
    }).then(data => {
        return readFile('3.txt');
    }).then(data => {
        //...
    });

可以看到,代码是从上至下纵向发展了,更加符合人们的逻辑。

下面手写一个Promise,按照Promises/A+规范,可以参照规范原文:

Promises/A+​promisesaplus.com

手写实现Promise是一道前端经典的面试题,比如美团的面试就是必考题,Promise的逻辑还是比较复杂的,考虑的逻辑也比较多,下面总结手写Promise的关键点,和怎样使用代码来实现它。

Promise代码基本结构

实例化Promise对象时传入一个函数作为执行器,有两个参数(resolve和reject)分别将结果变为成功态和失败态。我们可以写出基本结构

function Promise(executor) {
    this.state = 'pending'; //状态
    this.value = undefined; //成功结果
    this.reason = undefined; //失败原因

    function resolve(value) {
        
    }

    function reject(reason) {

    }
}

module.exports = Promise;

其中state属性保存了Promise对象的状态,规范中指明,一个Promise对象只有三种状态:等待态(pending)成功态(resolved)和失败态(rejected)
当一个Promise对象执行成功了要有一个结果,它使用value属性保存;也有可能由于某种原因失败了,这个失败原因放在reason属性中保存。

then方法定义在原型上

每一个Promise实例都有一个then方法,它用来处理异步返回的结果,它是定义在原型上的方法,我们先写一个空方法做好准备:

Promise.prototype.then = function (onFulfilled, onRejected) {
};

当实例化Promise时会立即执行

当我们自己实例化一个Promise时,其执行器函数(executor)会立即执行,这是一定的:

let p = new Promise((resolve, reject) => {
    console.log('执行了');
});
//运行结果:执行了

因此,当实例化Promise时,构造函数中就要马上调用传入的executor函数执行

function Promise(executor) {
    var _this = this;
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;

    executor(resolve, reject); //马上执行
    
    function resolve(value) {}
    function reject(reason) {}
}

已经是成功态或是失败态不可再更新状态

规范中规定,当Promise对象已经由pending状态改变为了成功态(resolved)或是失败态(rejected)就不能再次更改状态了。因此我们在更新状态时要判断,如果当前状态是pending(等待态)才可更新:

function resolve(value) {
        //当状态为pending时再做更新
        if (_this.state === 'pending') {
            _this.value = value;//保存成功结果
            _this.state = 'resolved';
        }

    }

function reject(reason) {
    //当状态为pending时再做更新
        if (_this.state === 'pending') {
            _this.reason = reason;//保存失败原因
            _this.state = 'rejected';
        }
    }

以上可以看到,在resolve和reject函数中分别加入了判断,只有当前状态是pending才可进行操作,同时将成功的结果和失败的原因都保存到对应的属性上。之后将state属性置为更新后的状态。

then方法的基本实现

当Promise的状态发生了改变,不论是成功或是失败都会调用then方法,所以,then方法的实现也很简单,根据state状态来调用不同的回调函数即可:

Promise.prototype.then = function (onFulfilled, onRejected) {
    if (this.state === 'resolved') {
        //判断参数类型,是函数执行之
        if (typeof onFulfilled === 'function') {
            onFulfilled(this.value);
        }

    }
    if (this.state === 'rejected') {
        if (typeof onRejected === 'function') {
            onRejected(this.reason);
        }
    }
};

需要一点注意,规范中说明了,onFulfilled 和 onRejected 都是可选参数,也就是说可以传也可以不传。传入的回调函数也不是一个函数类型,那怎么办?规范中说忽略它就好了。因此需要判断一下回调函数的类型,如果明确是个函数再执行它。

让Promise支持异步

代码写到这里似乎基本功能都实现了,可是还有一个很大的问题,目前此Promise还不支持异步代码,如果Promise中封装的是异步操作,then方法无能为力:

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 500);
});

p.then(data => console.log(data)); //没有任何结果

运行以上代码发现没有任何结果,本意是等500毫秒后执行then方法,哪里有问题呢?原因是setTimeout函数使得resolve是异步执行的,有延迟,当调用then方法的时候,此时此刻的状态还是等待态(pending),因此then方法即没有调用onFulfilled也没有调用onRejected。
这个问题如何解决?我们可以参照发布订阅模式,在执行then方法时如果还在等待态(pending),就把回调函数临时寄存到一个数组里,当状态发生改变时依次从数组中取出执行就好了,清楚这个思路我们实现它,首先在类上新增两个Array类型的数组,用于存放回调函数:

function Promise(executor) {
    var _this = this;
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledFunc = [];//保存成功回调
    this.onRejectedFunc = [];//保存失败回调
    //其它代码略...
}

这样当then方法执行时,若状态还在等待态(pending),将回调函数依次放入数组中:

Promise.prototype.then = function (onFulfilled, onRejected) {
    //等待态,此时异步代码还没有走完
    if (this.state === 'pending') {
        if (typeof onFulfilled === 'function') {
            this.onFulfilledFunc.push(onFulfilled);//保存回调
        }
        if (typeof onRejected === 'function') {
            this.onRejectedFunc.push(onRejected);//保存回调
        }
    }
    //其它代码略...
};

寄存好了回调,接下来就是当状态改变时执行就好了:

    function resolve(value) {
        if (_this.state === 'pending') {
            _this.value = value;
            //依次执行成功回调
            _this.onFulfilledFunc.forEach(fn => fn(value));
            _this.state = 'resolved';
        }

    }

    function reject(reason) {
        if (_this.state === 'pending') {
            _this.reason = reason;
            //依次执行失败回调
            _this.onRejectedFunc.forEach(fn => fn(reason));
            _this.state = 'rejected';
        }
    }

至此,Promise已经支持了异步操作,setTimeout延迟后也可正确执行then方法返回结果。

链式调用

Promise处理异步代码最强大的地方就是支持链式调用,这块也是最复杂的,我们先梳理一下规范中是怎么定义的:

  1. 每个then方法都返回一个新的Promise对象(原理的核心
  2. 如果then方法中显示地返回了一个Promise对象就以此对象为准,返回它的结果
  3. 如果then方法中返回的是一个普通值(如Number、String等)就使用此值包装成一个新的Promise对象返回。
  4. 如果then方法中没有return语句,就视为返回一个用Undefined包装的Promise对象
  5. 若then方法中出现异常,则调用失败态方法(reject)跳转到下一个then的onRejected
  6. 如果then方法没有传入任何回调,则继续向下传递(值的传递特性)。

规范中说的很抽像,我们可以把不好理解的点使用代码演示一下。
其中第3项,如果返回是个普通值就使用它包装成Promise,我们用代码来演示:

let p =new Promise((resolve,reject)=>{
    resolve(1);
});

p.then(data=>{
    return 2; //返回一个普通值
}).then(data=>{
    console.log(data); //输出2
});

可见,当then返回了一个普通的值时,下一个then的成功态回调中即可取到上一个then的返回结果,说明了上一个then正是使用2来包装成的Promise,这符合规范中说的。
第4项,如果then方法中没有return语句,就视为返回一个用Undefined包装的Promise对象

let p = new Promise((resolve, reject) => {
    resolve(1);
});

p.then(data => {
    //没有return语句
}).then(data => {
    console.log(data); //undefined
});

可以看到,当没有返回任何值时不会报错,没有任何语句时实际上就是return undefined;即将undefined包装成Promise对象传给下一个then的成功态。
第6项,如果then方法没有传入任何回调,则继续向下传递,这是什么意思呢?这就是Promise中值的穿透,还是用代码演示一下:

let p = new Promise((resolve, reject) => {
    resolve(1);
});

p.then(data => 2)
.then()
.then()
.then(data => {
    console.log(data); //2
});

以上代码,在第一个then方法之后连续调用了两个空的then方法 ,没有传入任何回调函数,也没有返回值,此时Promise会将值一值向下传递,直到你接收处理它,这就是所谓的值的穿透。
现在可以明白链式调用的原理,不论是何种情况then方法都会返回一个Promise对象,这样才会有下个then方法。
搞清楚了这些点,我们就可以动手实现then方法的链式调用,一起来完善它:

Promise.prototype.then = function (onFulfilled, onRejected) {
    var promise2 = new Promise((resolve, reject) => {
    //代码略...
    })
    return promise2;
};

首先,不论何种情况then都返回Promise对象,我们就实例化一个新promise2并返回。
接下来就处理根据上一个then方法的返回值来生成新Promise对象,由于这块逻辑较复杂且有很多处调用,我们抽离出一个方法来操作,这也是规范中说明的:

/**
 * 解析then返回值与新Promise对象
 * @param {Object} promise2 新的Promise对象 
 * @param {*} x 上一个then的返回值
 * @param {Function} resolve promise2的resolve
 * @param {Function} reject promise2的reject
 */
function resolvePromise(promise2, x, resolve, reject) {
    //...
}

resolvePromise方法用来封装链式调用产生的结果,下面我们分别一个个情况的写出它的逻辑,首先规范中说明,如果promise2x 指向同一对象,就使用TypeError作为原因转为失败。原文如下:

If promise and x refer to the same object, reject promise with a TypeError as the reason.

这是什么意思?其实就是循环引用,当then的返回值与新生成的Promise对象为同一个(引用地址相同),则会抛出TypeError错误:

let promise2 = p.then(data => {
    return promise2;
});

运行结果:

TypeError: Chaining cycle detected for promise #<Promise>

很显然,如果返回了自己的Promise对象,状态永远为等待态(pending),再也无法成为resolved或是rejected,程序会死掉,因此首先要处理它:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        reject(new TypeError('Promise发生了循环引用'));
    }
}

接下来就是分各种情况处理。当x就是一个Promise,那么就执行它,成功即成功,失败即失败。若x是一个对象或是函数,再进一步处理它,否则就是一个普通值:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        reject(new TypeError('Promise发生了循环引用'));
    }

    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        //可能是个对象或是函数
    } else {
        //否则是个普通值
        resolve(x);
    }
}

此时规范中说明,若是个对象,则尝试将对象上的then方法取出来,此时如果报错,那就将promise2转为失败态。原文:

If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
    //代码略...
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        //可能是个对象或是函数
        try {
            let then = x.then;//取出then方法引用
        } catch (e) {
            reject(e);
        }
        
    } else {
        //否则是个普通值
        resolve(x);
    }
}

多说几句,为什么取对象上的属性有报错的可能?Promise有很多实现(bluebird,Q等),Promises/A+只是一个规范,大家都按此规范来实现Promise才有可能通用,因此所有出错的可能都要考虑到,假设另一个人实现的Promise对象使用Object.defineProperty()恶意的在取值时抛错,我们可以防止代码出现Bug。
此时,如果对象中有then,且then是函数类型,就可以认为是一个Promise对象,之后,使用x作为this来调用then方法。

//其他代码略...
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    //可能是个对象或是函数
    try {
        let then = x.then; 
        if (typeof then === 'function') {
            //then是function,那么执行Promise
            then.call(x, (y) => {
                resolve(y);
            }, (r) => {
                reject(r);
            });
        } else {
            resolve(x);
        }
    } catch (e) {
        reject(e);
    }

} else {
    //否则是个普通值
    resolve(x);
}

这样链式写法就基本完成了。但是还有一种极端的情况,如果Promise对象转为成功态或是失败时传入的还是一个Promise对象,此时应该继续执行,直到最后的Promise执行完。

p.then(data => {
    return new Promise((resolve,reject)=>{
        //resolve传入的还是Promise
        resolve(new Promise((resolve,reject)=>{
            resolve(2);
        }));
    });
})

此时就要使用递归操作了

很简单,把调用resolve改写成递归执行resolvePromise方法即可,这样直到解析Promise成一个普通值才会终止,即完成此规范:

//其他代码略...
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    //可能是个对象或是函数
    try {
        let then = x.then; 
        if (typeof then === 'function') {
            let y = then.call(x, (y) => {
                //递归调用,传入y若是Promise对象,继续循环
                resolvePromise(promise2, y, resolve, reject);
            }, (r) => {
                reject(r);
            });
        } else {
            resolve(x);
        }
    } catch (e) {
        reject(e);
    }

} else {
    //是个普通值,最终结束递归
    resolve(x);
}

到此,链式调用的代码已全部完毕。在相应的地方调用resolvePromise方法即可。

最后的最后

其实,写到此处Promise的真正源码已经写完了,但是距离100分还差一分,是什么呢?

规范中说明,Promise的then方法是异步执行的。

ES6的原生Promise对象已经实现了这一点,但是我们自己的代码是同步执行,不相信可以试一下,那么如何将同步代码变成异步执行呢?可以使用setTimeout函数来模拟一下:

setTimeout(()=>{
    //此入的代码会异步执行
},0);

利用此技巧,将代码then执行处的所有地方使用setTimeout变为异步即可,举个栗子:

setTimeout(() => {
    try {
        let x = onFulfilled(value);
        resolvePromise(promise2, x, resolve, reject);
    } catch (e) {
        reject(e);
    }
},0);

好了,现在已经是满分的Promise源码了。

附上完整代码

function Promise(executor) {
let self = this
this.status = 'pending' //当前状态
this.value = undefined  //存储成功的值
this.reason = undefined //存储失败的原因
this.onResolvedCallbacks = []//存储成功的回调
this.onRejectedCallbacks = []//存储失败的回调
function resolve(value) {
  if (self.status == 'pending') {
    self.status = 'resolved'
    self.value = value
    self.onResolvedCallbacks.forEach(fn => fn());
  }
}
function reject(error) {
  if (self.status == 'pending') {
    self.status = 'rejected'
    self.reason = error
    self.onRejectedCallbacks.forEach(fn => fn())
  }
}
try {
  executor(resolve, reject)
} catch (error) {
  reject(error)
}
}
Promise.prototype.then = function (infulfilled, inrejected) {
let self = this
let promise2
infulfilled = typeof infulfilled === 'function' ? infulfilled : function (val) {
  return val
}
inrejected = typeof inrejected === 'function' ? inrejected : function (err) {
  throw err
}
if (this.status == 'resolved') {
  promise2 = new Promise(function (resolve, reject) {
    //x可能是一个promise,也可能是个普通值
    setTimeout(function () {
      try {
        let x = infulfilled(self.value)
        resolvePromise(promise2, x, resolve, reject)
      } catch (err) {
        reject(err)
      }
    });

  })
}
if (this.status == 'rejected') {

  promise2 = new Promise(function (resolve, reject) {
    //x可能是一个promise,也可能是个普通值
    setTimeout(function () {
      try {
        let x = inrejected(self.reason)
        resolvePromise(promise2, x, resolve, reject)
      } catch (err) {
        reject(err)
      }
    });
  })
}
if (this.status == 'pending') {
  promise2 = new Promise(function (resolve, reject) {
    self.onResolvedCallbacks.push(function () {
      //x可能是一个promise,也可能是个普通值
      setTimeout(function () {
        try {
          let x = infulfilled(self.value)
          resolvePromise(promise2, x, resolve, reject)
        } catch (err) {
          reject(err)
        }
      });
    })
    self.onRejectedCallbacks.push(function () {
      //x可能是一个promise,也可能是个普通值
      setTimeout(function () {
        try {
          let x = inrejected(self.reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (err) {
          reject(err)
        }
      });
    })
  })
}
return promise2
}
function resolvePromise(p2, x, resolve, reject) {
if (p2 === x && x != undefined) {
  reject(new TypeError('类型错误'))
}
//可能是promise,看下对象中是否有then方法,如果有~那就是个promise
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
  try {//为了防止出现 {then:11}这种情况,需要判断then是不是一个函数
    let then = x.then
    if (typeof then === 'function') {
      then.call(x, function (y) {
        //y 可能还是一个promise,那就再去解析,知道返回一个普通值为止
        resolvePromise(p2, y, resolve, reject)
      }, function (err) {
        reject(err)
      })
    } else {//如果then不是function 那可能是对象或常量
      resolve(x)
    }
  } catch (e) {
    reject(e)
  }
} else {//说明是一个普通值
  resolve(x)
}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值