ES6 Promise(2)

接着《ES6 Promise(1)》继续学习Promise

信任问题

回调函数信任问题:
在使用传统的回调函数来处理异步操作时,存在信任问题。
因为在一些情况下,可能无法确保回调函数会被及时调用,或者根本不会被调用。
例如,在一个异步请求完成后,如果网络出现问题或其他错误,回调函数可能永远不会被调用,导致无法得知异步操作的结果。这会使代码变得难以理解和维护。

结合笔记“ES6 Promise(1)”中“回调函数和promise实现平移动画”的例子来看。使用Promise不仅是解决了回调函数层层嵌套的问题,更多的是解决了“信任问题”,不交由第三方库,直接自己控制流程。

回调函数:
假设我们将回调函数cb传给第三方库method,那么由于种种原因我们并不能保证这个第三方库安装我们的预想去执行回调,它有可能 快了一些/慢了一些/因为bug执行了两次cb函数
这导致了回调函数信任问题的一个方面:在异步代码中,对于回调的执行时机和正确性,我们需要信任所使用的库或函数

使用Promise:
Promise 可以将控制权从第三方库转移到我们自己的代码中,可以在 .then().catch() 方法中定义回调函数,而不用担心第三方库是否会正确调用这些回调。Promise 提供了一种更可靠的方式来处理异步操作,避免了信任问题
Promise状态一旦改变为 成功/失败 就不能再被改变,所以传进去的函数不会被重复执行:
回调和promise对比

控制反转

控制反转:
控制反转是一种设计模式,它强调将决策权从代码的一部分转移到另一部分。
Promise 使用控制反转来解决回调函数信任问题
Promise 将决定什么时候调用回调函数,而不是由我们的代码来决定 。当异步操作完成时,Promise 决定调用 .then().catch() 方法中的回调函数。
这种控制权的转移使得代码更可靠、更易于管理,并提供了一种更为结构化的处理异步操作的方式

回调函数:控制权已经在第三方库而不在自己手上了,第三方库有可能会在执行回调函数的时候“添油加醋”干些别的。

使用Promise: 在resolve()中的代码都是自己写的,很大程度上改善了“控制反转”的问题:
对比例子


Promise.all()获取多个Promise实例的参数

  • Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例
  • 使用Promise.all()得到的一定是Promise 实例,如果参数数组中有不是 Promise实例 的存在,那么他会自动通过Promise.resolve()将其转换为 Promise实例 。

新的Promise 实例的状态判断:

  • 数组中各个 Promise实例 的状态都是 fulfilled,新的Promise实例 的状态才是fulfilled。
  • 一旦有一个数组中的Promise实例状态为rejected,新的Promise实例 的状态就会变成rejected。
  • 注意参数为空数组时会直接决定新的Promise实例的状态为 已成功

使用Promise.all()传参:

  • 新的Promise实例 的状态为fulfilled时,参数数组中各个 Promise 实例的返回值组成一个数组,传递给 新的Promise实例 的回调函数。
  • 新的Promise实例 的状态为rejected时,第一个被reject的Promise实例的返回值,会传递给 新的Promise实例 的回调函数。

参数

  • 参数可以是一个数组,各数组元素必须是Promise 实例(如果不是Promise 实例,就会先调用下面讲到的Promise.resolve方法将其转换为Promise 实例
  • Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例

状态(fulfilled/rejected)和传参

1
const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all()方法接受一个数组作为参数p1、p2、p3都是 Promise 实例
如果不是Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。
另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例

p的状态由p1、p2、p3决定,分成两种情况:

  1. 只有p1、p2、p3的状态都变成 fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数
  2. 只要p1、p2、p3之中有一个被rejected,p的状态就变成 rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数

例子:p状态为fulfilled

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
// 模拟需要多个请求的数据 才能进行下一步操作的情况
function getData1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('第一条数据加载成功');
resolve('data1');
}, 1000);
})
}
function getData2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('第二条数据加载成功');
resolve('data2');
}, 1000);
})
}
function getData3() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('第三条数据加载成功');
resolve('data3');
}, 1000);
})
}
function getData4() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('第四条数据加载成功');
resolve('data4');
}, 1000);
})
}
let p = Promise.all([ getData1(), getData2(), getData3(), getData4() ])
p.then(arr => {
console.log(arr);
})
/*
1秒后打印出如下数据:
第一条数据加载成功
第二条数据加载成功
第三条数据加载成功
第四条数据加载成功
['data1', 'data2', 'data3', 'data4']
*/

例子:p状态为rejected

将getData4()的状态改为reject并传参data4 err:

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
// 模拟需要多个请求的数据 才能进行下一步操作的情况
function getData1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('第一条数据加载成功');
resolve('data1');
}, 1000);
})
}
function getData2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('第二条数据加载成功');
resolve('data2');
}, 1000);
})
}
function getData3() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('第三条数据加载成功');
resolve('data3');
}, 1000);
})
}
function getData4() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// console.log('第四条数据加载成功');
reject('data4 err');
}, 1000);
})
}
let p = Promise.all([ getData1(), getData2(), getData3(), getData4() ])
p.then(arr => {
console.log(arr);
}, e => {
console.log('then的参数2', e);
})
/*
异步操作还是会执行,但是参数不会被传递!!
此时p的then()第一参数将不被执行,执行第二参数:
第一条数据加载成功
第二条数据加载成功
第三条数据加载成功
then的参数2 data4 err
*/

例子:参数为空数组

参数为空数组时会直接决定新的Promise实例的状态为 已成功

1
2
3
4
5
6
7
8
let p = Promise.all([])
p.then(() => {
console.log('lalalala');
}, e => {
console.log(e);
})

// lalalala

Promise.race()

race 竞赛,所以可以理解为参数数组中各个Promise实例在竞赛,谁的状态先改变就影响新的Promise实例的状态)

  • Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
  • 使用Promise.race()得到的一定是Promise 实例,如果参数数组中有不是 Promise实例 的存在,那么他会自动通过Promise.resolve()将其转换为 Promise实例 。
  • 新的 Promise 实例的状态的判断方法不同,只要参数中的 Promise实例 中有一个实例率先改变状态,新的 Promise 实例 的状态就跟着改变
  • 传参:率先改变的那个 Promise 实例的返回值,就传递给 新的Promise 实例 的回调函数。

和Promise.all()的区别

  • Promise.all()中参数为空数组时会直接决定新的Promise实例的状态为 已成功
  • 在Promise.race()中参数为空数组时会直接挂起,什么反应也没有。

参数

  • all()参数一样,可以是一个数组,各数组元素必须是Promise 实例(如果不是Promise 实例,就会先调用下面讲到的Promise.resolve方法将其转换为Promise 实例
  • Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例

状态(fulfilled/rejected)和传参

1
const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。
那个率先改变的 Promise 实例的返回值,就传递给p的回调函数


Promise.resolve()

  • Promise.resolve()可以将现有对象转为 Promise 对象,可以不带参数调用它迅速得到一个resolved状态的 Promise 对象
  • 注意:
    • 当你调用 Promise.resolve(value) 时,value 的解析会被放入微任务队列,而不是像普通的Promise定义函数一样立即执行(具体可看下面“不带参数”的例子)
    • 当你在 Promise 中返回一个值,无论是直接的值还是通过 resolve() 返回,这个值都会被传递给 then 方法注册的回调函数

传参

参数可以是 普通的值/Promise实例/thenable对象

例子:参数为普通的值

1
2
3
4
5
6
7
8
9
10
// Promise.resolve() 和 Promise.reject()
// 常用来生成已经被决议为失败或者成功的promise实例
// Promise.resolve
// --------------------------------------------
// 传递一个普通的值
let p1 = new Promise(resolve => {
resolve('成功!');
})
// 等价的:
let p2 = Promise.resolve( '成功!');

例子:参数为Promise实例

如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例

1
2
3
4
5
6
7
8
// 传递一个Promise实例
let poruomiesi = new Promise(resolve => {
resolve('耶!')
})
// 等价的:直接返回传递进去的promise
let p = Promise.resolve(poruomiesi)
p.then(data => void console.log(data))
console.log(p === poruomiesi) //true 耶!

例子:参数为thenable对象

  • thenable对象:和类数组的概念相似,类数组看着像是数组,那么就可以当作数组转换。thenable对象指具有then()的对象,它看起来像是Promise对象。
  • Promise.resolve方法会将这个thenable对象转为 Promise 对象,然后由于已成功的状态,就会立即执行thenable对象的then方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 传递一个thenable
// 如果传递的是个thenable
let obj = {
then(cb) {
console.log('我被执行了');
cb('哼!');
},
oth() {
console.log('我被抛弃了');
}
}

// 立即执行then方法
Promise.resolve(obj).then(data => {
// 成功状态的返回值'哼!'被当作参数data传入then的参数1函数
console.log(data);
})

/*
我被执行了
哼!
*/

例子:不带有任何参数

Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象,所以如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法:

1
2
3
4
5
const p = Promise.resolve();

p.then(function () {
// ...
});

上面代码的变量p就是一个 Promise 对象

注意:立即resolve()的 Promise 对象,是微任务,先于宏任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
setTimeout(function () {
console.log('three');
}, 0);

Promise.resolve().then(function () {
console.log('two');
});

console.log('one');

// one
// two
// three

虽然 setTimeout 的延时设为 0,但它仍然会被放入宏任务队列,而不是立即执行。同时,Promise 的 .then 方法会在当前的微任务队列中添加一个微任务。微任务队列的优先级高于任务队列。所以,代码输出的顺序是 'one' -> 'two' -> 'three'

需要了解:
在事件循环中,JS 引擎会先执行当前执行上下文中的同步代码,然后检查微任务队列,按顺序执行微任务。接着,它会检查宏任务队列,按顺序执行宏任务。这个过程不断循环,形成了事件循环
微任务通常包括 Promise 的 .then 回调、MutationObserver 回调等
宏任务可以包括定时器回调(如 setTimeout、setInterval)、事件回调(如 DOM 事件、网络请求等)等(同步任务也可以被看作是宏任务的一种)


Promise.reject()

  • Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
  • 注意:与Promise.resolve方法不一样Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。(可以参考下面“例子:参数为thenable对象”)

传参

例子:参数为普通的值

下面代码生成一个 Promise 对象的实例p,状态为rejected,回调函数会立即执行。

1
2
3
4
5
6
7
8
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
console.log(s)
});
// 出错了

例子:参数为thenable对象

下面代码中,Promise.reject方法的参数是一个thenable对象,执行以后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象:

1
2
3
4
5
6
7
8
9
10
11
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};

Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true