前言

Promise 对象用于表示一个异步操作的最终完成(或失败),及其结果值。它最早由社区提出和实现,其中有多种 Promise 规范。ES6 按照 Promise/A+ 规范将其写进了语言标准。 关于该规范的详情可参考:Promise/A+规范 中文翻译

基础实现

我们先尝试实现最简单的 Promise 功能:通过 Promise 包装异步请求,并使用then方法注册回调函数,通过resolve方法通知 Promise 异步请求已解决,并执行回调函数。 模拟一个基础的异步 http 请求,并使用 Promise 封装:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 模拟http请求
const mockAjax = (url, s, callback) => {
  console.log("[mockAjax] start");
  setTimeout(() => {
    console.log("[mockAjax] callback");
    callback("异步结果:" + url + "异步请求耗时" + s + "秒");
  }, 1000 * s);
};
// Promise基础功能
new Promise((resolve) => {
  mockAjax("getUserId", 1, function (result) {
    resolve(result);
  });
})
  .then((result) => {
    console.log("[then] onFulfilled:", result);
  })
  .then((result) => {
    console.log("[then] onFulfilled:", result);
  });

实现思路

  • then用于注册回调函数,因此可以在 Promise 实例中维护一个回调函数队列。
  • then要求可以进行链式调用,考虑在then方法中return this
  • Promise 的构造方法接受一个[resolve => {}]形式的函数,其中resolve方法接受异步请求的返回值并传递给then注册的回调函数。因此可以通过resolve方法调用回调函数队列中的函数。
  • 这里的思路类似于观察者模式。

依照上述两点,我们可以做最简单的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Promise {
  // 回调队列
  callbacks = [];
  // 构造函数传入需要执行的函数[形式为(resolve) => {}]
  constructor(fn) {
    fn(this._resolve.bind(this));
  }
  // then方法用于注册onFulfilled函数[形式为(value) => {}]
  then(onFulfilled) {
    // 将onFulfilled函数添加到callbacks中
    this.callbacks.push(onFulfilled);
    // 基础链式调用
    return this;
  }
  // resolve被调用时执行注册过的onFulfilled函数
  _resolve(value) {
    this.callbacks.forEach((fn) => fn(value));
  }
}

状态保存

在上面的实现中,存在这样的问题: 在resolve执行之后再通过then注册的回调函数不会执行。例如同步执行resolve,或者resolve之后再次调用then:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Promise
const p = new Promise((resolve) => {
  // 同步执行的resolve,比then先执行
  resolve(result);
  // 此处的回调不会执行
}).then((result) => {
  console.log("[then] onFulfilled:", result);
});

// 此处的回调不会执行
p.then((result) => {
  console.log("[then] onFulfilled:", result);
});

为了解决这个问题,我们需要在 Promise 中保存状态和值。规范中规定,Promise 的状态可以从pending转换为fulfilled或者rejected,分别代表“处理中”、“已解决”、“已失败”。状态转换的过程是不可逆的。修改上文的实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 带基础链式调用功能的简单实现
class Promise {
  // 回调队列
  callbacks = [];
  // Promise实例状态
  state = "pending";
  // Promise的值
  value = null;
  // 构造函数传入需要执行的函数[形式为(resolve) => {}]
  constructor(fn) {
    fn(this._resolve.bind(this));
  }
  // then方法用于注册onFulfilled函数[形式为(value) => {}]
  then(onFulfilled) {
    // 在Promise未解决之前,onFulfilled函数添加到callbacks中
    if (this.state === "pending") {
      this.callbacks.push(onFulfilled);
      // 在Promise解决之后,直接执行onFulfilled函数
    } else {
      onFulfilled(this.value);
    }
    // 基础链式调用
    return this;
  }
  // resolve被调用时执行注册过的onFulfilled函数
  _resolve(value) {
    // 改变状态
    this.state = "fulfilled";
    // 保存结果
    this.value = value;
    this.callbacks.forEach((fn) => fn(value));
  }
}

参考代码:Promise:基础实现

进阶:链式调用

考虑如下一种链式调用的情形:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 在链式调用中传递值
new Promise((resolve) => {
  mockAjax("getUserId", 1, function (result) {
    resolve(result);
  });
})
  .then((result) => {
    console.log("[then] onFulfilled:", result);
    return "第一个then的返回值";
  })
  .then((result) => {
    console.log("[then] onFulfilled:", result);
  });

注意到,第一个then方法最后返回了一个值,并且我们希望在第二个then方法中可以接收到。

链式 Promise

首先,第二个then方法中接受的值与最初 Promise 的值是不同的;其次,Promise 的状态改变是不可逆的,因此我们不能在then方法中重新修改 Promise 的值,这不符合规范。那么只剩下一种可能来实现then的链式调用:then方法最终返回的是一个新的 Promise 实例,并且该实例的值就是第一个then方法中return语句的返回值,比如上例中的字符串"第一个 then 的返回值"

我们就依据上文的示例来理清一下思路:

  • 第一个then方法注册的回调函数应该保存在第一个 Promise 实例之中(代称 p1),并且最终调用该回调函数的也应该是 p1 实例。
  • 在 p1 实例调用回调函数时,需要获得回调函数的返回值,并且传递给下一个 Promise 实例(代称 p2),也就是调用p2.resolve(/*回调函数的返回值*/)
  • p2.then以及之后的链式调用思路是递归的。

理解以上几点之后,我们尝试实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 链式Promise
class Promise {
  // 回调队列
  callbacks = [];
  // Promise实例状态
  state = "pending";
  // Promise的值
  value = null;
  // 构造函数传入需要执行的函数[形式为(resolve) => {}]
  constructor(fn) {
    fn(this._resolve.bind(this));
  }

  // _resolve方法用于改变Promise的状态为已解决,并执行回调队列[形式为(value) => {}]
  _resolve(value) {
    // 改变状态,保存值
    this.state = "fulfilled";
    this.value = value;
    // 依次执行回调队列
    this.callbacks.forEach((fn) => fn());
  }

  // then方法用于注册onFulfilled函数[形式为(value) => {}]
  then(onFulfilled) {
    // 返回了新的 Promise 实例,这样可实现真正的链式调用
    const nextPromise = new Promise((resolve) => {
      if (this.state === "pending") {
        this.callbacks.push(() => {
          this._execCb(onFulfilled, resolve);
        });
        return;
      }

      if (this.state === "fulfilled") {
        this._execCb(onFulfilled, resolve);
        return;
      }
    });

    return nextPromise;
  }

  _execCb(cb, resolve) {
    const x = cb(this.value);
    resolve(x);
  }
}

核心思路就是通过递归实现链式调用,可能不那么容易看懂,这里列出几个要点以帮助理解:

  • _execCb方法负责执行回调函数,并且将回调函数的返回值通过resolve(x)传递给下一个 Promise。
  • then方法中,先创建了新的 Promise,并且在其构造函数中,根据当前 Promise 的状态选择执行回调函数或者将该操作放入回调队列。
  • _resolve方法负责改变 Promise 的状态为已解决,并按顺序执行回调队列。

仔细理解以上三点,再总结一下核心思路:前一个 Promise 执行resolve时,回调队列将被执行,并且回调队列中所执行函数的返回值通过下一个 Promise 的resolve传入。这就是通过链式 Promise 实现链式调用的基础。

当返回值为 Promise

考虑如下一个应用场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 在链式调用中传递Promise
new Promise((resolve) => {
  mockAjax("getUserId", 1, function (result) {
    resolve(result);
  });
})
  .then((result) => {
    console.log("[then] onFulfilled:", result);
    return new Promise((resolve) => {
      resolve("第一个then返回了一个Promise");
    });
  })
  .then((result) => {
    console.log("[then] onFulfilled:", result);
  });

当注册的回调函数返回了一个 Promise 时,我们希望之后注册的回调队列能等待该 Promise 改变状态再执行,这又如何实现呢?在理解上文的基础上,我们理清一下思路:

  • 按照之前的逻辑,第一个then方法将生成一个 Promise(代称 p1),并且回调函数将返回一个 Promise 实例(代称 p2)。
  • 如果 p2 的状态依旧为pending,则需要等待其状态改变,再执行 p1 相应的状态改变方法。

其实这部分内容在 Promise/A+规范:Promise 解决过程 一节中有详细的处理逻辑,这里引用一部分内容:

Promise 解决过程是一个抽象的操作,其需输入一个promise和一个值,我们表示为[[Resolve]](promise, x),如果xthen方法且看上去像一个 Promise ,解决程序即尝试使promise接受x的状态;否则其用x的值来执行promise

更多详细的说明大家可以点击链接查看。

在这里我们先简化问题,只考虑回调函数返回值为 Promise 这一种特殊情况,且不处理 rejected 状态。

首先明白一个概念:在获得回调函数的返回值后,根据该返回值处理下一个 Promise 的过程称之为 Promise 解决过程。在该过程中,判断回调函数返回值为 Promise 时,调用该 Promise 的 then 方法,将此次解决过程注册为回调函数延迟执行。这一段有点绕,我们参考代码理解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Promise解决过程
function resolvePromise(promise, x, resolve) {
  if (x instanceof Promise) {
    const then = x.then;
    then.call(x, (y) => {
      resolvePromise(promise, y, resolve);
    });
    return;
  }
  resolve(x);
}

// 链式Promise
class Promise {
  // 回调队列
  callbacks = [];
  // Promise实例状态
  state = "pending";
  // Promise的值
  value = null;
  // 构造函数传入需要执行的函数[形式为(resolve) => {}]
  constructor(fn) {
    fn(this._resolve.bind(this));
  }

  // _resolve方法用于改变Promise的状态为已解决,并执行回调队列[形式为(value) => {}]
  _resolve(value) {
    // 改变状态,保存值
    this.state = "fulfilled";
    this.value = value;
    // 依次执行回调队列
    this.callbacks.forEach((fn) => fn());
  }

  // then方法用于注册onFulfilled函数[形式为(value) => {}]
  then(onFulfilled) {
    // 返回了新的 Promise 实例,这样可实现真正的链式调用
    const nextPromise = new Promise((resolve) => {
      if (this.state === "pending") {
        this.callbacks.push(() => {
          // 必须异步执行,否则无法获取nextPromise对象
          setTimeout(() => {
            this._execCb(onFulfilled, nextPromise, resolve);
          });
        });
        return;
      }

      if (this.state === "fulfilled") {
        // 必须异步执行,否则无法获取nextPromise对象
        setTimeout(() => {
          this._execCb(onFulfilled, nextPromise, resolve);
        });
        return;
      }
    });

    return nextPromise;
  }

  _execCb(cb, nextPromise, resolve) {
    const x = cb(this.value);
    resolvePromise(nextPromise, x, resolve);
  }
}

这部分内容可能不易理解,请大家多动手多思考。这一节的完整代码可参考:Promise:链式调用实现

完整实现

上文已经将 Promise 的核心逻辑实现。在理解了这部分的基础上,参照规范将其余部分进行实现应该不难了。 剩下的工作主要在于添加 rejected 状态处理,以及考虑一些边界条件(例如当resolve传递了Promise实例本身导致链式调用进入死循环)。 完整的实现代码可以参考:Promise/A+完整实现