完全搞懂promise

上一次梳理了原型链和继承的知识,这两者在 JS 的面向对象编程中可以说是非常常见的,尤其是开发 React 中的类式组件次次都要接触到 (虽我目前还是主要使用 Vue)。

然而其实在 JS 中,promise 出现的次数也相当频繁。只要涉及到异步事件的处理,几乎随处可见的就是 promise 的身影。

在这个笔记中,我决定记录下我在学习 promise 的课程中,涉及的的相关知识。

引子

在了解 Promise 之前,我们也要弄清楚一些概念。

函数对象?实例对象?

了解了 JS 的原型和继承之后,我们知道:在 Javascript 里面,严格来说其实并没有“类”的概念。类的实现基本是靠构造函数原型对象来进行模拟得到的。

一个大神的笔记中梳理了以下关系图,通过这张图,我们能够更好地理解,JS 中的(构造)函数对象和实例对象的关系。

构造函数和实例对象的关系

在这里,也不再过多赘述函数对象、实例对象以及继承链的更多细节了 (上一个笔记已写)。简单来说如下:

  • 函数对象:将函数作为对象使用时,即为函数对象。构造函数也是一个函数,只不过它于普通函数有点不同。
  • 实例对象:通过 new 一个构造函数产生的对象称为实例对象,简称对象

两种回调函数

同步回调函数

同步回调函数是立即执行的,完全执行完了才结束,它不会被放入回调队列中。常见的同步回调有:

  • 数组遍历相关的回调函数
  • Promise 中的 executor 函数

举一个例子,比如有以下的代码:

1
2
3
4
5
const arr = [1,3,5]
arr.forEach(item => { //遍历回调
console.log(item)
})
console.log('forEach() 之后')

问上述两个 console.log 谁先执行?
答案是:先 console.log 数组元素,再输出 forEach() 之后

异步回调函数

异步回调对比同步回调不同的是,他是放入回调队列中将来执行的,等到同步代码执行完之后才进行执行。

还是一个例子:

1
2
3
4
setTimeout(() => { // 通过 setTimeout 可以创建一个异步回调
console.log('timeout callback()')
}, 0);
console.log('setTimeout() 之后');

在这里先输出 setTimeout() 之后,然后才输出 timeout callback()

备注

在异步回调中,还有微任务 (microtask)宏任务 (task) 之分。这里微任务、宏任务等概念涉及 JS 的事件循环 (event loop) 机制,暂不赘述。

简单来说就是,因为 JS 的本质是单线程的,所以 JS 有个单线程的执行机制。其顺序为:

执行一个宏任务 (先执行同步代码) => 执行所有微任务 => UI Render => 执行下一个宏任务 => 执行所有微任务 => UI Render => …

其中,setTimeout 就是宏任务,promise.thenmutation 等是微任务。

JS 中的 error 处理

JS 错误的类型

  • Error:所有错误的父类型

  • ReferenceError:引用的变量不存在

    1
    2
    // a 未定义
    console.log(a) // 报错
  • TypeError:数据类型不正确的错误

    1
    2
    3
    4
    5
    6
    let b;
    console.log(b.xxx) // 报错
    // b 不是 Object, 无 xxx 属性
    b = {};
    b.xxx() // 报错
    // b 有 xxx 属性,但不是一个函数
  • RangeError:数据值不在其所允许的范围内

    1
    2
    3
    4
    function fn() {
    fn();
    }
    fn(); // 报错,因为栈空间溢出
  • SyntaxError:语法错误

    1
    let a = """";  // 报错

错误处理

  • 捕获错误:try ... catch

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    try {
    let d;
    console.log(d.xxx);
    } catch (error) {
    console.log(error);
    }

    // error 是一个对象 (Object)

    // 此时 error 是 TypeError 的实例
    // 这里的 error.__proto__ = Error
  • 抛出错误:throw error

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 一个栗子
    function something() {
    if (Date.now() % 2 === 1) {
    console.log('当前时间为奇数,可以执行任务');
    } else {
    throw new Error('当前时间为偶数,无法执行任务')
    }
    }

    // 在外面捕获异常
    try {
    something();
    } catch (error) {
    alert(error.message);
    }

错误对象

错误对象包含两个主要属性:

  • message 属性:错误相关信息
  • stack 属性:函数调用栈记录信息

promise 的理解和使用

什么是 promise?

  1. 抽象表达

    Promise 是 JS 中进行异步编程的新的解决方案。旧的是纯回调形式

  2. 具体表达

    • 从语法上来说:Promise 是一个构造函数
    • 从功能上来说:promise 对象用来封装一个异步操作并可以获取其结果

promise 的状态

状态改变

  1. pending 变为 resolved
  2. pending 变为 rejected

基本流程

MDN 文档的 Promise 执行流程图

基本使用

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
// 1. 创建一个新的 promise 对象
// 为了方便后期取到 value/reason,将 promise 对象赋给 p

const p = new Promise((resolve, reject) => {
// 执行器 executor 函数,同步回调

// 2. 执行异步操作任务
setTimeout(() => {
// 随便写一个异步任务
const time = Date.now();
if (time % 2 == 0) {
// 3a. 如果成功了,调用 resolve(value)
resolve("成功" + time);
} else {
// 3b. 如果失败了,调用 reject(reason)
reject("失败" + time);
}
}, 1000);

});

p.then(
// 异步回调
value => { // 接收得到成功的 value 数据 onResolved
console.log('成功的回调' + value);
},
reason => { // 接收得到失败的 reason 数据 onRejected
console.log('失败的回调' + reason)
},
);

为什么“promise”?

  1. 指定回调函数的方式更加灵活。

    • 旧方法:必须在启动异步任务前指定回调函数;

    • 使用 promise:启动异步任务 => 返回 promise 对象 => 给 promise 对象绑定回调函数,甚至可以在异步任务完成之后绑定

  2. 支持链式调用,可以解决回调地狱的问题。回调地狱:回调函数嵌套调用,不便于阅读,不便于异常处理;

    • 解决方案:promise 链式调用;
    • 终极解决方案:async / await
回调地狱 demo
1
2
3
4
5
6
7
8
// 2a. 回调地狱的一个 demo
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback)
}, failureCallback)
}, failureCallback)

肉眼可见,在有非常多的回调函数的时候,这种逻辑和不断缩进的代码会弄的人非常头大。

1
2
3
4
5
6
7
8
9
10
11
12
// 2b. 使用 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)

在使用 promise 的链式调用之后,整个代码的逻辑变得清晰很多。

1
2
3
4
5
6
7
8
9
10
11
// 2c. async/await:回调地狱的终极解决方案
async function request() {
try {
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log('Got the final result: ' + finalResult);
} catch (error) {
failureCallback(error);
}
}

将异步操作用 async/await 进行书写,就和写同步操作一样简单。

如何使用 promise

构造函数

Promise 的构造函数:Promise(executor) {}

  • executor 函数:同步执行 (resolve, reject) => {}
  • resolve 函数:内部定义成功时我们调用的函数 value => {}
  • reject 函数:内部定义失败时我们调用的函数 reason => {}

说明executor 会在 Promise 内部立即同步回调,异步操作是在执行器中执行的。

then 方法

Promise.prototype.then 方法:(onResolved, onRejected) => {}

  • onResolved 函数:成功的回调函数 (value) => {}
  • onRejected 函数:失败的回调函数 (reason) => {}

说明:设置用于得到成功 value 的成功回调和用于得到失败 reason 的失败回调,返回一个新的 promise 对象。

catch 方法

Promise.prototype.catch 方法:(onRejected) => {}

  • onRejected 函数:失败的回调函数 (reason) => {}

说明then() 的语法糖,相当于 then(undefined, onRejected)

resolve 方法

Promise.resolve(value) 方法:(value) => {}

  • value:成功的数据或 promise 对象
1
2
3
4
5
// 产生一个成功值为 1 的 promise 对象
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
const p2 = Promise.resolve(2);

说明:返回一个成功/失败的 promise 对象。

reject 方法

Promise.reject 方法:(reason) => {}

  • reason:失败的原因

说明:返回一个失败的 promise 对象。

all 方法

Promise.all 方法:(promises) => {}

  • promises:包含 n 个 promise 的数组
1
const pAll = Promise.all([p1, p2, p3]);

说明:返回一个新的 promise,只有所有的 promise 都成功才算成功,只要有一个失败了就直接失败。

race 方法

Promise.race 方法:(promises) => {}

  • promises:包含 n 个 promise 的数组

说明:返回一个新的 promise,第一个完成的 promise 的结果状态就是最终的结果状态。

注意点

如何改变 promise 的状态?

  1. resolve(value):如果当前状态是 pending,就会变为 resolved

  2. reject(reason):如果当前状态是 pending,就会变为 rejected

  3. 抛出异常:如果当前状态是 pending,就会变为 rejected

    注意:这里可以 throw 任何东西,并不一定要是一个 error。比如说除了 throw new Error('出错了') 以外,也可以 throw 3

一个 promise 指定多个成功/失败回调函数,都会调用吗?

Yes。当 promise 改变为对应状态时都会调用。

改变 promise 状态和指定回调函数,谁先谁后?

  • 都有可能,正常情况下是先指定回调再改变状态,但也可以先改状态再指定回调;

  • 如何先改状态再指定回调?

    • 在执行器中直接调用 resolve() / reject()
    • 延迟更长时间才调用 then()
  • 什么时候才能得到数据?

    • 如果先指定回调函数,那就当状态发生改变时,回调函数就会调用,得到数据;

      1
      2
      3
      4
      5
      6
      7
      8
      new Promise((resolve, reject) => {
      setTimeout(() => {
      resolve(1); // 后改变的状态 (同时指定数据)
      }, 1000);
      }).then( // 先指定回调函数,保存当前指定的回调
      value => {},
      reason => {console.log('reason', reason)}
      )
    • 如果先改变状态,那当指定回调函数时,回调函数就会调用,得到数据。

      1
      2
      3
      4
      5
      6
      new Promise((resolve, reject) => {
      resolve(1); // 先改变的状态 (同时指定数据)
      }).then( // 后指定回调函数,异步执行回调函数
      value => {},
      reason => {console.log('reason', reason)}
      )

promise.then() 返回新的结果状态

例如有以下的代码,试问其执行结果是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Promise((resolve, reject) => {
resolve(1);
}).then(
value => {
console.log('onResolved1()', value);
}
reason => {
console.log('onRejected1()', reason);
}
).then(
value => {
console.log('onResolved2()', value);
}
reason => {
console.log('onRejected2()', reason);
}
)

输出为

1
2
onResolved1() 1
onResolved2() undefined

那么,promise.then() 返回的新 promise 的结果状态,由什么决定?

  • 简单表达:由 then() 指定的回调函数执行的结果决定;
  • 详细表达:
    • 如果抛出异常,新 promise 变为 rejectedreason 为抛出的异常
    • 如果返回的是非 promise 的任意值,新 promise 变为 resolvedvalue 为返回的值
    • 如果返回的是另一个新 promise,此 promise 的结果就会成为新 promise 的结果

promise 如何串联多个操作任务?

  • promisethen() 返回一个新的 promise,可以看成 then() 的链式调用;

    注意:同步操作可以 return 值,但异步操作必须包裹一个新的 promise 对象

  • 通过 then 的链式调用串连多个同步/异步任务。

promise 异常穿透?

  • 当使用 promisethen 链式调用时,可以在最后指定失败的回调;
  • 前面任何操作发生异常,都会传到最后失败的回调中处理。

如何中断 promise 链?

  • 当使用 promisethen 链式调用时,在中间中断,不再调用后面的回调函数;
  • 方法:在回调函数中返回一个 pending 状态的 promise 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Promise((resolve, reject) => {
resolve(1);
}).then(
value => {
// ***
return new Promise(() => {});
// 返回一个 pending 的 promise
// ***
}
).then(
value => {
console.log('onResolved2()', value);
}
reason => {
console.log('onRejected2()', reason);
}
)