解析ES6的Promise核心机制并手写实现

262 阅读10分钟

Promise 是异步编程的一种解决方案,所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

Promises对单个异步操作做出了这样的抽象定义,具体如下所示。

  • Promise操作只会处在3种状态的一种:未完成态、完成态和失败态。
  • Promise的状态只会出现从未完成态向完成态或失败态转化,不能逆反。完成态和失败态不能互相转化。
  • Promise的状态一旦转化,将不能被更改。

image.png

Promise构造函数

Promise()使用一个简单的构造器界面来让用户方便地创建Promise对象:

/* 需用户声明的执行器函数 */
executor = function(resolve, reject) {
    if(/*异步操作成功*/){
        resolve(data);
    }else {
        reject(err)
    }
}
/* 创建Promise对象的构造器 */
var promise = new Promise(executor)

Promise构造函数接受⼀个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不⽤⾃⼰部署。

  • resolve函数的作⽤是:Pending-->Resolved,成功时调⽤,并将异步操作的结果作为参数传递出去;
  • reject函数的作⽤是,Pending-->Rejected,失败时调⽤,并将异步操作报出的错误作为参数传递出去。

executor()是用户定义的执行器函数。当JavaScript引擎通过new运算来创建promise对象时,它事实上会在调用executor()之前就创建好一个新的promise对象的实例,并且得到关联给该实例的两个置值器:resolve()与reject()函数。 接下来,它会调用executor(),并将resolve()与reject()作为入口参数传入,而executor()函数会被执行直到退出。

executor()中的用户代码可以利用上述的两个置值器,来向promise对象“所代理的那个数据”置值。亦即是说,为promise对象绑定(binding)值的过程是由用户代码触发的。这个过程看起来像是“让用户代码回调JavaScript引擎”。 例如:

/* 用户通过代码(resolve或reject)来回调引擎以置值 */
new Promise(function (resolve, reject){
    resolve(100)
})

最后需要补充的是,executor()函数中的resolve()置值器可以接受任何值—除当前promise自身之外。当试图用自身来置值时,JavaScript会抛出一个异常。 ​

Then方法

Promise实例⽣成以后,可以⽤then⽅法分别指定Resolved状态和Rejected状态的回调函数。它的作用是为Promise实例添加状态改变后的需要执行的用户代码。

promise.then(function(res)  {
    console.log(res)
}, function(err){
    console.log(err)
})

Promise处理异步的核心

正上面例子中向promise传入的用户执行器函数直接调用了resolve(100),没有任何的异步逻辑,这样的代码也是成立的,因此Promise处理异步函数的原理并不是内置在Promise机制内的。

/* 用户通过代码(resolve或reject)来回调引擎以置值 */
new Promise(function (resolve, reject){
    resolve(100)
})

也就是说,Promise机制中并没有延时,也没有被延时的行为,更没有对“时间”这个维度的控制。因此在JavaScript中创建一个promise时,创建过程是立即完成的;使用原型方法promise.XXX来得到一个新的promise(即promise2)时也是立即完成的。 ​

同样类似于此的,所有promise对象都是在你需要时立即就生成的,重要的是,这些promise所代理的那个值/数据还没有“就绪(Ready)”。这个就绪过程要推迟到“未知的将来”才会发生。而一旦数据就绪,promise.then(foo)中的foo就会被触发了。 ​

换句话说,Promise处理异步逻辑的核心在于可以向promise构造器函数传入一个异步的执行器函数,一旦异步函数成功时调用resolve进行置值并响应结果通过resolve传给构造函数并保存在构造函数内部,失败时调用reject置值。结合then方法,promise中传入的异步函数一旦成功返回,用户传入Resolved状态回调函数会被触发**。**

通过这种方式处理后,直观的表现为用户传入的异步函数可以并不要求立即生成返回值,换句话说用户不关心它的结果什么时候返回,只要求在它有值返回的时候调用Resolved状态的回调函数。 ​

Promise是一种可在语言层面实现并行执行的模型。它封装了一个“剥离了时间特性的数据”,并代理在该数据上的一切行为。由于该数据是剥离了时间特性的,因此施于它的行为也是没有时序意义、可并行的。 ​

Promise本身并不具有“并行执行”的特性,它的promise实例相当于是封装了数据的触发器:当数据就绪(Ready)时,就触发指定行为(Actions)。而后者(行为或反应),才是真正的执行逻辑。 ​

因此Promise语境下的Hello World程序的正确写法是:

Promise.resolve('hello world') // data Ready ?
  .then(res => console.log(res)) // call Action?

Promise作为构造器,其作用是将一个为futurel置值的函数关联(resolving或binding)到具体的promise实例。 ​

简易版Promsie实现

上面已经基本讲完了Promise的核心机制,Promise内部保存了一个状态容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。 ​

Promise通常包含两部分,Promise构造函数以及基于原型链实现的then方法。

  • Promise构造函数接受一个执行器函数,用户通过代码(resolve或reject)来回调引擎以置值。
  • then方法为Promise实例添加状态改变后的需要执行的用户代码ResolveCallback和RejectCallback。

基于上面的讨论我们很模拟出基本的实现:

promsie构造函数
  1. Promise构造函数接受executor作为执行器函数,通常这个executor里包裹了一个异步操作。
  2. Promise构造函数内部保存了state状态和data异步函数结果,以及定义了resolve置值函数和reject置值函数。
  3. 调用executor并传入构造函数内部的resolve方法和reject方法作为函数实际参数。开始置值操作

image.png

then方法
  1. 当Promise的状态发生了改变,不论是成功或是失败都会调用then方法。它的作用是为Promise实例添加状态改变时的回调函数并在异步完成时执行这个函数。
  2. then方法被调用的时候,并不确定当前promise处于那个状态下,因为针对三种状态分别处理
    • 如果state已经变为了fulfilled状态,运行用户传入的ResolveCallback
    • 如果state已经变为了failed状态,运行用户传入的RejectCallback
    • 当执行then时候,异步函数可能还没有执行完成,此时state状态依然是pending,这时要将回调函数保存起来,留待状态改变的时候执行。而状态的改变依赖用户传入的执行器函数,所以回到构造函数内部需要增加ResolvedCallbacks队列和RejectedCallbacks队列用以保存回调函数,并且在状态改变的时候需要手动调用对应的回调函数

image.png

分析完以后对应如下代码:

// myPromise.js
function Promise (executor) {
    this.state = 'pending';
    this.data = undefined;
    this.reason= undefined;
    this.resolvedCallbacks=[];
    this.rejectedCallbacks=[];
    let self = this
    // 保存内部状态和响应数据,依次调用成功的回调函数
    let resolve = value => {
        if(this.state === 'pending'){
            this.state = 'fulfilled';
            this.data = value;
            for(let i = 0; i< self.resolvedCallbacks.length; i++){
                self.resolvedCallbacks[i](value);
            }
        }
    };
    // 保存内部状态和响应数据,依次调用失败的回调函数
    let reject = reason => {
        if(this.state === 'pending'){
            this.state = 'failed';
            this.reason = reason;
            for (let i = 0; i < self.rejectedCallbacks.length; i++) {
                self.rejectedCallbacks[i](reason);
            }
        }
    };
    try {
        // 向promise对象“所代理的那个数据”置值
        // 由用户传入的回调函数(接受resolve, reject参数)
        executor(resolve, reject);
    } catch (err) {
        reject(err);
    }
}
Promise.prototype.then = function(fn1, fn2) {
    var self = this
    var promise2
  
    // 首先对入参 fn1, fn2做判断
    fn1 = typeof fn1 === 'function' ? fn1 : function(v) {}
    fn2 = typeof fn2 === 'function' ? fn2 : function(r) {}
  
    if (self.state === 'fulfilled') {
          // 把 fn1、fn2 放在 try catch 里面,毕竟 fn1、fn2 是用户传入的,报错嘛,很常见
          try {
              var x = fn1(self.data)
          } catch (e) {       
          }
    }
  
    if (self.state === 'rejected') {
          try {
              var x = fn2(self.data)
          } catch (e) {
          }
    }
  
    if (self.state === 'pending') {
        // 当执行then时候,异步函数可能还没有执行完成,此时state状态依然是pending
      	// 所以这步处理要将Resolved回调函数和Rejected回调函数保存
      	self.resolvedCallbacks.push(function(value){
            try {
                var x = fn1(self.data);
            } catch (e) {
            }
        })
        self.rejectedCallbacks.push(function(value) {
            try {
                var x = fn2(self.data);
            } catch (e) {
            }
        })
    }
  }
  
  module.exports = Promise

至此,一个简易版的promise已经实现。 ​

验证我们手写的Promsie

// mydemo.js
const Promise = require('./myPromise.js')

// Promsie构造函数转入的回调函数是一个“数据置值器”
// 接收resolve,和reject,成功时调用resolve置值,失败时调用reject置值
// 用户通过代码(resolve或reject)来回调引擎以置值
const p = new Promise(function (resolve, reject){
    // 设置了一个定时器,1s后才能知道对应的状态
    try{
        setTimeout(() => {
            resolve('我在1s后被执行')
        }, 1000);
    } catch (e) {
        reject(e)
    }
})

// Promise的状态发生了改变后需要执行的用户代码
// then执行的时候state的状态还没确定下来
p.then((res) => {
    console.log(res)
}, (e) => {
    console.log(e)
})

运行结果如预期 image.png

包含then链的Promsie实现

假设我们有一个场景是多异步相互依赖,后一个异步流程依赖于前一个异步流程,这里针对这种场景promise提供了链式调用的写法。链式调用支持promise每一次返回新的promise,响应结果作为回调函数函数传给新的promsie。 ​

两个promise对象之间顺序执行的关系,在JavaScript中被称为“Then链”。通过调用p.then()的方式来约定当前promise对象与下一个promise2对象之间的“链”关系,并且这事实上也代表了它们之间的顺序执行关系,是Promise机制的基本用法和关键机制。 ​

p.then()代表了对顺序逻辑的理解,同时它隐含地说明:promise2与promise两者所代理的数据之间是有关联的。 ​

因此从宏观的角度上来看,是“给promise绑定值”的行为(result ready)触发了thenable行为。所谓“thenable行为”,就是调用“Then链(thenable chain)”的后续promise置值器,并在整个链上触发连锁的“thenable行为”。 ​

最后,在p.then()中,它主要完成了三件事:

  • 创建新的promise2对象;并且,
  • 登记p与promise2之间的关系;然后,
  • 将ResolvedCallbacks、RejectedCallbacks关联给promise2的resolve置值器。

因此对上面实现的promise进行改造,主要是针对then中的处理,返回一个新的promise函数,回调函数成功调用并返回后进行置值处理。then链的执行流程变成了如下:

image.png

完整代码如下

// myPromise.js
function Promise (executor) {
    this.state = 'pending';
    this.data = undefined;
    this.reason= undefined;
    this.resolvedCallbacks=[];
    this.rejectedCallbacks=[];
    let self = this
    // 保存内部状态和响应数据,依次调用成功的回调函数
    let resolve = value => {
        if(this.state === 'pending'){
            this.state = 'fulfilled';
            this.data = value;
            for(let i = 0; i< self.resolvedCallbacks.length; i++){
                self.resolvedCallbacks[i](value);
            }
        }
    };
    // 保存内部状态和响应数据,依次调用失败的回调函数
    let reject = reason => {
        if(this.state === 'pending'){
            this.state = 'failed';
            this.reason = reason;
            for (let i = 0; i < self.rejectedCallbacks.length; i++) {
                self.rejectedCallbacks[i](reason);
            }
        }
    };
    try {
        // 向promise对象“所代理的那个数据”置值
        // 由用户传入的回调函数(接受resolve, reject参数)
        executor(resolve, reject);
    } catch (err) {
        reject(err);
    }
}
Promise.prototype.then = function(fn1, fn2) {
    var self = this
    var promise2
  
    // 首先对入参 fn1, fn2做判断
    fn1 = typeof fn1 === 'function' ? fn1 : function(v) {}
    fn2 = typeof fn2 === 'function' ? fn2 : function(r) {}
  
    if (self.state === 'fulfilled') {
        return promise2 = new Promise(function(resolve, reject) {
            // 把 fn1、fn2 放在 try catch 里面,毕竟 fn1、fn2 是用户传入的,报错嘛,很常见
            try {
                var x = fn1(self.data)
                // fn1 执行后,会有返回值,通过 resolve 注入到 then 返回的 promise 中
                resolve(x)
            } catch (e) {
                reject(e)                
            }
        })
    }
  
    if (self.state === 'failed') {
        return promise2 = new Promise(function(resolve, reject) {
            try {
                var x = fn2(self.data)
                resolve(x)
            } catch (e) {
                reject(e)
            }
        })
    }
  
    if (self.state === 'pending') {
        return promise2 = new Promise(function(resolve, reject) {
            self.resolvedCallbacks.push(function(value){
                try {
                    var x = fn1(self.data);
                    resolve(x)
                } catch (e) {
                    reject(e)
                }
            })
            self.rejectCallbacks.push(function(value) {
                try {
                    var x = fn2(self.data);
                    resolve(x)
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
  }

验证promsie支持链式调用的特性。

const Promise = require('./myPromise.js')

const p = new Promise(function (resolve, reject){
    try{
        setTimeout(() => {
            resolve('我在1s后被执行')
        }, 1000);
    } catch (e) {
        reject(e)
    }
})

p.then((res) => {
    console.log('p1', res)
    return '回调结果成功'
}).then((res) => {
    console.log('p2', res)
})

image.png

这里需要注意的是下一个then链返回的是上一个then链回调的结果,如果上一个then链回调没有返回值将会返回undefined,即如果注视p.then中的回调中的return返回值,运行后将会返回 image.png

当then函数返回是一个promise时,上面的代码运行后会返回这个promise对象,

例如改造一下mydemo.js:

const Promise = require('./myPromise.js')

const p = new Promise(function (resolve, reject){
    // 设置了一个定时器,1s后才能知道对应的状态
    try{
        setTimeout(() => {
            resolve('我在1s后被执行')
        }, 1000);
    } catch (e) {
        reject(e)
    }
})

p.then((res) => {
    console.log('p1', res)
    // return '回调结果成功'
    return new Promise(function (resolve, reject){
        try{
            setTimeout(() => {
                resolve('我在第2s后被执行')
            }, 1000);
        } catch (e) {
            reject(e)
        }
    })
}).then((res) => {
    console.log('p2', res)
})

为了得到预期的效果,需要对单独判断一下:

// myPromise.js

...

Promise.prototype.then = function(fn1, fn2) {
    var self = this
    var promise2
  
    // 首先对入参 fn1, fn2做判断
    fn1 = typeof fn1 === 'function' ? fn1 : function(v) {}
    fn2 = typeof fn2 === 'function' ? fn2 : function(r) {}
  
    if (self.state === 'fulfilled') {
        return promise2 = new Promise(function(resolve, reject) {
            // 把 fn1、fn2 放在 try catch 里面,毕竟 fn1、fn2 是用户传入的,报错嘛,很常见
            try {
                var x = fn1(self.data)
                if (x instanceof Promise) {
                    // 如果onResolved的返回值是一个Promise对象,直接取它的结果作为promise2的结果
                    x.then(res => resolve(res), (e) => reject(e))
                } else {
                    // fn1 执行后,会有返回值,通过 resolve 注入到 then 返回的 promise 中
                    resolve(x)
                }
            } catch (e) {
                reject(e)                
            }
        })
    }
  
    if (self.state === 'failed') {
        return promise2 = new Promise(function(resolve, reject) {
            try {
                var x = fn2(self.data)
                if (x instanceof Promise) {
                    // 如果onResolved的返回值是一个Promise对象,直接取它的结果作为promise2的结果
                    x.then(res => resolve(res), (e) => reject(e))
                } else {
                    // fn1 执行后,会有返回值,通过 resolve 注入到 then 返回的 promise 中
                    resolve(x)
                }
            } catch (e) {
                reject(e)
            }
        })
    }
  
    if (self.state === 'pending') {
        return promise2 = new Promise(function(resolve, reject) {
            self.resolvedCallbacks.push(function(value){
                try {
                    var x = fn1(self.data);
                    if (x instanceof Promise) {
                    	// 如果onResolved的返回值是一个Promise对象,直接取它的结果作为promise2的结果
                      x.then(res => resolve(res), (e) => reject(e))
                    } else {
                        // fn1 执行后,会有返回值,通过 resolve 注入到 then 返回的 promise 中
                        resolve(x)
                    }
                } catch (e) {
                    reject(e)
                }
            })
            self.rejectCallbacks.push(function(value) {
                try {
                    var x = fn2(self.data);
                    if (x instanceof Promise) {
                        // 如果onResolved的返回值是一个Promise对象,直接取它的结果作为promise2的结果
                        x.then(res => resolve(res), (e) => reject(e))
                    } else {
                        // fn1 执行后,会有返回值,通过 resolve 注入到 then 返回的 promise 中
                        resolve(x)
                    }
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
  }

运行demo