前言

Promise是当前ES6语法中的异步解决方案,本文从基本概念开始,层层分析,一步一步手写实现Promise,希望能和大家一起彻底掌握Promise。

概述

Promise是异步编程的一种解决方案,跟传统回调函数来实现异步相比,其设计更合理,代码更易懂。Promise是”美颜后”的异步解决方案.
在ES6中, 其写进了语言标准提供原生支持。
Promise有两大特点:

  1. 对象的状态不受外界的影响,Promise对象代表一个异步操作,有三种状态: pendingfulfilledrejected
  2. Promise是一个状态机,一旦状态改变,就不会再变,并且在任何时候都可以得到这个结果。

JavaScript进阶之手写Promise - 图1

上图摘自MDN

Promise的具体用法可以参考阮一峰老师的 《ECMAScript 6 入门》MDN
这里有几个点需要注意:

  1. Promise可以有finally,用于做跟状态无关的业务逻辑操作。
  2. Promise中定义的函数在resolve之后还是可以执行语句,所以建议在resolve前加上return

    1. new Promise((resolve, reject) => {
    2. resolve(1);//为了防止输出2,改成return resolve,日常编码中也要养成return resolve的习惯
    3. console.log(2);// 可以输出2
    4. }).then(r => {
    5. console.log(r);
    6. });
  3. then方法中的第二个参数 reject=>{} 可以省略,建议使用 catch(error=>{}) ,其不仅能够获取reject的内容,还可以捕获到Promise函数和then函数中产生的异常。Promise内部的异常会被吃掉,不会被外部的异常捕获。

  4. 注意Promise.all和Promise.race的用法及使用场景。

如果熟悉了Promise的使用,其实我们知道,Promise提供了异步编程的语法糖,使原来异步回调的操作可以用同步的方式来表达。

回调地狱

在传统AJAX异步解决方案中,我们一般使用回调函数来解决数据的接收和处理,如下:

$.get(url, (data) => {
    console.log(data)
)

在某些需求场景下,我们需要发送多个异步请求,并且每个请求之间结果之间需要相互依赖,随着回调函数相互嵌套的增加,函数之间的逻辑就会耦合在一起,难以维护,形成回调地狱。如下所示:

let country = 'china';
let city = 'shanghai';
let district = 'PD'
$.get(`xxxxx/countries`,countries=>{
  /**
  **这里可以再第一个select控件中,渲染国家列表,
  **/
  countries.forEach((item)=>{
          if(item===country){
        //查找并选中当前国家
        $.get(`xxxxx/findCitiesByCountry/${country}`, cities => {
             /**
              **这里可以再第二个select控件中,渲染城市列表,
              **/
            cities.forEach((item)=>{
              if(item === city){
               //查找并选中当前城市
               $.get(`xxxxx/findDistrictsByCity/${city}`, dists => {
                                              /**
                        **这里可以再第三个select控件中,渲染地区列表,
                        **/
                             dists.forEach(item=>{
                          if(item==district){
                             //查找并选中地区
                        }
                      })
               })
              }
            })
        })
      }
  });
});

上述是一个简单的三级联动功能,使用三个回调函数。它们相互嵌套逻辑复杂,耦合严重。
Promise解决了回调地狱问题,通过Promise将上述代码改写成

let country = 'china';
let city = 'shanghai';
let district = 'PD'
new Promise(() => {
    $.get(`xxxxx/countries`, countries => {
        return countries;
    });
}).then((countries) => {
    countries.forEach((item) => {
        if (item === country) {
            $.get(`xxxxx/findCitiesByCountry/${country}`, cities => {
                return cities;
            })
        }
    })
}).then((cities) => {
    cities.forEach((item) => {
        if (item === city) {
            $.get(`xxxxx/findDistrictsByCity/${city}`, dists => {
                return dists;
            })
        }
    })
}).then((dists) => {
    dists.forEach(item => {
        if (item == district) {
            //查找并选中地区
        }
    })
})

此时,将异步执行由原来的回调,改成了 then...then....then... 链式调用的方式。
线性的链式执行(同步)更符合人类的思考习惯(更直白的说,按照顺序一步一步的闭环符合人类思考习惯。Promise就是将原本异步回调的语法形式,改写成同步。所以实现Promise,就是把异步回调这种丑陋的方式改成链式调用。通过手写Promise,我们来理解和消化其设计思想。

开始

有了上述的铺垫,我们了解Promise的概念和特征,也知道了Promise的优势,下面我们一步步来实现Promise。

  1. Promise是一个构造函数,并且传入的参数是一个函数,并且该函数在构造函数中执行

    function Promise(executor){
     try{
     executor()
    }catch(e){
       console.log(e):
    }
    }
    
  2. executor函数的两个参数resolvereject,是executor函数中的回调函数。

    function Promise(executor){
    function resolve(value){
     //可以将executor中的数据传入resolve中
    }
    function reject(value){
     //可以将executor中的数据传入reject中
    }
     try{
     executor(resolve,reject)
    }catch(e){
       console.log(e):
    }
    }
    
  1. Promise实现了状态机,在执行resolve时,由 PENDING=>FULFILLED ,在执行reject时,由 PENDING=>REJECTED 。 ```javascript const pending = ‘PENDING’; const rejecting = ‘REJECTED’; const fulfilled = ‘FULFILLED’; function Promise(executor){ var that = this; that.status = pending; that.value = null; that.error = null; function resolve(val){ //当且仅当PENDING=》FULFILLED if(that.status === pending){ that.status = fulfilled;
     that.value = val;
    
    } } function reject(val){ //当且仅当PENDING=》REJECTED if(that.status === pending){ that.status = rejecting;
     that.error = val;
    
    } } try{ executor(resolve,reject); }catch(e){ //在executor中产生的异常在reject中可以捕获。但是reject的异常,智能catch捕获 reject(e); } }

Promise.prototype.then = function(onFulfilled, onRejected){ var that = this; if(that.status === fulfilled){ //当状态改变后,执行then中的回调 onFulfilled(that.value); } if(that.status === rejecting){ //同上 onRejected(that.error) } };

执行如下代码
```javascript
  new Promise((resolve)=>{
    resolve(1);
  }).then((res)=>{
    console.log(res);
  });

打印结果如下
JavaScript进阶之手写Promise - 图2

  1. Promise是异步的

如果executor函数存在异步,则需要等待resolve或者reject回调执行才会执行then中的函数体。

此处使用回调来解决异步:
第一步:定义两个Promise的成员:onResolvedCallBack和onRejectCallBack,存储then函数中的入参。
第二步:当执行then函数时,如果当前状态还是PENDING(此时executor内部有异步操作)那么就将then中的参数传入onResolvedCallBack和onRejectCallBack中。如果此时状态是非PENDING,那么直接执行传入的函数即可。
第三步:Promise中的resolve函数执行时,触发onResolvedCallBack或者onRejectCallBack中的函数。

具体代码如下:

const pending = 'PENDING';
const rejecting = 'REJECTED';
const fulfilled = 'FULFILLED';
function Promise1(executor) {
  var that = this;
  that.status = pending;
  that.value = null;
  that.error = null;
  that.resolvedCallbacks = [];
  that.rejectedCallbacks = [];

  function resolve(val) {
    if (that.status === pending) {
      that.status = fulfilled;
      that.value = val;
      that.resolvedCallbacks.map(cb => cb(that.value));
    }
  }

  function reject(val) {
    if (that.status === pending) {
      that.status = rejecting;
      that.error = val;
      that.rejectedCallbacks.map(cb => cb(that.value));
    }
  }

  try {
    executor(resolve, reject);
  } catch (e) {
    reject(e);
  }
}


Promise1.prototype.then = function (onFulfilled, onRejected) {
  var that = this;
  //为了保证兼容性,then的参数只能是函数,如果不是要防止then的穿透问题
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : r => {
          throw r
        }
  if (that.status === pending) {
    that.resolvedCallbacks.push(onFulfilled)
    that.rejectedCallbacks.push(onRejected)
  }

  if (that.status === fulfilled) {
    onFulfilled(that.value);
  }

  if (that.status === rejecting) {
    onRejected(that.error)
  }

};

执行如下一段代码:

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

p.then((res) => { console.log(res); });

console.log(2);

打印结果如下:
image.png
我们再来执行如下代码

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

p.then((res) => { console.log(res); });

console.log(2);

此处我们打印结果为
image.png
但是真正的Promise打印结果为:先2后1

  1. Promise的then属于microtasks

microtask和macrotask是Event Loop的知识点,关于Event Loop可以参考阮一峰老师的《JavaScript 运行机制详解:再谈Event Loop》
此处我们使用setTimeout来模拟then的microtasks(注:)

function resolve(value) {
  setTimeout(() => {
    if (that.state === PENDING) {
      that.state = RESOLVED
      that.value = value
      that.resolvedCallbacks.map(cb => cb(that.value))
    }
  }, 0)
}
function reject(value) {
  setTimeout(() => {
    if (that.state === PENDING) {
      that.state = REJECTED
      that.value = value
      that.rejectedCallbacks.map(cb => cb(that.value))
    }
  }, 0)
}
  1. resolve支持传入Promise对象

我们执行如下代码:

let p = new Promise((resolve) => {
  var a = new Promise((resolve) => { resolve(1) });
  resolve(a);
});

p.then((res) => {
  console.log(res);
});

此处resolve传入的是Promise对象,打印结果为:
image.png
所以在resolve函数中需要对value做一次判断

function resolve(value) {
  if (val instanceof Promise) {
    return val.then(resolve, reject);
  }
  setTimeout(() => {
    if (that.state === PENDING) {
      that.state = RESOLVED
      that.value = value
      that.resolvedCallbacks.map(cb => cb(that.value))
    }
  }, 0)
}
  1. then可以链式调用

在Promise中,在then中执行return语句,返回的一定是Promise对象,这也是then能够链式调用的原因。
首先我们将then中的如下片段

  if (that.status === pending) {
    that.resolvedCallbacks.push(onFulfilled)
    that.rejectedCallbacks.push(onRejected)
  }

变形

if (that.status === pending) {
  that.resolvedCallbacks.push(()=>{onFulfilled(that.value)});
  that.rejectedCallbacks.push(()=>{onRejected(that.value)});
}

它们之间只是写法的差异,效果相同。

因为我们需要对then里传入的函数onFulfilled, onRejected返回的值进行判断,所以我们需要对then继续改写

if (that.status === pending) {
  that.resolvedCallbacks.push(()=>{const x = onFulfilled(that.value)});
  that.rejectedCallbacks.push(()=>{const x = onRejected(that.value)});
}

因为then返回的是Promise,所以继续完善

if (that.status === pending) {
  return new Promise(resolve,reject){
      that.resolvedCallbacks.push(()=>{const x = onFulfilled(that.value)});
      that.rejectedCallbacks.push(()=>{const x = onRejected(that.error)});
  }
}

执行onFulfilled和onRejected时,使用try…catch…,所以继续完善

let promise2 = null;
if (that.status === pending) {
  return promise2 = new Promise((resolve,reject)=>{
    that.resolvedCallbacks.push(()=>{
      try{
        const x = onFulfilled(that.value);
      }catch(e){
        reject(e);
      }
    });

    that.rejectedCallbacks.push(()=>{
      try{
        const x = onRejected(that.error);
      }catch(e){
        reject(e);
      }
    });
  });
}

上述x是onFulfilled(that.value)和onRejected(that.error)的返回值,为了保证then可以链式调用,也就是promise2的resolve能够resolve一个Promise对象,但是x返回的可能是Promise对象,可能是值,也可能是函数,那么此处需要对x进行适配一下。此时引入resolvePromise函数,实现如下:

/**
 * 对resolve 进行改造增强 针对x不同值情况 进行处理
 * @param  {promise} promise2 promise1.then方法返回的新的promise对象
 * @param  {[type]} x         promise1中onFulfilled的返回值
 * @param  {[type]} resolve   promise2的resolve方法
 * @param  {[type]} reject    promise2的reject方法
 */
function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {  
        // 如果从onFulfilled中返回的x 就是promise2 就会导致循环引用报错
        return reject(new TypeError('循环引用'));
    }

    let called = false; // 避免多次调用
    // 如果x是一个promise对象 (该判断和下面 判断是不是thenable对象重复 所以可有可无)
    if (x instanceof Promise) { // 获得它的终值 继续resolve
        if (x.status === PENDING) { // 如果为等待态需等待直至 x 被执行或拒绝 并解析y值
            x.then(y => {
                resolvePromise(promise2, y, resolve, reject);
            }, reason => {
                reject(reason);
            });
        } else { 
            // 如果 x 已经处于执行态/拒绝态(值已经被解析为普通值),用相同的值执行传递下去 
            x.then(resolve, reject);
        }
        // 如果 x 为对象或者函数
    } else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
        try { // 是否是thenable对象(具有then方法的对象/函数)
            let then = x.then;
            if (typeof then === 'function') {
                then.call(x, y => {
                    if(called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject);
                }, reason => {
                    if(called) return;
                    called = true;
                    reject(reason);
                })
            } else { // 说明是一个普通对象/函数
                resolve(x);
            }
        } catch(e) {
            if(called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}

此时that.status === pending的代码块也要继续修改

            if (that.status === pending) {
                return promise = new Promise1((resolve, reject) => {
                    that.resolvedCallbacks.push(() => {
                        try {
                            const x = onFulfilled(that.value);
                            resolvePromise(promise,x,resolve,reject);
                        } catch (e) {
                            reject(e);
                        }
                    });
                    that.rejectedCallbacks.push(() => {
                        try {
                            const x = onRejected(that.error);
                        } catch (e) {
                            reject(e);
                        }
                    });
                })

            }

同理that.status===fulfilled和that.status===rejecting的时候代码如下:

    if (that.status === FULFILLED) { // 成功态
        return promise2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                try{
                    let x = onFulfilled(that.value);
                    resolvePromise(promise2, x, resolve, reject); 
                } catch(e) {
                    reject(e); 
                }
            });
        })
    }

    if (that.status === REJECTED) {
        return promise2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                try {
                    let x = onRejected(that.reason);
                    resolvePromise(promise2, x, resolve, reject);
                } catch(e) {
                    reject(e);
                }
            });
        });
    }
  1. Promise的all和race

Promise.all的用法如下

const p = Promise.all([p1, p2, p3]).then((resolve)=>{},(reject)=>{});

Promise.all方法接受一个数组作为参数,只有当数组的所有Promise对象的状态全部fulfilled,才会执行后续的then方法。
根据all的用法和特点,我们推测Promise.all返回一个Promise对象,在Promise对象中去等待Promise数组对象的函数执行完毕,数组中的每个对象执行完毕都+1,当等于数组的长度时,resolve数组对象中所有resolve出来的值。

Promise.all = function(promises) {
    return new Promise((resolve, reject) => {
        let done = gen(promises.length, resolve);
        promises.forEach((promise, index) => {
            promise.then((value) => {
                done(index, value)
                //每执行一次都会往values数组中放入promise对象数组成员resolve的值
                //当values的长度等于promise对象数组的长度时,resolve这个数组values
            }, reject)
        })
    })
}

//使用闭包,count和values在函数的声明周期中都存在
function gen(length, resolve) {
    let count = 0;
    let values = [];
    return function(i, value) {
        values[i] = value;
        if (++count === length) {
            console.log(values);
            resolve(values);
        }
    }
}

Promise.race的用法和all类似,区别就是promise对象数组其中有一个fulfilled,就执行then方法,实现如下

Promise.race = function(promises) {
    return new Promise((resolve, reject) => {
        promises.forEach((promise, index) => {
           promise.then(resolve, reject);
        });
    });
}

需要注意的是all和race函数中的数组成员不一定是Promise对象,如果不是Promise提供了resolve方法将其转化成Promise对象。resolve的实现很简单如下:

Promise.resolve = function (value) {
    return new Promise(resolve => {
        resolve(value);
    });
}

至此一个比较规范的Promise实现了。

参考

《ES6入门-阮一峰》
《ES6 系列之我们来聊聊 Promise》
《异步解决方案——Promise与Await》
《Promise原理讲解 && 实现一个Promise对象》
《面试精选之Promise》
《Promise/A+》
《Promise之你看得懂的Promise》
《八段代码彻底掌握 Promise》
《Promise不会??看这里!!!史上最通俗易懂的Promise!!!》
《Promise 必知必会(十道题)》