Promise使用与实现学习笔记

Promise是一个对象,它代表了一个异步操作的最终完成或者失败。因为大多数人仅仅是使用已创建的Promise实例对象,所以本教程将首先说明怎样使用Promise,再说明如何创建Promise

本质上,Promise是一个被某些函数传出的对象,我们附加回调函数(callback)使用它,而不是将回调函数传入那些函数内部。

假设现在有一个名为createAudioFileAsync()的函数,如果给出一些配置和两个回调函数,这个函数能异步地生成音频文件。一个回调函数是文件成功创建时的回调,另一个则是出现异常时的回调。

以下为使用createAudioFileAsync()的示例:

// 成功的回调函数
function successCallback(result) {
  console.log("音频文件创建成功: " + result);
}

// 失败的回调函数
function failureCallback(error) {
  console.log("音频文件创建失败: " + error);
}

createAudioFileAsync(audioSettings, successCallback, failureCallback)

最新的方式就是返回一个promise对象,使得你可以将你的回调函数绑定在该 promise 上:

如果函数 createAudioFileAsync() 被重写为返回 Promise 对象的形式,就可以像这样简单地使用:

const promise = createAudioFileAsync(audioSettings); 
promise.then(successCallback, failureCallback);

简写为:

createAudioFileAsync(audioSettings).then(successCallback, failureCallback);

我们把这个称为异步函数调用,这种形式有若干优点。下面我们将会逐一讨论这些优点。

约定

不同于“老式”的传入回调,在使用 Promise 时,会有以下约定:

  • 在 本轮 Javascript event loop(事件循环)运行完成之前,回调函数是不会被调用的。
  • 通过 then() 添加的回调函数总会被调用,即便它是在异步操作完成之后才被添加的函数。
  • 通过多次调用 then(),可以添加多个回调函数,它们会按照插入顺序一个接一个独立执行。

因此,Promise 最直接的好处就是链式调用(chaining)。

链式调用

连续执行两个或者多个异步操作是一个常见的需求,在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。我们可以通过创造一个 Promise 链来实现这种需求。

见证奇迹的时刻:then() 函数会返回一个全新的 Promise,和原来的不同:

const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);

或者

const promise2 = doSomething().then(successCallback, failureCallback);

第二个对象(promise2)不仅表示 doSomething() 函数的完成,也代表了你传入的 successCallback 或者 failureCallback 的完成,successCallback 或 failureCallback 有可能返回一个 Promise 对象,从而形成另一个异步操作。这样的话,任何一个 promise2 新增的回调函数都会被依次排在由上一个successCallback 或 failureCallback 执行后所返回的 Promise 对象的后面。

基本上,每一个 Promise 都代表了链中另一个异步过程的完成。

在过去,要想做多重的异步操作,会导致经典的回调地狱:

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

通过新的功能方法,我们把回调绑定到被返回的 Promise 上代替以往的做法,形成一个 Promise 链:

doSomething().then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

then里的参数是可选的,catch(failureCallback) 是 then(null, failureCallback) 的缩略形式。如下所示,也可以用 箭头函数 来表示:

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);

注意:一定要有返回值,否则,callback 将无法获取上一个 Promise 的结果。(如果使用箭头函数,() => x 比 () => { return x; } 更简洁一些,但后一种保留 return 的写法才支持使用多个语句。)

Catch 的后续链式操作

有可能会在一个回调失败之后继续使用链式操作,即 使用一个 catch,这对于在链式操作中抛出一个失败之后,再次进行新的操作很有用。请阅读下面的例子:

new Promise((resolve, reject) => {
    console.log('初始化');

    resolve();
})
.then(() => {
    throw new Error('有哪里不对了');

    console.log('执行“这个”');
})
.catch(() => {
    console.log('执行“那个”');
})
.then(() => {
    console.log('执行“这个”,无论前面发生了什么');
});

输出结果如下:

初始化
执行“那个”
执行“这个”,无论前面发生了什么

注意:因为抛出了错误 有哪里不对了,所以 执行“这个” 没有被输出。

错误传递

在之前的回调地狱示例中,你可能记得有 3 次 failureCallback 的调用,而在 Promise 链中只有尾部的一次调用。

doSomething()
.then(result => doSomethingElse(value))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);

通常,一遇到异常抛出,promise 链就会停下来,直接调用链式中的 catch 处理程序来继续当前执行。这看起来和以下的同步代码的执行很相似。

try {
  let result = syncDoSomething();
  let newResult = syncDoSomethingElse(result);
  let finalResult = syncDoThirdThing(newResult);
  console.log(`Got the final result: ${finalResult}`);
} catch(error) {
  failureCallback(error);
}

在ECMAScript 2017标准的 async/await 语法糖中,这种同步形式代码的对称性得到了极致的体现:

async function foo() {
  try {
    let result = await doSomething();
    let newResult = await doSomethingElse(result);
    let finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}

这个例子是在 Promise 的基础上构建的,例如,doSomething() 与之前的函数是相同的。你可以在这里阅读更多的与这个语法相关的文章。

通过捕获所有的错误,甚至抛出异常和程序错误,Promise 解决了回调地狱的基本缺陷。这对于构建异步操作的基础功能而言是很有必要的。

Promise 拒绝事件

当 Promise 被拒绝时,会有下文所述的两个事件之一被派发到全局作用域(通常而言,就是window;如果是在web worker中使用的话,就是 Worker 或者其他 worker-based 接口)。这两个事件如下所示:

rejectionhandled

当 Promise 被拒绝、并且在 reject 函数处理该 rejection 之后会派发此事件。

unhandledrejection

当 Promise rejected,但没有提供 reject 函数来处理该 rejection 时,会派发此事件。
以上两种情况中,PromiseRejectionEvent 事件都有两个属性,一个是 promise 属性,该属性指向被驳回的 promise,另一个是 reason 属性,该属性用来说明 promise 被驳回的原因。

因此,我们可以通过以上事件为promise失败时提供补偿处理,也有利于调试 promise 相关的问题。在每一个上下文中,该处理都是全局的,因此不管源码如何,所有的错误都会在同一个handler中被捕捉处理。

一个特别有用的例子:当你写 Node.js代码时,有些依赖模块可能会有未被处理的 rejected promises,这些都会在运行时打印到控制台。你可以在自己的代码中捕捉这些信息然后添加unhandledrejection相应的 handler 来做分析和处理,或者只是为了让你的输出更整洁。举例如下:

window.addEventListener("unhandledrejection", event => {
  /* 你可以在这里添加一些代码,以便检查
     event.promise 中的 promise 和
     event.reason 中的 rejection 原因 */

  event.preventDefault();
}, false);

调用 event 的 preventDefault() 方法是为了告诉 JavaScript 引擎当 promise 被拒绝时不要执行默认操作,默认操作一般会包含把错误打印到控制台。

理想情况下,在忽略这些事件之前,我们应该检查所有被拒绝的 promise,来确认这不是代码中的 bug。

在旧式回调 API 中创建 Promise

可以通过 Primise 的构造器从零开始创建 Promise。 应当只需要用它包裹旧的 API 就可以实现。

理想状态下,所有的异步函数都已经返回 Promise 了。但有一些 API 仍然使用旧式的被传入的成功或者失败的回调。典型的例子就是 setTimeout() 函数:

setTimeout(() => saySomething("10 seconds passed"), 10000);

混用旧式回调和 Promise 是会造成问题的。如果 saySomething 函数失败了或者包含了编程错误,那就没有办法捕获它了。这得怪 setTimeout。

幸运的是我们可以用 Promise 来包裹它。最好的做法是将有问题的函数包装在最低级别,并且永远不要再直接调用它们:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

通常,Promise 的构造器允许我们手动操作 resolve 和 reject 的执行函数。既然 setTimeout 没有真的执行失败,那么我们可以在这种情况下忽略 reject。

组合

Promise.resolve()Promise.reject() 是手动创建一个已经 resolve 或者 reject 的 promise 快捷方法。它们有时很有用。

Promise.all()Promise.race() 是并行运行异步操作的两个组合式工具。

我们可以发起并行操作,然后等多个操作全部结束后进行下一步操作,如下:

Promise.all([func1(), func2(), func3()])
.then(([result1, result2, result3]) => { /* use result1, result2 and result3 */ });

可以使用一些聪明的 JavaScript写法实现时序组合:

[func1, func2, func3].reduce((p, f) => p.then(f), Promise.resolve())
.then(result3 => { /* use result3 */ });

通常,我们递归调用一个由异步函数组成的数组时相当于一个 Promise 链:

Promise.resolve().then(func1).then(func2).then(func3);

我们也可以写成可复用的函数形式,这在函数式编程中极为普遍:

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

composeAsync 函数将会接受任意数量的函数作为其参数,并返回一个新的函数,该函数接受一个通过 composition pipeline 传入的初始值。这对我们来说非常有益,因为任一函数可以是异步 或同步的,它们能被保证按顺序执行:

const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);

在 ECMAScript 2017 标准中, 时序组合可以通过使用 async/await 而变得更简单:

let result;
for (const f of [func1, func2, func3]) {
  result = await f(result);
}
/* use last result (i.e. result3) */

时序

为了避免意外,即使是一个已经变成 resolve 状态的 Promise,传递给 then() 的函数也总是会被异步调用:

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

传递到 then() 中的函数被置入了一个微任务队列,而不是立即执行,这意味着它是在 JavaScript 事件队列的所有运行时结束了,事件队列被清空之后才开始执行:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

嵌套

简便的 Promise 链式编程最好保持扁平化,不要嵌套 Promise,嵌套经常会是粗心导致的。可查阅下一节的常见错误中的例子。

嵌套 Promise 是一种可以限制 catch 语句的作用域的控制结构写法。明确来说,嵌套的 catch 仅捕捉在其之前同时还必须是其作用域的 failureres,而捕捉不到在其链式以外或者其嵌套域以外的 error。如果使用正确,那么可以实现高精度的错误修复。

doSomethingCritical()
.then(result => doSomethingOptional()
  .then(optionalResult => doSomethingExtraNice(optionalResult))
  .catch(e => {console.log(e.message)})) // 即使有异常也会忽略,继续运行;(最后会输出)
.then(() => moreCriticalStuff())
.catch(e => console.log("Critical failure: " + e.message));// 没有输出

注意,有些代码步骤是嵌套的,而不是一个简单的纯链式,这些语句前与后都被()包裹着。

这个内部的 catch 语句仅能捕获到 doSomethingOptional() 和 doSomethingExtraNice() 的失败,而且还是在moreCriticalStuff() 并发运行以后。重要提醒:如果 doSomethingCritical() 失败,这个错误才仅会被最后的(外部)catch 语句捕获到。

常见错误

在编写 Promise 链时,需要注意以下示例中展示的几个错误:

// 错误示例,包含 3 个问题

doSomething().then(function(result) {
  doSomethingElse(result) // 没有返回 Promise 以及没有必要的嵌套 Promise
  .then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());

// 最后是没有使用 catch 终止 Promise 调用链,可能导致没有捕获的异常
第一个错误是没有正确地将事物相连接。当我们创建新 Promise 但忘记返回它时,会发生这种情况。因此,链条被打破,或者更确切地说,我们有两个独立的链条竞争(同时在执行两个异步而非一个一个的执行)。这意味着 doFourthThing() 不会等待 doSomethingElse() 或doThirdThing() 完成,并且将与它们并行运行,可能是无意的。单独的链也有单独的错误处理,导致未捕获的错误。

第二个错误是不必要地嵌套,实现第一个错误。嵌套还限制了内部错误处理程序的范围,如果是非预期的,可能会导致未捕获的错误。其中一个变体是 promise 构造函数反模式,它结合了 Promise 构造函数的多余使用和嵌套。

第三个错误是忘记用 catch 终止链。这导致在大多数浏览器中不能终止的 Promise 链里的 rejection。

一个好的经验法则是总是返回或终止 Promise 链,并且一旦你得到一个新的 Promise,返回它。下面是修改后的平面化的代码:

doSomething()
.then(function(result) {
  return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing());
.catch(error => console.log(error));

注意:() => x 是 () => { return x; }的简写。

上述代码的写法就是具有适当错误处理的简单明确的链式写法。

使用 async/await 可以解决以上大多数错误,使用 async/await 时最常见的错误就是忘记了await 关键字。

实现

根据promise/A+规范,我们可以用JavaScript实现一个Promise对象,源码如下:

/**
 * Promise实现 遵循promise/A+规范
 * @see https://promisesaplus.com/
 */

var PENDING = 'pending';
var FULFILLED = 'fulfilled';
var REJECTED = 'rejected';

function Promise(executor) {
  var that = this;

  // 初始状态
  that.status = PENDING;
  // fulfilled状态时 返回的值
  that.value = undefined;
  // rejected状态时 拒绝的原因
  this.reason = undefined;
  // 存储fulfilled状态对应的onFulfilled函数
  this.onFulfilledFns = [];
  // 存储rejected状态对应的onRejected函数
  this.onRejectedFns = [];

  function resolve(value) {
    if (value instanceof Promise) {
      return value.then(resolve, reject);
    }

    if (that.status === PENDING) {
      that.status = FULFILLED;
      that.value = value;
      that.onFulfilledFns.forEach(function(cb) {
        cb(that.value);
      });
    }
  }

  function reject(reason) {
    if (that.status === PENDING) {
      that.status = REJECTED;
      this.reason = reason;
      that.onRejectedFns.forEach(function(cb) {
        cb(that.reason);
      });
    }
  }

  // 捕获在excutor执行器中抛出的异常
  try {
    executor(resolve, reject);
  } catch (e) {
    reject(e);
  }
}

/**
 * 2.2 Promise必须提供一个then方法来获取它当前的值或者拒绝原因
 * 2.2.1 onFulfilled和onRejected都是可选的参数
 * 
 * 
 */
Promise.prototype.then = function (onFulfilled, onRejected) {
  /**
   * 2.2.2 如果onFulfilled是函数:
   * 2.2.2.1 此函数必须在promise成功(fulfilled)后被调用,并把promise的value作为它的第一个参数。
   * 2.2.2.2 此函数在promise成功(fulfilled)之前不可被调用。
   * 2.2.2.3 此函数不可被调用超过一次。
   */
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;

  /**
   * 2.2.3 如果onRejected是函数:
   * 2.2.3.1 此函数必须在promise拒绝(rejected)后被调用,并把promise的reason作为它的第一个参数。
   * 2.2.3.2 此函数在promise拒绝(rejected)之前不可被调用。
   * 2.2.3.3 此函数不可被调用超过一次。
   */
  onRejected = typeof onRejected === 'function' ? onRejected : reason => {throw reason};

  var that = this;
  var promise2 = null;

  /**
   * 2.2.7 then方法必须返回一个promise对象
   * `promise2 = promise1.then(onFulfilled, onRejected);`
   * 2.2.7.1 如果onFulfilled或onRejected返回一个值x,运行 Promise Resolution Procedure [[Resolve]](promise2, x)
   * 2.2.7.2 如果onFulfilled或onRejected抛出一个异常e,promise2 必须被拒绝(rejected)并把e当作reason
   * 2.2.7.3 如果onFulfilled不是一个函数并且promise1已经完成(fulfilled), promise2 必须成功执行并返回相同的value
   * 2.2.7.4 如果onRejected不是一个函数并且promise1已经被拒绝(rejected), promise2 必须拒绝执行并返回相同的reason
   * 
   */
  // 等待态
  if(that.status === PENDING) {
    return promise2 = new Promise(function(resolve, reject) {
      that.onFulfilledFns.push(function(value) {
        try {
          var x = onFulfilled(value);
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      });

      that.onRejectedFns.push(function(reason) {
        try {
          var x = onRejected(reason);
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      });
    });
  }

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

  // 失败态
  if(that.status == REJECTED) {
    return promise2 = new Promise(function(resolve, reject) {
      try {
        var x = onRejected(this.reason);
        resolvePromise(promise2, x, resolve, reject);
      } catch(e) {
        reject(e);
      }
    });
  }
}

Promise.prototype.catch = function () {

}

Promise.prototype.finally = function () {

}

/**
 * 2.3 The Promise Resolution Procedure
 * 
 * 2.3.1 如果promise和x引用同一个对象,则用TypeError作为原因拒绝(reject)promise。
 */
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    // 如果从onFulfilled中返回的x 就是promise2 就会导致循环引用报错
    reject(new TypeError('Circular reference'));
  }

  var called = false;

  /**
   * 2.3.2 如果x是一个promise,采用promise的状态
   * 2.3.2.1如果 x 处于等待态, promise 需保持为等待态直至 x 被执行或拒绝
   * 2.3.2.2如果 x 处于执行态,用相同的值执行 promise
   * 2.3.2.3如果 x 处于拒绝态,用相同的原因拒绝 promise
   */
  if (x instanceof Promise) {
    if (x.status === PENDING) {
      x.then(y => {
        resolvePromise(promise2, y, resolve, reject);
      }, reason => {
        reject(reason);
      });
    } else {
      x.then(resolve, reject);
    }
  } 
  /**
   * 2.3.3 如果x是个对象或者方法
   * 2.3.3.1把x.then赋值给then
   * 2.3.3.2如果取 x.then 的值时抛出错误 e ,则以 e 为原因拒绝 promise
   * 2.3.3.3如果 then 是函数,将x作为函数的作用域this调用之。传递两个回调函数作为参数,
   * 第一个参数叫做 resolvePromise ,第二个参数叫做 rejectPromise:
   * 2.3.3.3.1 如果/当 resolvePromise被一个值y调用,运行 [[Resolve]](promise2, y)
   * 2.3.3.3.2 如果/当 rejectPromise被一个原因r调用,用r拒绝(reject)promise
   * 2.3.3.3.3 如果resolvePromise和 rejectPromise都被调用,
   * 或者对同一个参数进行多次调用,第一次调用执行,任何进一步的调用都被忽略
   * 2.3.3.3.4 如果调用then抛出一个异常e
   * 2.3.3.3.4.1 如果resolvePromise或 rejectPromise已被调用,忽略。
   * 2.3.3.3.4.2 或者, 用e作为reason拒绝(reject)promise
   * 2.3.3.4 如果then不是一个函数,用x完成(fulfill)promise
   */
  else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
    try {
      let then = x.then;
      if (typeof then === 'function') {
        then.call(x, function(y) {
          if(called) {
            return;
          }
          called = true;
          resolvePromise(promise2, y, resolve, reject);
        }, function(reason) {
          if(called) {
            return;
          }
          called = true;
          reject(reason);
        })
      } else {
        resolve(x);
      }
    } catch(e) {
      if(called) {
        return;
      }
      called = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}

参考

这篇文章目前没有评论。

回复

(必填项)

(必填项)

(可选)