你真的理解Promise吗?

1,389 阅读7分钟

异步Javascript与回调地狱

由于Javascript起初是在浏览器中运行的编程语言,其执行会阻塞浏览器对dom树的解析和渲染,所以js中充满了大量的异步操作,例如:dom事件绑定,Ajax异步请求。Javascript起初是使用回调函数作为异步操作的解决方案。

dom.addEventListener(event, callback)

但使用回调函数面临了一个问题,就是当多个异步操作互相嵌套时就会产生“回调地狱”。

function asyncFunc1(res) {
  asyncFunc2(res, function(res1) {
    asyncFunc3(res1, function(res2) {
      asyncFunc4(res2, function(res3) {
        ...
      })
    })
  })
}

上面的代码可以理解为外层的函数的返回结果会作为内层函数的参数,并且一层一层传递下去。但是,这样的代码并不美观,非常影响阅读,并且耦合性强,修改起来很容易出现bug。为了解决这一问题,Promise就此问世。

Promise简介

Promise是es6版本Javascript异步编程的解决方案。从语法上来看,Promise是一个对象,可以从中获取到异步操作的结果。我们也可以将其理解为一个容器,包含着未来将要完成的事件。

Promise采用链式调用的方式(类似于JQuery),在调用链的各个环节执行不同的操作,降低了每一步操作之间的耦合,有效解决了回调地狱的问题。

new Promise(function(resolve, reject) {
  resolve(someResult);
}).then(function(res) {
  return res;  
},function(err) {
  
}).catch(function(err) {
  
})

如上例代码所示,我们通过Promise构造函数创建一个promise对象,该对象的状态立即变为pending意为等待异步方法执行完成,当方法执行成功后,通过参数传进去的resolve方法将执行结果传给promise对象,并将promise对象的状态设置为fulfilled意为执行成功。一旦执行失败,便可通过reject方法处理失败信息,并将状态设置为rejected。

Promise对象状态一旦变为fulfilled或者rejected便不再可变。

Promise对象还有两个实例方法then和catch,由于可以进行链式调用,我们可以知道then和catch分别返回一个promise对象。then方法可以接收两个参数,一个执行成功的回调,另一个是失败后的回调,而catch只接收失败后的回调。

一步步手写实现Promise

上面简单介绍了Promise的是如何执行的,但是我们不仅仅应该知道它是怎么使用,还要懂得其原理。所以接下来我们将一步步实现自己的promise。

最基本的promise

在这一步中我们先实现一个接收同步方法的promise,promise对象有三个状态,所以我们先定义三个常量来代表这些状态。

var PENDING = 'pending';
var FULFILLED = 'fulfilled';
var REJECTED = 'rejected';

该同步方法可以接收两个参数:resolve和reject,resolve方法会将promise对象状态变为fulfilled,并将接收到的值赋给promise对象。而reject将状态置为rejected,并传入错误信息。由于promise对象状态变为fulfilled或者rejected之后不再可变,所以在这两个方法中判断当前状态是否为pending,只有是pending时才可以执行里面的逻辑。

function resolve(val) {
  if (this.status === PENDING) {
    this.status = FULFILLED;
    this.value = val;
  }
}
function reject(err) {
  if (this.status === PENDING) {
    this.status = REJECTED;
    this.error = err;
  }
}

接下来实现我们Promise构造函数的主体部分,从上面resolve和reject两个方法可以看出,Promise构造函数将定义三个字段:status,value和error,并且在内部执行传进来的函数。

function CustomPromise(executor) {
    this.status = PENDING;
    this.value = null;
    this.error = null;
    try {
        executor(resolve.bind(this), reject.bind(this));
    } catch (e) {
        reject.call(this, e.message)
    }
}

注意在上面执行executor的时候resolve和reject两个方法使用bind绑定到CustomPromise创建的实例上,否则resolve和reject里面的this会指向全局作用域。由于executor执行过程中也可能出现错误,使用try...catch...来捕获错误信息。

现在来实现then方法和catch方法,then方法接收两个函数作为参数,分别在executor执行成功和失败后调用,catch则只处理失败的情况。同时如果要实现多级调用链的话,我们需要在then和catch中返回一个customPromise对象。

CustomPromise.prototype.then = function(successHandler, failureHandler) {
    var self = this;
    if (self.status === FULFILLED) {
        try {
            var res = successHandler(self.value);
            resolve.call(self, res);
        } catch (e) {
            reject.call(self, e.message);
        }
    }
    if (self.status === REJECTED) {
        reject.call(self, failureHandler(self.value));
    }
    return self;
}
CustomPromise.prototype.catch = function(failureHandler) {
    var self = this;
    reject.call(self, failureHandler(self.error));
    return self;
}

在then方法内部判断当前状态为fulfilled或者是rejected(由于当前只接收同步方法暂不考虑pending状态),如果是fulfilled,将successHandler的返回值通过resolve方法处理,并且捕获successHandler执行过程中的错误,如果报错便使用reject更改错误信息。

这样一个最基本的promise就完成了,现在让我们来测试一下吧。

function resolvedFunc(resolve, reject) {
    resolve(3);
}
function rejectedFuc(resolve, reject) {
    reject(3);
}
function errorFunc(resolve, reject) {
    throw new Error('error');
}
console.log(new CustomPromise(resolvedFunc));
//CustomPromise { status: 'fulfilled', value: 3, error: null }
console.log(new CustomPromise(rejectedFuc));
//CustomPromise { status: 'rejected', value: null, error: 3 }
console.log(new CustomPromise(errorFunc));
//CustomPromise { status: 'rejected', value: null, error: 'error' }
console.log(new CustomPromise(resolvedFunc).then(res => res + 3).then(res => res + 3));
//CustomPromise { status: 'fulfilled', value: 9, error: null }
console.log(new CustomPromise(rejectedFuc).catch(err => err + 3));
//CustomPromise { status: 'rejected', value: null, error: 6 }

上面测试了五种不同的情况,下面的注释便是执行的结果,从结果我们可以看出customPromise对象的status,value和error字段的变化都是符合预期的。

既然同步方法测试成功了,那来看一下异步方法的测试会怎么样吧。

function someAsync(resolve, reject) {
    setTimeout(() => {
        resolve(3)
    }, 1000);
}
new CustomPromise(someAsync).then(res => console.log(res));
//

看样子好像不太行,由于resolve方法是在一秒后才执行,但是我们的successhandler却是立即执行了,所以打印出来的值为空。

添加异步操作

在处理异步方法时,我们可以参考javascript的消息队列模式。当遇到异步事件时,先将其放入一个队列中,当执行成功或者失败时,再将其取出。

// CustomPromise;
this.onResolve = [];
this.onReject = []

我们在CustomPromise的构造函数中加入两个数组onResolve和onReject分别来代表执行成功和失败的回调队列。

//CustomPromise.prototype.then
if (this.status === PENDING) {
   self.onResolve.push(function () {
       try {
           var res = successHandler(self.value);
           resolve.call(self, res);
       } catch (e) {
           reject.call(this, e.message);
       }
    });
    if (failureHandler) {
       self.onReject.push(function () {
           reject.call(self, failureHandler(self.value));
       })
    }
}
//CustomPromise.protype.catch
CustomPromise.prototype.catch = function(failureHandler) {
    var self = this;
    if (self.status === PENDING) {
        self.onReject.push(function () {
            reject.call(self, failureHandler(self.error));
        })
    }
    if (self.status === REJECTED) {
        reject.call(self, failureHandler(self.error));
    }
    return self;
}
//resolve
function resolve(val) {
    if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = val;
        this.onResolve.map(r => r());
    }
}
//reject
function reject(err) {
    if (this.status === PENDING) {
        this.status = REJECTED;
        this.error = err;
        this.onReject.map(r => r());
    }
}

这里我们再then方法中加入了pending状态的处理逻辑,将成功的回调加入onResolve队列,并且把失败的回调放入onReject队列。在resolve和reject方法中分别加入对相应回调队列的处理方式,当异步结果返回时,从队列中拿出回调函数并执行。

那么让我们测试一下我们的成果吧

function someAsync(resolve, reject) {
    setTimeout(() => {
        resolve(3)
    }, 1000);
}
new CustomPromise(someAsync).then(res => console.log(res));
//3

看起来结果是正确的,在一秒之后控制台打印出了3,但是当我们测试一下多级调用时又出现了一些问题。

function someAsync(resolve, reject) {
    setTimeout(() => {
        resolve(3)
    }, 1000);
}
new CustomPromise(someAsync).then(res =>res + 4).then(res => console.log(res));
//3

控制台打印出来的是3而不是正确结果7,问题出在哪里了呢?虽然把successHandler加入了执行队列,但是并没有执行就已经返回了原来的promise对象,所以第二个console打印出来的还是resolve传进去的3。这个问题也困扰了笔者好久,看了很多篇文章也没发现解决方法。最后想到,如果在then方法返回一个新的customPromise对象会是如何呢,那让我们来试试吧。

CustomPromise.prototype.then = function (successHandler, failureHandler) {
    var self = this;
    if (this.status === FULFILLED) {
        return new CustomPromise(function (resolve, reject) {
            try {
                var res = successHandler(self.value);
                resolve(res);
            } catch (e) {
                reject(e.message);
            }
        });
    }
    if (this.status === REJECTED) {
        return new CustomPromise(function (resolve, reject) {
            reject(failureHandler(self.error));
        })
    }
    if (this.status === PENDING) {
        return new CustomPromise(function (resolve, reject) {
            self.onResolve.push(function () {
                try {
                    var res = successHandler(self.value);
                    resolve(res);
                } catch (e) {
                    reject(e.message);
                }
            });
            if (failureHandler) {
                self.onReject.push(function () {
                  var err = failureHandler(self.error);
                  reject(err)
                })
            }


        })
    }
}

这次对我们的then方法进行了一次大改造,在每一个状态判断条件下都创建并返回了一个新的customPromise对象,并且使用新customPromise对象的resolve和reject方法,这样就可以保证每次更改的都是新对象的状态值。

那最后再来测试一次

new CustomPromise(someAsync)
.then(res => res + 4)
.then(res => res - 9)
.then(res => res + 1)
.catch(err => console.log(err))
.then(res => {
    console.log(res)
})
//-1

控制台打印出了正确的结果-1,这样我们的promise就大功告成了!