Promise 源码:同步执行 resolve

7,615 阅读4分钟

前言

在上一篇《Promise 源码:实现一个简单的 Promise》当中,我们实现了一个可以简单可用的 Promise。但它实际上还是有不少的缺陷的,比如:

  1. Promise 构造函数里直接同步 resolve,则执行不到 then。
  2. 只有 resolve,没有 reject。
  3. 一些极端的情况没有考虑到。

接下来通过阅读 github 上 then/promise 项目源码来学习学习 Promise 的实现原理。

本篇主要解决第一个问题,即同步执行 resolve,也是上篇实现的代码中忽略的问题:在执行 resolve 之后,没能执行到 then 的回调函数。

new Promise(function (resolve) {
  resolve(1);
}).then(function (val) {
  console.log(val);
});

注:本次阅读的是 then/promise 的 3.0.0 版本,源码请戳 这里

解读

then/promise 项目中 Promise 的代码实现只在 index.js 文件,而且不到 100 行代码,阅读起来也没那么的困难。

nextTick

首先热身一下,先来看看这一端代码:

var nextTick

if (typeof setImmediate === 'function') { // IE >= 10 & node.js >= 0.10
  nextTick = function(fn){ setImmediate(fn) }
} else if (typeof process !== 'undefined' && process && typeof process.nextTick === 'function') { // node.js before 0.10
  nextTick = function(fn){ process.nextTick(fn) }
} else {
  nextTick = function(fn){ setTimeout(fn, 0) }
}

这一段代码跟 js 的事件循环有关,在执行上下文栈为空时,才会去执行以上的任务队列。这里做判断主要是兼容 node 和浏览器,可以把以上代码就当成 setTimeout。

Promise

热身完毕,接下来看的是 Promise 的实现。首先代码写的一个构造函数,所有的代码实现都在其构造函数里,然后暴露出一个 then 函数。

function Promise(fn) {
    // ...
    
    then.then = function() {
        // ...
    }
    
    // ...
}

构造函数里定义了许许多多的变量和函数,暂时不一一解释,用到的时候再解释。

我们先来看看同步与异步的两种执行步骤:

// 同步
new Promise(function (resolve) {
  resolve(1);
}).then(function (val) {
  console.log(val);
});

// 异步
new Promise(function (resolve) {
  setTimeout(function () {
    resolve(1);
  }, 1000);
}.then(function (val) {
  console.log(val);
});

同步和异步的时候,函数的执行顺序是不一样的。

constructor -> fn --同步--> resolve(reject) -> then -> then 回调
constructor -> fn --异步--> then -> resolve(reject) -> then 回调

同步

先从同步的使用方式入手,它会先执行 resolve,接着执行 then 函数,最后执行 then 回调函数。

new Promise(function (resolve) {
  resolve(1);
}).then(function (val) {
  console.log(val);
});

从 fn 的执行开始看吧,fn 执行失败会 catch 调用 reject:

try { fn(resolve, reject) }
catch(e) { reject(e) }

resolve

然后开始执行 resolve 函数,将 1 作为参数传递。来看看 resolve 的实现,先把一些暂时不用到的代码去掉:

function resolve(newValue) {
  resolve_(newValue)
}

function resolve_(newValue) {
  if (state !== null)
    return
  try {
    state = true
    value = newValue
    finale()
  } catch (e) { reject_(e) }
}

这里用到了 state 和 value 两个变量,还调用了一个 finale 函数。

state 是用来记住状态的一个变量,执行 resolve 成功赋值 true,执行 reject 成功赋值 false,否则为 null。该变量只要是用来防止多次 resolve,只要调用代码调用了 resolve,后面的 resolve 一律无效。

value 用来保存 resolve 执行时传入的参数,以便后面 then 的回调时能取到。

finale 暂时忽略,因为同步执行的时候,finale 里的代码几乎是不执行的。

then

执行完 resolve 函数,接着执行 then 函数:

this.then = function(onFulfilled, onRejected) {
  return new Promise(function(resolve, reject) {
    handle({ onFulfilled: onFulfilled, onRejected: onRejected, resolve: resolve, reject: reject })
  })
}

先暂时不管返回了 Promise 实例,这个是为了后面的 then 链式调用。这里 then 函数执行后,会将 then 的回调函数 onFulfilled 和 onRejected 作为参数传入 handle 函数之中。onFulfilled 是 resolve 成功后的回调,onRejected 是 reject 成功后的回调。

handle

跳到 handle 函数:

function handle(deferred) {
  nextTick(function() {
    var cb = state ? deferred.onFulfilled : deferred.onRejected
    if (typeof cb !== 'function'){
      (state ? deferred.resolve : deferred.reject)(value)
      return
    }
    var ret
    try {
      ret = cb(value)
    }
    catch (e) {
      deferred.reject(e)
      return
    }
    deferred.resolve(ret)
  })
}

handle 函数最主要的就是执行 cb 的代码。其它的代码都是在做判断,判断 cb 的类似是否是函数。所以 handle 函数在此时就是执行 then 回调函数,将之前 resolve 存的 value 作为参数传递。

这里的 nextTick 几乎是无效的,因为代码是同步执行的,它会在异步的时候发挥作用的。最后执行的 deferred.resolve(ret) 也是为了实现 then 链接调用,在此时执行与执行没多大的区别。

到这里,Promise 的同步就执行完毕了,再次回顾一下执行顺序:

constructor -> fn --同步--> resolve(reject) -> then -> then 回调

总结

古往今来同步执行的代码都比较好理解,毕竟是按顺序执行的。再看一遍 Promise 执行同步代码:

new Promise(function (resolve) {
  resolve(1);
}).then(function (val) {
  console.log(val);
});

执行同步代码时,会先执行 resolve,会用变量 value 来保存传递的参数,再用 statue 变量来保存状态,第一是防止多次 resolve,第二是通过它来判断回调 onFulfilled(成功回调) 还是 onRejected(失败回调)。

执行的顺序如下:

constructor -> fn --同步--> resolve(reject) -> then -> then 回调

同时,Promise 的实现代码里用到了许多的 try catch,一旦报错 catch 到,就会执行 reject 而不是 resolve,所以 Promise 一般不会脚本报错,而是回调 reject 函数,这点是需要注意的。