探究 JavaScript Promises 的详细实现

写在前面:编程

这是一篇总结文章,但也能够理解为是一篇翻译,主体脉络参考自下面这篇文章:json

www.mattgreer.org/articles/pr…数组

若英文阅读无障碍,墙裂推荐该文章的阅读。promise

前言

在平常写代码的过程当中,我很常常会用到 promises 语法。当我自觉得了解 promises 详细用法时,却在一次讨论中被问住了:“你知道 promises 内部的实现过程是怎样的么?” 是的,回想起来,我只是知道该如何使用它,殊不知道其内部真正的实现原理。这篇文章正是我本身的关于 promises 的回顾与总结。若是你看完了整篇文章,但愿你也会更加理解 promises 的实现与原理。浏览器

咱们将会从零开始,逐步实现一个本身的 promises。最终的代码将会和 Promises/A+ 规范类似,而且将会明白 promises 在异步编程中的重要性。固然,本文会假设读者已经拥有了关于 promises 的基础知识。bash

最简单的 Promises

让咱们从最简单的 promises 实现开始吧。当咱们想要将下面的代码闭包

doSomething(function(value) {
  console.log('Got a value:' + value);
});
复制代码

转变为异步

doSomething().then(function(value) {
  console.log('Got a value:' + value);
});
复制代码

这个时候,咱们须要怎么作呢?很是简单的方式就是,将原来的 doSomething()函数从原来的写法异步编程

function doSomething(callback) {
  var value = 42;
  callback(value);
}
复制代码

转变为以下这种 'promise' 写法:函数

function doSomething() {
  return {
    then: function(callback) {
      var value = 42;
      callback(value);
    }
  };
}
复制代码

上面只是一个 callback 写法的一种语法糖包装而已,看起来毫无心义。不过,这是个很是重要的转变,咱们已经开始触达了 promises 的一个核心理念:

Promises 捕获最终值( eventual values ),并将其放入到一个 Object 中。

Ps: 这里有必要解释“最终值”的概念。它是异步函数的返回值,状态是不肯定的,有可能成功,也有可能失败(以下图)。

eventual value

关于 Promises 与最终值( eventual values ),下文会包含更多的讨论。

定义一个简单的 Promise 函数

上面简单的改写并不足以对 promise 的特性作任何的说明,让咱们来定义一个真正的 promise 函数吧:

function Promise(fn) {
  var callback = null;
  this.then = function(cb) {
    callback = cb;
  };

  function resolve(value) {
    callback(value);
  }

  fn(resolve);
}
复制代码

代码解析:将then的写法拆分,同时引入了resolve函数,方便处理 Promise 的传入对象(函数)。同时,使用callback做为沟通then函数与resolve函数的桥梁。这个代码实现,有一点 Promise 该有的样子了,不是么?

在此基础上,咱们的doSomething()函数将会写成这种形式:

function doSomething() {
  return new Promise(function(resolve) {
    var value = 42;
    resolve(value);
  });
}
复制代码

当咱们尝试执行的时候,会发现执行会报错。这是由于,在上面的代码实现中,resolve()会比then更早被调用,此时的callback仍是null。为了解决这个问题,咱们使用setTimeout的方式 hack 一下:

function Promise(fn) {
  var callback = null;
  this.then = function(cb) {
    callback = cb;
  };

  function resolve(value) {
    // 强制此处的 callback 在 event loop 的下一个
    // 迭代中调用,这样 then()将会在其以前执行
    setTimeout(function() {
      callback(value);
    }, 1);
  }

  fn(resolve);
}
复制代码

通过这样的修改以后,咱们的代码将能够成功运行。

这样的代码糟糕透了

咱们设想的实现,是能够在异步状况下也能够正常工做的。可是此时的代码,是很是脆弱的。只要咱们的then()函数中包含有异步的状况,那么变量callback将会再次变成null。既然这个代码这么渣渣,为何还要写下来呢?由于上面的模式很方便咱们待会的拓展,同时,这个简单的写法,也可让大脑对thenresolve的工做方式有一个初步的了解。下面咱们考虑在此基础上作必定的改进。

Promises 拥有状态

Promises 是拥有状态的,咱们须要先了解 Promises 中都有哪些状态:

一个 promise 在等待最终值的时候,将会是 pending 状态,当获得最终值的时候,将会是 resolved 状态。

当一个 promise 成功获得最终值的时候,它将会一直保持这个值,不会再次 resolve。

(固然,一个 promise 的状态也能够是 rejected,下文会细述)

为了将状态引入到咱们的代码实现中,咱们将原来的代码改写为下面:

function Promise(fn) {
  var state = 'pending';
  //value 表示经过resolve函数传递的参数
  var value;
  //deferred 用于保存then()里面的函数参数
  var deferred;

  function resolve(newValue) {
    value = newValue;
    state = 'resolved';

    if(deferred) {
      handle(deferred);
    }
  }

  function handle(onResolved) {
    if(state === 'pending') {
      deferred = onResolved;
      return;
    }

    onResolved(value);
  }

  this.then = function(onResolved) {
    handle(onResolved);
  };

  fn(resolve);
}
复制代码

这个代码看起来更加复杂了。不过此时的代码可让调用方任意调用then()方法,也能够任意使用resolve()方法了。它也能够同时运行在同步、异步的状况下。

代码解析:代码中使用了state这个flag。同时,then()resolve()将公共的逻辑提取到了一个新的函数handle()中:

  • then()resolve()更早被调用的时候,此时的状态是 pending,对应的 value 值并无准备好。咱们将then()里面对应的回调参数保存在 deferred 中,方便 promise 在获取到 resolved 的时候调用。
  • resolve()then()更早被调用的时候,此时的状态设置为 resolved,对应的 value 值也已经获得。当then()被调用的时候,直接调用then()里面对应的回调参数便可。
  • 因为then()resolve()将公共的逻辑提取到了一个新的函数handle()中,所以无论上面的两个 case 谁被触发,最终都会执行 handle 函数。

若是你仔细看会发现,此时的setTimeout已经不见了。咱们经过 state 的状态控制,已经获得了正确的执行顺序。固然,下面的文章中,还有会使用到setTimeout的时候。

经过使用 promise,咱们调用对应方法的顺序将不会受到任何影响。只要符合咱们的需求,在任什么时候刻调用resolve()then()都不会影响其内部逻辑。

此时,咱们能够尝试屡次调用then方法,会发现每一次获得的都是相同的 value 值。

var promise = doSomething();

promise.then(function(value) {
  console.log('Got a value:', value);
});

promise.then(function(value) {
  console.log('Got the same value again:', value);
});
复制代码

链式 Promises

在咱们平常针对 promises 的编程中,下面的链式模式是常见的:

getSomeData()
.then(filterTheData)
.then(processTheData)
.then(displayTheData);
复制代码

getSomeData()返回的是一个 promise,此时能够经过调用then()方法。但值得注意的是,第一个then()方法的返回值也必须是一个 promise,这样才可让咱们的链式 promises 一直延续下去。

then()方法必须永远返回一个 promise。

为了实现这个目的,咱们将代码作进一步的改造:

function Promise(fn) {
  var state = 'pending';
  var value;
  var deferred = null;

  function resolve(newValue) {
    value = newValue;
    state = 'resolved';

    if(deferred) {
      handle(deferred);
    }
  }

  function handle(handler) {
    if(state === 'pending') {
      deferred = handler;
      return;
    }

    if(!handler.onResolved) {
      handler.resolve(value);
      return;
    }

    var ret = handler.onResolved(value);
    handler.resolve(ret);
  }

  this.then = function(onResolved) {
    return new Promise(function(resolve) {
      handle({
        onResolved: onResolved,
        resolve: resolve
      });
    });
  };

  fn(resolve);
}
复制代码

呼啦~ 如今的代码让人看起来彷佛有点抓狂😩。哈哈哈,你是否会庆幸一开始的时候咱们代码不是那么复杂呢?这里面真正的一个关键点在于:then()方法永远返回一个新的 promise。

doSomething().then(function(result){
  console.log("first result : ", result);
  return 88;
}).then(function(secondResult){
  console.log("second result : ", secondResult);
  return 99;
})
复制代码

让咱们来详细看看第二个 promise 的 resolve 过程。它接收来自第一个 promise 的 value 值。详细的过程发生在 handle()方法的底部。入参handler带有两个参数:一个是 onResolved回调,一个是对resolve()方法的引用。在这里,每个新的 promise 都会有一个对内部方法resolve()的拷贝以及对应的运行时闭包。这是链接第一个 promise 与第二个 promise 的桥梁。

在代码中,咱们能够获得第一个 promise 的 value 值:

var ret = handler.onResolved(value);
复制代码

在上面的例子中,handler.onResolved表示的是:

function(result){
  console.log("first result : ", result);
  return 88;
}
复制代码

也就是说,handler.onResolved实际上返回的是第一个 promise 的 then 被调用时候的传入参数(函数)。第一个 handler 的返回值被用于第二个 promise 的 resolve 传入参数。

这就是整个链式 promise 的工做方式。

若是咱们想要将全部的 then 返回的结果,该怎么作呢?咱们可使用一个数组,来存放每一次的返回值:

doSomething().then(function(result) {
  var results = [result];
  results.push(88);
  return results;
}).then(function(results) {
  results.push(99);
  return results;
}).then(function(results) {
  console.log(results.join(', ');
});

// the output is
//
// 42, 88, 99
复制代码

promises 永远 resolve 返回的是一个值。当你想要返回多个值的时候,能够经过建立某些符合结构来实现(如数组、object等)。

then 中的传入参数是可选的

then() 中的传入参数(回调函数)是并非必填的。若是为空,在链式 promise 中,将会返回前一个 promise 的返回值。

doSomething().then().then(function(result) {
  console.log('got a result', result);
});

// the output is
//
// got a result 42
复制代码

你能够查看handle()中的实现方式,当前一个 promise 没有 then 的传入参数的时候,它会 resolve 前一个 promise 的value 值:

if(!handler.onResolved) {
  handler.resolve(value);
  return;
}
复制代码

在链式 promise 中返回新的 promise

咱们的链式 promise 实现,依然显得有些简单。这里的 resolve 返回的是一个简单的值。假如想要 resolve 返回的是一个新的 promise 呢?好比下面的方式:

doSomething().then(function(result) {
  // doSomethingElse 返回的是一个promise
  return doSomethingElse(result);
}).then(function(finalResult) {
  console.log("the final result is", finalResult);
});
复制代码

若是是这样的状况,那么咱们上面的代码彷佛没法应对这样的状况。对于紧随其后的那个 promise 而言,它获得的 value 值将会是一个 promise。为了获得预期的值,咱们须要这样作:

doSomething().then(function(result) {
  // doSomethingElse 返回的是一个promise
  return doSomethingElse(result);
}).then(function(anotherPromise) {
  anotherPromise.then(function(finalResult) {
    console.log("the final result is", finalResult);
  });
});
复制代码

OMG... 这样的实现实在是太糟糕了。难道做为使用者,我还要每一次都须要本身来手动书写这些冗余的代码么?是否能够在 promise 代码内部处理一下这些逻辑呢?实际上,咱们只须要在已有代码中的 resolve()中增长一点判断便可:

function resolve(newValue) {
  if(newValue && typeof newValue.then === 'function') {
    newValue.then(resolve);
    return;
  }
  state = 'resolved';
  value = newValue;

  if(deferred) {
    handle(deferred);
  }
}
复制代码

上面的代码逻辑中咱们看到,resolve()中若是遇到的是 promise,将会一直迭代调用resolve()。直到最后得到的值再也不是一个 promise,才会依照已有的逻辑继续执行。

还有一个值得注意的点:看看代码中是如何断定一个对象是否是具备 promise 属性的?经过断定这个对象是否有then方法。这种断定方法被称为 "鸭子类型"(咱们并不关心对象是什么类型,究竟是不是鸭子,只关心行为)。

这种宽松的界定方式,可使得具体的不一样 promise 实现彼此之间有一个很好地兼容。

Promises 的 rejecting

在链式 promise 章节中,咱们的实现已经相对而言是很是完整的。可是咱们并无讨论到 promises 中的错误处理。

在 promise 的决议过程当中,若是发生了错误,那么 promise 将会抛出一个拒绝决议,同时给出对应的理由。对于调用者,怎么知道错误发生了呢?能够经过 then()方法的第二个传入参数(函数):

doSomething().then(function(value) {
  console.log('Success!', value);
}, function(error) {
  console.log('Uh oh', error);
});
复制代码

正如上面提到的,一个 promise 会从初始状态 pending 转换为要么是resolved 状态,要么是 rejected 状态。这二者,只能有一个做为最终的状态。对应到then()的两个参数,只有一个会被真正执行。

在 promise 内部实现中,一样容许有一个reject()函数来处理 reject 状态,能够看作是 resolve()函数的孪生兄弟。此时,doSomething()函数也将会被改写为支持错误处理的方式:

function doSomething() {
  return new Promise(function(resolve, reject) {
    var result = somehowGetTheValue();
    if(result.error) {
      reject(result.error);
    } else {
      resolve(result.value);
    }
  });
}
复制代码

对于此,咱们的代码该作如何的对应改造呢?来看代码:

function Promise(fn) {
  var state = 'pending';
  var value;
  var deferred = null;

  function resolve(newValue) {
    if(newValue && typeof newValue.then === 'function') {
      newValue.then(resolve, reject);
      return;
    }
    state = 'resolved';
    value = newValue;

    if(deferred) {
      handle(deferred);
    }
  }

  function reject(reason) {
    state = 'rejected';
    value = reason;

    if(deferred) {
      handle(deferred);
    }
  }

  function handle(handler) {
    if(state === 'pending') {
      deferred = handler;
      return;
    }

    var handlerCallback;

    if(state === 'resolved') {
      handlerCallback = handler.onResolved;
    } else {
      handlerCallback = handler.onRejected;
    }

    if(!handlerCallback) {
      if(state === 'resolved') {
        handler.resolve(value);
      } else {
        handler.reject(value);
      }

      return;
    }

    var ret = handlerCallback(value);
    handler.resolve(ret);
  }

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

  fn(resolve, reject);
}
复制代码

代码解析:不只仅新增了一个reject()函数,并且handle()方法内部也增长了对 reject的逻辑处理:经过对state的判断,来决定具体执行handlerreject/resolved

不可知的错误,一样应该引起rejection

上面的代码,只对已知的错误进行了处理。当发生某些不可知错误的时候,一样应该引起 rejection。须要在对应的处理函数中增长try...catch

首先是在resolve()方法中:

function resolve(newValue) {
  try {
    // ... as before
  } catch(e) {
    reject(e);
  }
}
复制代码

一样的,在 handle()执行具体 callback的时候,也可能发生未知的错误:

function handle(handler) {
  // ... as before

  var ret;
  try {
    ret = handlerCallback(value);
  } catch(e) {
    handler.reject(e);
    return;
  }

  handler.resolve(ret);
}
复制代码

Promises 会吞下错误

有时候,对于 promises 的错误解读,将会致使 promises 吞下错误。这是个常常坑开发者的点。

让咱们来考虑这个例子:

function getSomeJson() {
  return new Promise(function(resolve, reject) {
    var badJson = "<div>uh oh, this is not JSON at all!</div>";
    resolve(badJson);
  });
}

getSomeJson().then(function(json) {
  var obj = JSON.parse(json);
  console.log(obj);
}, function(error) {
  console.log('uh oh', error);
});
复制代码

这段代码将会如何进行呢?在then()中的 resolve 执行的是对 JSON 的解析。它觉得可以执行,结果却抛出了异常,由于传入的 value 值并非 JSON 格式。咱们写了一个 error callback 来捕获这个错误。这样是没有问题,对吧?

不,结果可能并不符合你的指望。此时的 error callback 并不会触发。结果将会是:控制台上没有任何的 log 输出。这个错误就这样被平静地吞掉了。

为何会这样?由于咱们的错误发生在then()的 resolve 回调内部,源码上看是发生在 handle()方法内部。这将会致使的是,then()返回的新的 promise 将会被触发 reject,而不是现有的这个 promise 会触发 reject:

function handle(handler) {
  // ... as before

  var ret;
  try {
    ret = handlerCallback(value);
  } catch(e) {
  	// 到达这里,触发的是handler.reject()
  	// 这是then()返回的新的promise的reject()
  	// 若是改为 handler.onRejected(ex),将会触发本promise的reject()
    handler.reject(e);
    return;
  }

  handler.resolve(ret);
}
复制代码

若是将上面代码中的catch部分改写成:handler.onRejected(ex);将会触发的是本 promise 的reject()。但这就违背了 promises 的原则:

一个 promise 会从初始状态 pending 转换为要么是 resolved 状态,要么是 rejected 状态。这二者,只能有一个做为最终的状态。对应到then()的两个参数,只有一个会被真正执行。

由于已经触发了 resolved 状态,那么久不可能再次触发 rejected 状态。错误是在具体执行 resolved 函数的时候发生的,那么这个 error,将会被下一个 promise 捕获。

咱们能够这样验证:

getSomeJson().then(function(json) {
  var obj = JSON.parse(json);
  console.log(obj);
}).then(null, function(error) {
  console.log("an error occured: ", error);
});
复制代码

这多是 promises 中最坑人的一个点了。固然,只要理解了其中的原因,那么就能够很好地避免。为了更好地体验,咱们有什么解决方法来规避这个坑呢?请看下一节:

done()来帮忙

大部分的 promise 库都包含有一个 done()方法。它实现的功能和then()方法类似,只是很好的规避了刚刚提到的then()的坑。

done()方法能够像then()那样被调用。二者之间主要有两点不一样:

  • done()方法返回的不是一个 promise
  • done()中的任何错误将不会被 promise 实现捕获(直接抛出)

在咱们的例子中,若是使用done()方法,将会更加保险:

getSomeJson().done(function(json) {
  // when this throws, it won't be swallowed
  var obj = JSON.parse(json);
  console.log(obj);
});
复制代码

从rejection中恢复

从 promise 中的 rejection 恢复是有可能的。若是在一个包含有 rejection 的 promise 中增长更多的then()方法,那么从这个then() 开始,将会延续链式 promise 的正常处理流程:

aMethodThatRejects().then(function(result) {
  // won't get here
}, function(err) {
  // since aMethodThatRejects calls reject()
  // we end up here in the errback
  return "recovered!";
}).then(function(result) {
  console.log("after recovery: ", result);
}, function(err) {
  // we won't actually get here
  // since the rejected promise had an errback
});

// the output is
// after recovery: recovered!
复制代码

Promise 决议必须是异步的

在本文的开头,咱们使用了一个 hack 来让咱们的简单代码可以正确容许。还记得么?使用了一个 setTimeout。当咱们完善了对应的逻辑以后,这个 hack 就没有再使用了。但事实是:Promises/A+ 规范要求 promise 决议必须是一步的。为了实现这个需求,最简单的作法就是再次使用 setTimeout将咱们的handle()方法包装一层:

function handle(handler) {
  if(state === 'pending') {
    deferred = handler;
    return;
  }
  setTimeout(function() {
    // ... as before
  }, 1);
}
复制代码

很是简单的实现。可是,实际上的 promises 库并不倾向于使用setTimeout。若是对应的库是用于 NodeJS,那么它们倾向于使用 process.nextTick,若是对应的库是用于浏览器,那么它们倾向于使用setImmediate

为何

具体的作法咱们知道了,可是为何规范中会有这样的要求呢?

为了确保一致性与可信赖的执行过程。让咱们考虑这样的状况:

var promise = doAnOperation();
invokeSomething();
promise.then(wrapItAllUp);
invokeSomethingElse();
复制代码

上面的代码会被怎样执行呢?基于命名,你可能设想这个执行过程会是这样的:invokeSomething() -> invokeSomethingElse() -> wrapItAllUp()。但实际上,这取决于在咱们当前的实现过程当中,promise 的 resolve 过程是同步的仍是异步的。若是doAnOperation()的 promise 执行过程是异步的,那么其执行过程将会是设想的流程。若是doAnOperation()的 promise 执行过程是同步的,它真实的执行过程将会是invokeSomething() -> wrapItAllUp() -> invokeSomethingElse()。这时,可能会致使某些意想不到的后果。

所以,为了确保一致性与可信赖的执行过程。promise 的 resolve 过程被要求是异步的,即便自己可能只是简单的同步过程。这样作,可让全部的使用体验都是一直的,开发者在使用过程当中,也再也不须要担忧各类不一样的状况的兼容。

结论

若是读到了这里,那么能够肯定是真爱了!咱们将 promises 的核心概念都讲了一遍。固然,文章中的代码实现,大部分都是简陋的。可能也会和真正的代码库实现有必定的出入。但但愿不妨碍您对总体 promises 的理解。更多的关于 promises 的实现细节(如:all()race等),能够查看更多的文档与源码实现。

当真正理解了 promises 的工做原理以及它的一些边界状况,我才真正喜欢上它。今后个人项目中关于 promises 的代码也变得更加简洁。关于 promises,还有不少内容值得去探讨,本文只是一个开始。

相关文章
相关标签/搜索