通过promise-polyfill源码深入理解promise工作机制

1,402 阅读10分钟

Promise核心概述

Promise是js异步编程的一种解决方案,作为容器封装一些逻辑(通常是异步操作),并维护内部执行状态;状态主要是两种,初始状态为pending,操作结束后可改为fulfilled(resolved)或rejected;==then为Promise原型上的方法,通过传入参数函数(可以提供两个,分别对应两种状态)作为当前promise状态变化后的回调并返回一个新的Promise实例实现链式调用==,解决了callback方式的“回调地狱”问题;Promise.prototype.catch和Promise.prototype.finally实际上为Promise.prototype.then的特殊情况,本文不做赘述;

Promise基本使用

const promise = new Promise((resolve, reject) => {
    ...
    resolve();
}).then(() => {
    ...
}).then(() => {
    ...
});

promise.then(() => {
    ...
});

先看一段熟悉的代码,尝试分析一下这样一段“简单”的代码到底干了什么事情,执行顺序是什么;由于Promise实现很多都使用了native代码,我们“曲线救国”,用polyfill来做一个分析;

下面是promise-polyfill这个库的github地址,代码少而精,值得一读;

一个简单优雅的Promise浏览器polyfill

promise-polyfill核心源码分析

为了方便抓住重点,下文的源码分析中我会直接省略掉非核心代码;

首先是Promise构造器,也就是我们通过new Promise()调用的方法;

/**
 * @function Promise构造器
 * @param fn 用户代码传给Promise构造器的参数函数
 */
function Promise(fn) {

  // 在当前实例中保存执行状态,初始为pending
  this._state = 0;
  // 用户代码通过resolve(或reject)传入的参数
  this._value = undefined;
  // 当前Promise注册的回调列表
  // 每个item实际上是callback + 提供该callback的then中创建的新Promise实例封装成的一个对象
  this._deferreds = [];

  // 调用doResolve,参数为构造器的参数函数及this,即当前promise实例
  doResolve(fn, this);
  
}

可以看到Promise实例中会保存当前Promise内部的执行状态、内部操作结束后用户代码通过resolve或reject传入的参数、回调列表,其中回调列表的每一项其实是==callback和提供该callback的then中创建的新Promise实例封装成的一个对象==,理解这个很关键,不过我们先继续,一会再回过头来看这个对象;接着构造器中==同步==调用了doResolve,调用后执行结束返回promise实例;接下来看下doResolve实现;

/**
 * @function 执行用户代码传给Promise构造器的参数函数,注入resolve、reject
 * @param fn 用户代码传给Promise构造器的参数函数
 * @param self 当前promise实例
 */
function doResolve(fn, self) {
  try {
    // 同步调用fn并注入两个函数作为参数
    // 注意,这两个函数最终是提供给用户程序来调用的
    fn(
      function(value) {
        resolve(self, value);
      },
      function(reason) {
        reject(self, reason);
      }
    );
  } 
  // fn异常时调用reject
  catch (ex) {
    reject(self, ex);
  }
}

==doResolve主要作用就是执行用户代码给Promise构造器传入的参数函数==,并注入由Pormise内部提供的resolve和reject函数;接下来看看resolve和reject的实现;

/**
 * @function 改变当前Promise实例的状态,保存用户传入的参数,并尝试执行回调
 * @param self 当前promise实例
 * @param newValue 用户代码传给resolve的参数
 */
function resolve(self, newValue) {
  try {
    // 内部状态改为resolved
    self._state = 1;
    // 保存用户代码传给resolve的参数
    self._value = newValue;
    finale(self);
  } catch (e) {
    reject(self, e);
  }
}

function reject(self, newValue) {
  // 内部状态改为rejected
  self._state = 2;
  self._value = newValue;
  finale(self);
}

可见当用户代码中调用resolve后,当前Promise实例的执行状态会发生改变,并将用户代码传入的参数保存下来,并调用finale;所以==resolve和reject的主要作用就是改变当前promise的状态并尝试调用回调==;接下来看看finale的实现;

/**
 * @function 通过遍历尝试执行当前Promise实例中保存的所有回调
 * @param self 当前promise实例
 */
 function finale(self) {
  // 遍历当前Promise实例中保存的回调列表
  for (var i = 0, len = self._deferreds.length; i < len; i++) {
    // 调用handle方法执行callback
    handle(self, self._deferreds[i]);
  }
  // 全部callback执行完成后列表置空
  self._deferreds = null;
}

==finale的主要作用就是调用当前Promise实例中所有注册的callback==,那这些callback是怎么注册的呢?其实就是通过Promise.prototype.then;不过我们先看下handle方法是如何执行这些callback的;

/**
 * @param self 当前promise实例
 * @param deferred callback + 提供该callback的then中创建的新Promise实例封装成的一个对象
 */
function handle(self, deferred) {
  // promise内部状态还未改变,即还是pending
  if (self._state === 0) {
    // 保存该回调并返回
    self._deferreds.push(deferred);
    return;
  }
  // 利用Promise上的静态方法_immediateFn来执行callback
  Promise._immediateFn(function() {
    // 根据promise内部状态(即resolved或rejected)执行对应callback
    var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
    // 执行回调,参数为用户代码通过resolve或reject传入的值
    var ret = cb(self._value);
    // 再次调用resolve
    // 传入deferred.promise(即提供该callback的then中创建的新Promise实例)和该callback的返回值
    resolve(deferred.promise, ret);
  });
}

Promise._immediateFn其实就是用setTimeout 0,即用宏任务模拟真实Promise实现中产生的微任务,不做赘述;可以看到handle方法做了几件事情,若当前Promise状态还在pending,则==将callback和提供该callback的then创建的promise封装成一个对象保存==到列表中,这个过程可以理解为==pending状态下promise的callback注册==;若当前Promise状态已改变,则根据状态==起一个异步任务,在异步任务中执行对应的callback==,polyfill中是宏任务,而真实的promise是微任务;另外一个重要的事情是在这个异步任务中执行callback后,再次调用resolve并传入deferred.promise,即提供这个微任务中执行的callback的then创建的promise,==利用resolve来改变这个promise的状态并尝试调用注册在这个promise中的callback==;

最后我们看下Promise.prototype.then的实现;

/**
 * @param onFulfilled 当前promise状态变为resolved后的回调
 * @param onRejected 当前promise状态变为rejected后的回调
 */
Promise.prototype.then = function(onFulfilled, onRejected) {
  // 创建一个新的Promise,参数为一个空函数
  var promise = new this.constructor(() => {});
  // 调用handle尝试执行当前promise的回调
  handle(this, new Handler(onFulfilled, onRejected, promise));
  // 返回新创建的promise
  return promise;
};

注意,由于then是Promise原型上的方法,所以函数体中this指向用户代码中.then前面的promise对象,这个对象可能是new Promise()返回的,也可能是then()返回的;可以看到,then方法中主要就做了两件事情,一个是创建一个新的Promise实例并返回;另一个是将then方法的参数,即两个callback和新创建的promise实例封装并传递给handle,尝试利用handle尝试调用this指向的promise的回调;回忆一下,==在handle中如果该promise状态还是pending,则会将回调注册起来,否则起一个异步任务执行==;

看一眼用来封装callback和提供该callback的then创建的promise的Handler构造;

/**
* @param onFulfilled resolved的回调
* @param onRejected rejected的回调
* @param promise promise实例
*/
function Handler(onFulfilled, onRejected, promise) {
 this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
 this.onRejected = typeof onRejected === 'function' ? onRejected : null;
 this.promise = promise;
}

小结

总结一下几个重要的点

  • new Promise((resolve, reject) => {})中==同步调用==传入的fn函数并注入resolve、reject函数,Promise实例状态可通过resolve或reject发生变化,最后返回promise实例;
  • Promise.prototype.then(resolveCallback, rejectCallback)中==同步==创建一个新的promise实例,并尝试调用then方法的参数,即callback函数;如果当前promise还在pending状态则将callback和then创建的promise保存在该promise中,可理解为注册回调,等待该promise状态变化后再起==异步任务==执行已注册的callback;如果当前promise已resolve或reject,则直接起一个==异步任务==,在该任务中根据状态执行对应callback,并在执行完callback后改变then创建的promise状态并调用该promise中注册的回调(同样以==异步任务==的方式);以此类推;

可见其实除了执行callback时会起一个异步任务(真实的Promise实现中是微任务,polifill中是宏任务),其他代码都是同步执行;

实例分析

来个稍微复杂一点的例子加深理解;

new Promise((resolve, reject) => {
    console.log('outer promise');
    resolve();
}).then(() => {
    new Promise((resolve, reject) => {
        console.log('inner promise');
        resolve();
    }).then(() => {
        console.log('inner then1');
    }).then(() => {
        console.log('inner then2');
    });
}).then(() => {
    console.log('outer then1');
}).then(() => {
    console.log('outer then2');
});

下面是输出结果;

outer promise
inner promise
inner then1
outer then1
inner then2
outer then2

我们来分析一下执行顺序;

  1. 首先开始同步执行外层Promise构造,构造器内部调用传入的参数函数,==输出outer promise==;接着用户代码调用resolve,外层promise状态变为resolved,构造器调用结束返回外层promise outerP1;
  2. 接着同步在outerP1上调用then方法,then内部检查outerP1状态,发现是resolved,于是起一个异步任务task1并同步返回一个新的promise实例outerP2(初始状态为pending),在task1中做两件事情,一是调用当前then中提供的callback,二是改变outerP1的状态并调用outerP1中注册的callback;
  3. 继续同步执行外层第二个then,由于outerP2状态还是pending,则将自己创建的promise outerP3和对应的callback都注册到outerP2中保存起来;
  4. 继续同步执行到外层第二个then,同样由于outerP3状态还是pending则将自己创建的promise outerP4和对应的callback都注册到outerP3中保存起来;
  5. ==第一轮同步代码执行结束==;
  6. 接下来开始执行刚才放进事件循环的异步任务task1,同步调用内层的Promise构造,构造器内部调用传入的参数函数,==输出inner promise==;接着用户代码调用了resolve,内层promise状态变为resolved,构造器调用结束,返回内层promise innerP1;
  7. 接着同步在innerP1上调用then方法,then内部检查当前promise状态,发现是resolved,于是起一个异步任务task2调用callback并同步返回一个新的promise实例innerP2;
  8. 继续同步执行内层第二个then,由于innerP2状态还是pending则注册自己和callback到innerP2,返回innerP3;
  9. 此时异步任务task1中的callback执行结束,改变outerP2的状态为resolved并起一个异步任务task3调用其内部保存的callback,即输出outer then1;
  10. 此时事件循环中还有task2和task3,按顺序先执行执行task2,==输出inner then1==,同时使innerP2状态变为resolved并起一个异步任务task4,在task4中调用innerP2中注册的callback,即输出inner then2;
  11. 继续执行task3,==输出outer then1==,并使outerP3状态变为resolved,起一个异步任务task5调用对应callback,即输出outer then2;
  12. 此时事件循环中还有task4和task5,先执行执行task4,==输出inner then2==,然后将innerP3状态改为resolved,由于innerP3后续没有调用then,即没有注册callback,则task4执行结束;
  13. 接着调用事件循环中最后一个任务task5,==输出outer then2==;
  14. 至此,所有代码执行完毕;

最后整理一下刚才的代码,贴上一些注释方便大家理解;

// 同步执行,返回outerP1
new Promise((resolve, reject) => {
    console.log('outer promise');
    // outerP1状态resolved
    resolve();
})
// 同步执行,起异步任务task1,返回outerP2
.then(
    // task1
    () => {
    // 同步执行,返回innerP1
    new Promise((resolve, reject) => {
        console.log('inner promise');
        // innerP1状态resolved
        resolve();
    })
    // 同步执行,起异步任务task2,返回innerP2
    .then(
        // task2
        () => {
        console.log('inner then1');
        // task2执行结束后使innerP2状态变为resolved并起异步任务task4
    })
    // 同步执行,注册回调到innerP2,返回innerP3
    .then(
        // task4
        () => {
        console.log('inner then2');
    });
    // task1执行结束后使outerP2状态变为resolved并起异步任务task3
})
// 同步执行,注册回调到outerP2,返回outerP3
.then(
    // task3
    () => {
    console.log('outer then1');
    // task3执行结束后使outerP3状态变为resolved并起异步任务task5
})
// 同步执行,注册回调到outerP3,返回outerP4
.then(
    // task5
    () => {
    console.log('outer then2');
});