js异步
# JS 异步
- 所谓"异步",简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段;
- 这种不连续的执行,就叫做异步。相应地,连续的执行,就叫做同步。
# JS 异步编程六种方案
异步编程进化史:
callback -> promise/A+ -> generator -> async/await,事件监听,发布订阅(总共6种)
async/await 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。可以说是异步终极解决方案了。async 等于Generator+自动执行器
# 异步方案比较 【要点】
# callback
Node在处理异常有一个约定,将异常作为回调的第一个实参传回,如果为空表示没有出错。
async(function(err,callback){
if(err){console.log(err);}
});
2
3
优点:简单、方便、实用。 缺点:易形成回调函数地狱。
# 事件监听、发布订阅
优点:与回调函数相比,事件监听方式实现了代码的解耦,将两个回调函数分离了开来,更方便进行代码的管理。 缺点:使用起来不方便,每次都要手动地绑定和触发事件。 而发布/订阅模式与其类似,就不多说了。
# promise跟callback的比较
优点:使用Promise
的方式,我们成功地将回调函数嵌套调用变成了链式调用,与前两种方式相比逻辑更强,执行顺序更清楚。
缺点:代码冗余,异步操作都被包裹在Promise
构造函数和then
方法中,主体代码不明显,语义变得不清楚。
# generator跟promise的比较
优点:generator 方式使得异步操作很接近同步操作,十分的简洁明了。另外,gen 执行 yield 语句时,只是将执行上下文暂时弹出,并不会销毁,这使得上下文状态被保存。 缺点: 流程管理不方便,需要一个执行器来执行 generator 函数。
# async跟generator的比较
- 内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,**而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。**返回值不同,Generator 返回遍历器,相比于 async 返回 promise 对象操作更加麻烦。
- 更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
- 更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
# async跟promise的比较
- 处理 then 的调用链,能够更清晰准确的写出代码; 能更好地处理 then 链;
- 并且也能优雅地解决回调地狱问题。简约而干净Concise and clean;
- 错误处理Error handling;
- 更好的处理中间值;
- 条件语句: 代码嵌套(6层)可读性较差,它们传达的意思只是需要将最终结果传递到最外层的Promise。使用async/await编写可以大大地提高可读性
- 当然async/await函数也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all 的方式。
使用示例比较: 假设我们需要分别读取 a、b、c 三个文件,具体代码如下: 我们可以看出来 async 函数比起 Promise 的链式操作,以及 Generator 的手动执行,要方便得太多了,代码上也简洁明了,让我们看起来一目了然。
# Promise
# Promise/A+ 规范
Promise 规范有很多,如 Promise/A,Promise/B,Promise/D 以及 Promise/A 的升级版 Promise/A+,有兴趣的可以去了解下,最终 ES6 中采用了 Promise/A+ 规范
规范虽然不长,但细节也比较多,几个要点简单说明下:
- Promise 本质是一个状态机。每个 promise 只能是 3 种状态中的一种:pending、fulfilled 或 rejected。状态转变只能是 pending -> fulfilled 或者 pending -> rejected。状态转变不可逆。
- then 方法可以被同一个 promise 调用多次。
- **then 方法必须返回一个 promise。**规范里没有明确说明返回一个新的 promise 还是复用老的 promise(即 return this),大多数实现都是返回一个新的 promise,而且复用老的 promise 可能改变内部状态,这与规范也是相违背的。
- 值穿透。
简单示例:如何实现一个 sleep 函数 超时封装promise;
# 如何实现sleep的效果(es5或者es6)
//while循环的方式实现
function sleep(delay) {
var start = Date.now(), expire = start + delay;
while (Date.now() < expire) ;//while ((new Date()).getTime() - start < delay) {
return;
}
//执行sleep(1000)之后,休眠了1000ms之后输出了1111。上述循环的方式缺点很明显,容易造成死循环。
//generator方式实现
function* sleep(ms) {//通过generate来实现
yield new Promise(function(resolve, reject) {
console.log(111);
setTimeout(resolve, ms);
});
}
sleep(500).next().value.then(function() {
console.log(2222);
});
//generator方式实现
function delayPms(time) {//promise的方式;
return new Promise((resolve) => setTimeout(resolve, time))
}
//promise简写
const delayPms = ms => new Promise(resolve => setTimeout(resolve, ms))
async function test(){//通过async封装
var temple= if (isDelay) { await delayPms(1000);}
console.log(1111)
return temple
}
test();//延迟1000ms输出了1111
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
# 三种状态和两种变化途径
Promise对象只有三种状态。
- 异步操作“未完成”(pending)
- 异步操作“已完成”(resolved,又称fulfilled)
- 异步操作“失败”(rejected)
这三种的状态的变化途径只有两种。
- 异步操作从“未完成”到“已完成”
- 异步操作从“未完成”到“失败”。
# 常用方法【要点】
# 静态方法(4个)
- Promise.resolve 返回一个fulfilled状态的promise对象
- Promise.reject 返回一个rejected状态的promise对象
- Promise.all(Arr)接受一个promise对象的数组,待全部完成之后,统一执行success
- Promise.race接受一个包含多个promise对象的数组,只要有一个完成,就执行success
其他新方法:
Promise.allSettled(Arr)接受一个promise对象的数组;
- 接收一个Promise数组,数组中如有非Promise项,则此项当做成功
- 把每一个Promise的结果,集合成数组,返回;
- 当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个
promise
的结果时,通常使用它。 - 相比之下,
Promise.all()
更适合彼此相互依赖或者在其中任何一个reject
时立即结束。
区别:一句话概括Promise.allSettled和Promise.all的最大不同:Promise.allSettled永远不会被reject。
Promise.all
将在 Promises 数组中的其中一个 Promises 失败后立即失败。Promise.allSettled
将永远不会失败,一旦数组中的所有 Promises 被完成或失败,它就会完成。
Promise.any(Arr)接受一个promise对象的数组,any与all相反; 【目前还是试验性阶段】
- 接收一个Promise数组,数组中如有非Promise项,则此项当做成功
- 如果有一个Promise成功,则返回这个成功结果
- 如果所有Promise都失败,则报错
Promise.all(Arr)
var p = Promise.all([p1, p2, p3]);
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
// 生成一个Promise对象的数组
var promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON("/post/" + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
2
3
4
5
6
7
8
9
Promise.allSettled(Arr)
一句话概括Promise.allSettled和Promise.all的最大不同:Promise.allSettled永远不会被reject。
const promises = [
delay(100).then(() => 1),
delay(200).then(() => 2),
Promise.reject(3)
]
Promise.allSettled(promises).then(values=>console.log(values))
// 最终输出:
// [
// {status: "fulfilled", value: 1},
// {status: "fulfilled", value: 2},
// {status: "rejected", value: 3},
// ]
2
3
4
5
6
7
8
9
10
11
12
13
可以看到所有promise的数据都被包含在then语句中,且每个promise的返回值多了一个status字段,表示当前promise的状态,没有任何一个promise的信息被丢失。
因此,当用Promise.allSettled时,我们只需专注在then语句里,当有promise被异常打断时,我们依然能妥善处理那些已经成功了的promise,不必全部重来。
# 属性方法(3个)
- Promise.prototype.then
- Promise.prototype.catch
- Promise.prototype.finally()
# 内部实现原理【要点】
# 特点
- Promise 本质是一个状态机。每个 promise 只能是 3 种状态中的一种:pending、fulfilled 或 rejected
- then 方法可以被同一个 promise 调用多次。
- then 方法必须返回一个 promise。
# 实现promise
#
Promise
的几种状态、如何实现的?async
库是如何实现的?
- 1: 类构造时 三种状态,两个成功、失败队列。绑定resolve及reject方法处理函数;
- 2: 添加
_resolve
执行时的函数,依次执行成功及错误函数执行的函数,及记得处理本身就是promise的处理;并清空队列;_reject
执行时的函数,依次执行错误函数执行的函数,并清空队列;为了支持同步的Promise,这里采用异步调用;【setTimeout(run, 0)】 - 3:添加.then方法, new MyPromise状态判断; 添加catch, finally;及静态resolve reject, all ,race方法;
// 判断变量否为function
const isFunction = variable => typeof variable === 'function'
// 定义Promise的三种状态常量
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
class MyPromise {
constructor(handle) {
if (!isFunction(handle)) {
throw new Error('MyPromise must accept a function as a parameter')
}
this._status = PENDING// 添加状态
this._value = undefined // 添加状态; value 变量用于保存 resolve 或者 reject 中传入的值
this._fulfilledQueues = []// 添加成功回调函数队列
this._rejectedQueues = [] // 添加失败回调函数队列
try { // 执行handle
handle(this._resolve.bind(this), this._reject.bind(this))
} catch (err) {
this._reject(err)
}
}
_resolve(val) {// 添加resovle时执行的函数
const run = () => {//首先得判断当前状态是否为等待中,因为规范规定只有等待态才可以改变状态
if (this._status !== PENDING) return
const runFulfilled = (value) => {// 依次执行成功队列中的函数,并清空队列
let cb;
while (cb = this._fulfilledQueues.shift()) {
cb(value)
}
//that._fulfilledQueues.map(cb => cb(that.value))
}
const runRejected = (error) => {// 依次执行失败队列中的函数,并清空队列
let cb;
while (cb = this._rejectedQueues.shift()) {
cb(error)
}
}
/* 如果resolve的参数为Promise对象,则必须等待该Promise对象状态改变后,
当前Promsie的状态才会改变,且状态取决于参数Promsie对象的状态
*/
if (val instanceof MyPromise) {
val.then(value => {
this._value = value
this._status = FULFILLED
runFulfilled(value)
}, err => {
this._value = err
this._status = REJECTED
runRejected(err)
})
} else {
this._value = val
this._status = FULFILLED
runFulfilled(val)
}
}
setTimeout(run, 0)// 为了支持同步的Promise,这里采用异步调用
}
_reject(err) {// 添加reject时执行的函数
if (this._status !== PENDING) return
const run = () => {// 依次执行失败队列中的函数,并清空队列
this._status = REJECTED
this._value = err
let cb;
while (cb = this._rejectedQueues.shift()) {
cb(err)
}
}
setTimeout(run, 0) // 为了支持同步的Promise,这里采用异步调用
}
then(onFulfilled, onRejected) {// 添加then方法
const { _value, _status } = this
return new MyPromise((onFulfilledNext, onRejectedNext) => {// 返回一个新的Promise对象
let fulfilled = value => {// 封装一个成功时执行的函数
try {
if (!isFunction(onFulfilled)) {
onFulfilledNext(value)
} else {
let res = onFulfilled(value);
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err)
}
}
let rejected = error => { // 封装一个失败时执行的函数
try {
if (!isFunction(onRejected)) {
onRejectedNext(error)
} else {
let res = onRejected(error);
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err)
}
}
switch (_status) {
case PENDING:// 当状态为pending时,将then方法回调函数加入执行队列等待执行
this._fulfilledQueues.push(fulfilled)
this._rejectedQueues.push(rejected)
break
case FULFILLED:// 当状态已经改变时,立即执行对应的回调函数
fulfilled(_value)
break
case REJECTED:
rejected(_value)
break
}
})
}
catch(onRejected) {// 添加catch方法
return this.then(undefined, onRejected)
}
finally(cb) {// 添加finally方法
return this.then(
value => MyPromise.resolve(cb()).then(() => value),
reason => MyPromise.resolve(cb()).then(() => { throw reason })
);
}
static resolve(value) {// 添加静态resolve方法
if (value instanceof MyPromise) return value // 如果参数是MyPromise实例,直接返回这个实例
return new MyPromise(resolve => resolve(value))
}
static reject(reason) {// 添加静态reject方法
return new MyPromise((resolve, reject) => reject(reason))
}
static all(list) {// 添加静态all方法
return new MyPromise((resolve, reject) => {
let values = []//返回值的集合
let count = 0
for (let [i, p] of list.entries()) {
// 数组参数如果不是MyPromise实例,先调用MyPromise.resolve
this.resolve(p).then(res => {
values[i] = res
count++
// 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilled
if (count === list.length) resolve(values)
}, err => {
reject(err)// 有一个被rejected时返回的MyPromise状态就变成rejected
})
}
})
}
static race(list) {// 添加静态race方法
return new MyPromise((resolve, reject) => {
for (let p of list) {//注意这里的遍历跟上面不一样;
this.resolve(p).then(res => {// 只要有一个实例率先改变状态,新的MyPromise的状态就跟着改变
resolve(res)
}, err => {
reject(err)
})
}
})
}
}
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# 第三方库Bluebird
Bluebird API Reference (opens new window)
示例:
# 10 个 Ajax 同时发起请求,全部返回展示结果,并且至多允许三次失败,说出设计思路
第一时间想到 Promise.all
,但是这个函数有一个局限在于如果失败一次就返回了,直接这样实现会有点问题,需要变通下。以下是两种实现思路:记住错误及正常的的数量;再做返回判断;
// 以下是不完整代码,着重于思路 非 Promise 写法
let successCount = 0
let errorCount = 0
let datas = []
ajax(url, (res) => {
if (success) {
success++
if (success + errorCount === 10) {
console.log(datas)
} else {
datas.push(res.data)
}
} else {
errorCount++
if (errorCount > 3) {
// 失败次数大于3次就应该报错了
throw Error('失败三次')
}
}
})
// Promise 写法
let errorCount = 0
let p = new Promise((resolve, reject) => {
if (res.success) {
resolve(res.data)
} else {
errorCount++
if (errorCount > 3) {// 失败次数大于3次就应该报错;
reject(error)
} else { //// 失败次数小于3次时,正常返回;
resolve(error)
}
}
})
Promise.all([p]).then(v => {
console.log(v);
});
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
# generator/co
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator 函数是一个普通函数,但是有两个特征。
- 一是,
function
关键字与函数名之间有一个星号; - 二是,函数体内部使用
yield
表达式,定义不同的内部状态(yield
在英语里的意思就是“产出”)。
# 分类组合
- generator + 回调函数
- generator + Promise: 除了
Thunk
函数,我们还可以借助Promise
对象来执行 generator 函数。 - generator + co 模块
node.js 中的**
co
模块是一个用来自动执行generator
函数的模块**,它的入口是一个co(gen)
函数,它预期接收一个 generator 对象或者 generator 函数作为参数,返回一个Promise
对象。
# 使用总括
- 由于yield表达式可以暂停执行,next方法可以恢复执行,这使得Generator函数很适合用来将异步任务同步化。
- 但是Generator函数的流程控制会稍显麻烦,因为每次都需要手动执行next方法来恢复函数执行,并且向next方法传递参数以输出上一个yiled表达式的返回值。
- 于是就有了thunk(thunkify)函数和co模块来实现Generator函数的自动流程控制。
- 通过thunk(thunkify)函数分离参数,以闭包的形式将参数逐一传入,再通过apply或者call方法调用,然后配合使用run函数可以做到自动流程控制。
- 通过co模块,实际上就是将run函数和thunk(thunkify)函数进行了封装,并且yield表达式同时支持thunk(thunkify)函数和Promise对象两种形式,使得自动流程控制更加的方便。
# 执行 generator 函数
# 自动执行
要点: g.next() 后的res是否为done, 如果是done的话直接return res.value; 如果不是的话再次then循环;
const run = gen => {
const g = gen()
const next = data => {
let result = g.next(data)
if (result.done) return result.value
result.value.then(data => {
next(data)
})
}
next()
}
2
3
4
5
6
7
8
9
10
11
# 手动执行
手动执行其实就是用then方法,层层添加回调函数。详见下下面的手动编写;
const g = gen();
g.next().value.then(data => {
g.next(data).value.then(data => g.next(data), err => g.throw(err));
}, err => g.throw(err));
2
3
4
# co模块
使用co模块的前提条件是,Generator函数的yield表达式后面,只能是thunk(thunkify)或者Promise对象,如果是数组或对象的成员全部都是promise对象,也可以使用co模块。
# 原理
co模块其实就是将两种自动执行器(thunk(thunkify)函数和Promise对象),包装成一个模块。
# 执行
基于Promise对象
const person = (sex, fn) => {
return new Promise((resolve, reject) => {
window.setTimeout(() => {
const data = {
name: 'samy',
height: 100
}
resolve(data)
}, 1000)
})
}
function *gen () {
const data = yield person('boy')
console.log(data) // {name: 'samy', height: 100}
}
const g = gen()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 自动执行
const run = gen => {
const g = gen()
const next = data => {
let result = g.next(data)
if (result.done) return result.value
result.value.then(data => {
next(data)
})
}
next()
}
const run = (gen) => {
let g = gen()
let next = (data) => {
let { done, value } = g.next(data)
if (done) return value;
value.then(data => {
next(data)
})
}
}
next()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 手动执行
手动执行实际上就是层层使用then方法和next方法。根据这个可以写出自动执行器。
g.next().value.then(data => {
g.next(data)
})
2
3
# async/await
node7.6 以上的版本引入了一个 ES7 的新特性 Async/Await 是专门用于控制异步代码。为了使异步操作得更加方便,本质上 async 函数是 Generator 函数的语法糖。 async/await
是一个很重要的语法糖
,他的作用是用同步的方式,执行异步操作。
async
函数是generator
函数的语法糖,它相对于一个自带执行器(如 co 模块)的generator
函数。async
函数中的await
关键字预期接收一个Promise
对象,如果不是 Promise 对象则返回原值,这使得它的适用性比 co 执行器更广。
说明:
注意事项及优化的地方:
- await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中
- finally多个await命令后面的异步操作,如果不存在继发关系,最好让它们用Promise.all同时触发; let [a, b] = await Promise.all([a(), b()]);
- await命令只能用在async函数之中,如果用在普通函数,就会报错;
async function f() {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出错了
2
3
4
5
6
7
8
9
完整示例:参照文章开头四种类型的比较示例图;
# 内部实现原理
它就是 Generator 函数的语法糖,async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。、跟co的一致;
内部实现: 下面给出spawn函数的实现,基本就是前文自动执行器的翻版。
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async函数是基于generator实现,所以涉及到generator相关知识。在没有async函数之前,通常使用co库来执行generator,所以通过co我们也能模拟async的实现。
function generatorToAsync(generatorFn) {
return function () {
const gen = generatorFn.apply(this, arguments) // gen有可能传参
return new Promise((resolve, reject) => { // 返回一个Promise
function next(key, arg) {
let res
try {
res = gen[key](arg) // 这里有可能会执行返回reject状态的Promise
} catch (error) {
return reject(error) // 报错的话会走catch,直接reject
}
const { value, done } = res // 解构获得value和done
if (done) return resolve(value)// 如果done为true,说明走完了,进行resolve(value)
// 如果done为false,说明没走完,还得继续走; value有可能是:常量,Promise,Promise有可能是成功或者失败
return Promise.resolve(value).then(val => next('next', val), err => next('throw', err))
}
next("next") // 第一次执行
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async/await
版本
async function asyncFn() {
const num1 = await fn(1)
console.log(num1) // 2
const num2 = await fn(num1)
console.log(num2) // 4
const num3 = await fn(num2)
console.log(num3) // 8
return num3
}
const asyncRes = asyncFn()
console.log(asyncRes) // Promise
asyncRes.then(res => console.log(res)) // 8
2
3
4
5
6
7
8
9
10
11
12
使用generatorToAsync函数
的版本
function* gen() {
const num1 = yield fn(1)
console.log(num1) // 2
const num2 = yield fn(num1)
console.log(num2) // 4
const num3 = yield fn(num2)
console.log(num3) // 8
return num3
}
const genToAsync = generatorToAsync(gen)
const asyncRes = genToAsync()
console.log(asyncRes) // Promise
asyncRes.then(res => console.log(res)) // 8
2
3
4
5
6
7
8
9
10
11
12
13
14
# 配合promise的使用
# 如何避免 await/async 地狱
如果不存在继发关系,最好让它们用Promise.all同时触发; let [a, b] = await Promise.all([a(), b()]);
//async/await 结合promise使用就好了, 即异步流程控制的最后加上 promise的catch.
async function task(){
return await req();
}
task().catch(e => console.error(e))
//结合async/await 结合Promise.all使用时, 如何捕获异常&处理?
async function hello(flag){
return new Promise((resolve, reject) => {
if(flag) setTimeout(() => resolve('hello'), 100);
else reject('hello-error');
})
}
async function demo(flag){
return new Promise((resolve, reject) => {
if(flag) setTimeout(() => resolve('demo'), 100);
else reject('demo-error');
})
}
async function main(){
let res = await hello(1).catch(e => console.error(e));
console.log('res => ', res);
let result = await Promise.all([hello(1), demo(1)]);
// let result = await Promise.all([hello(1), demo(0)]).catch(e => console.error('error => ', e));
console.log('result => ', result);
}
main()
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
# 编写一个订购披萨和饮料的代码
流程:
- 获取披萨菜单
- 获取饮料菜单
- 从披萨菜单中选择披萨
- 从饮料菜单中选择饮料
- 把选好的披萨加到购物车
- 把选好的饮料加到购物车
- 下单
promises 一个有趣的特性就是你可以在一行代码中去得到 Promise ,而在另外一行中去等待并 resolve,这是避免 async/await 地狱的关键之处。
(async () => {
const promise = doSomeAsyncTask()
const value = await promise
console.log(value) // the actual value
})()
2
3
4
5
它可能会像这样:
(async () => {
const pizzaData = await getPizzaData() // async call
const drinkData = await getDrinkData() // async call
const chosenPizza = choosePizza() // sync call
const chosenDrink = chooseDrink() // sync call
await addPizzaToCart(chosenPizza) // async call
await addDrinkToCart(chosenDrink) // async call
orderItems() // async call
})()
async function orderItems() {
const items = await getCartItems() // async call
const noOfItems = items.length
for(var i = 0; i < noOfItems; i++) {
await sendRequest(items[i]) // async call
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
优化:
找到语句的依赖关系;
选择饮料的时候并不依赖于选择披萨,所以选择披萨和饮料是可以并行执行的。
封装相互依赖的异步方法;
选择披萨的依赖有获取披萨菜单、选择、添加到购物车。所以把这些依赖放在一个异步方法里,饮料同理;
并行执行;
利用事件循环去非阻塞并行地执行这些异步方法,通常会用的两个方法就是尽早的返回 Promise 和使用 Promise.all()
把相互依赖的语句封装在各自的函数里,现在同时去执行 selectPizza() 和 selectDrink();修改下代码
async function selectPizza() {
const pizzaData = await getPizzaData() // async call
const chosenPizza = choosePizza() // sync call
await addPizzaToCart(chosenPizza) // async call
}
async function selectDrink() {
const drinkData = await getDrinkData() // async call
const chosenDrink = chooseDrink() // sync call
await addDrinkToCart(chosenDrink) // async call
}
(async () => {
const pizzaPromise = selectPizza()
const drinkPromise = selectDrink()
await pizzaPromise
await drinkPromise
orderItems() // async call
})()
// Although I prefer it this way
(async () => {
Promise.all([selectPizza(), selectDrink()]).then(orderItems) // async call
})()
async function orderItems() {
const items = await getCartItems(); // async call
const noOfItems = items.length;
const promises = [];
for (var i = 0; i < noOfItems; i++) {
const orderPromise = sendRequest(items[i]); // async call
promises.push(orderPromise); // sync call
}
await Promise.all(promises); // async call
}
// Although I prefer it this way
async function orderItems() {
const items = await getCartItems(); // async call
const promises = items.map(item => sendRequest(item));
await Promise.all(promises); // async call
}
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
# 用Promise对象实现的Ajax操作
var getJSON = function(url) {
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
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
// 生成一个Promise对象的数组
var promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON("/post/" + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
2
3
4
5
6
7
8
9
# 限制并发量
- 根据
max
值我们建立一个请求的执行数组requestArr(长度上限就为max
了) - 我们将请求的promise一项一项丢入到其中(递归实现),执行完毕就删除掉
- requestArr到达上限那么我们等待请求执行,有空位在继续添加
urls
添加完毕之后我们将requestArr(这里只需要拿到最后的请求数组是因为之前请求执行完了才会自己删除自己,所以只需要判定最后的是否都执行完就ok了)进行Promise.all判定执行callback。
then优化前:
function requestData(urls = [], max = 1, callback = () => { }) {
let requestArr = [], i = 0;
function toFetch() {
if (i === urls.length) {
return Promise.resolve();// 判定都添加完后返回resolve
}
let fetchItem = fetch(urls[i++]);// 通过fetch方法创建请求promise
requestArr.push(fetchItem);// 给每一项定义一个执行完毕删除自身的微任务
fetchItem.then(() => { requestArr.splice(requestArr.indexOf(fetchItem), 1) });
let result = Promise.resolve();
// 当执行数组达到上限时我们通过Promise.race方法判定是否有“空位置”
if (requestArr.length === max) {
result = Promise.race(requestArr);
}
return result.then(() => toFetch());
}
toFetch().then(() => Promise.all(requestArr)).then(() => { callback(); })
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
await 优化后:
通过await我们就可以直接在任务队列满掉的时候去判断停住函数
// 这边直接定义一个数组接受所有fetch request
async function requestData(urls = [], max = 1, callback = () => { }) {
const fetchArr = []
const requestArr = []
for (const item of urls) {
const p = fetch(item) // 这个是promise
fetchArr.push(p)
// 如果最大限制比数组小我们才需要走这一步
if (max <= urls.length) {
const e = p.then(() => {
requestArr.splice(requestArr.indexOf(p), 1)
})
requestArr.push(e)
if (requestArr.length >= max) {
await Promise.race(requestArr);
}
}
}
Promise.all(fetchArr).then(() => { callback() })
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 特殊用法
const www = new Proxy(new URL('https://www'), {
get: function get(target, prop) {
let o = Reflect.get(target, prop);
// 当 `[[Get]]` 的属性为 `URL` 已有的函数,那么就返回这个已有的函数
// 补:例如 www.justjavac.com.toString()
if (typeof o === 'function') {
return o.bind(target)
}
// 当 `prop` 不是字符串的时候,返回当前的属性
// 补:object key 只能是字符串和 Symbol,所以这里是判断是否为 Symbol。
// 使用场景是 www.justjavac.com + 'foo/bar'
if (typeof prop !== 'string') {
return o;
}
// 如果 `prop` 为 `then`,就把 `URL` 转化为 `fetch` 后的
// `Promise`。(这样的结果就是不能调用 `www.then.com` 这样的网址了)
if (prop === 'then') {
return Promise.prototype.then.bind(fetch(target));
}
// 对于其余的情况,把新增加的字符串加进域名中,并重新包一层 `Proxy`。
target = new URL(target);
target.hostname += `.${prop}`;
return new Proxy(target, { get });
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
www.baidu.com.then(response => {
console.log(response.status);// ==> 200
})
2
3
用 async
/ await
语法则变得更为简单:
const response = await www.baidu.com
console.log(response.ok)// ==> true
console.log(response.status);// ==> 200
2
3
# 捕获异常
**能被 try catch 捕捉到的异常,必须是在报错的时候,线程执行已经进入 try catch 代码块,且处在 try catch 里面,这个时候才能被捕捉到。**如果是在之前,或者之后,都无法捕捉异常。
# 分情况
# 之前
代码报错的时候,线程执行未进入 try catch,那么无法捕捉异常。
比如语法异常(syntaxError),因为语法异常是在语法检查阶段就报错了,线程执行尚未进入 try catch 代码块,自然就无法捕获到异常。
try{
a.
}catch(e){
console.log("error",e);
}
// output
Uncaught SyntaxError: Unexpected token '}'
2
3
4
5
6
7
# 之中
代码报错的时候,线程执行处于 try catch 之中,则能捕捉到异常。
- 方法和执行都在 try 里面,能捕捉到异常。
try{
function d(){a.b;}
d();
}catch(e){
console.log("error",e);
}
// output
error ReferenceError: a is not defined
2
3
4
5
6
7
8
- 方法定义在外部,执行方法在 try 里面,能捕捉到异常
function d(){a.b;}
try{
d();
}catch(e){
console.log("error",e);
}
// output
error ReferenceError: a is not defined
2
3
4
5
6
7
8
上述报错的时机,都是代码执行进入了 try catch ,执行 d() 方法的时候,线程执行处在 try 里面,所以能捕捉到。
# 之后
代码报错的时候,线程已经执行完 try catch,这种不能捕捉到异常。
try{
a.
}catch(e){
console.log("error",e);
}
// output
Uncaught SyntaxError: Unexpected token '}'
2
3
4
5
6
7
# Promise 没异常
相对于外部 try catch,Promise 没有异常!
异常都被包裹起来了。所以异常都不会被外层的 try catch 捕捉,因此在外层的 try catch 看来,Promise 根本没有异常,事实上也确实没有“异常”,比如:
try{
new Promise(function (resolve, reject) {
a.b; //**try catch 无法捕捉 Promise 的异常,是因为 Promise 的异常没有往上抛。**
}).then(v=>{
console.log(v);
});
console.log(111);
}catch(e){
console.log('error',e);
}
console.log(222);
// output
111
222
Uncaught (in promise) ReferenceError: a is not defined
2
3
4
5
6
7
8
9
10
11
12
13
14
15
显然,a.b 报错之后的,111 和 222 都能正常运行,promise 的异常都已经被内部 catch 了,在外层的 try catch 看来就是没有异常,线程继续执行。
try catch 无法捕捉 Promise 的异常,是因为 Promise 的异常没有往上抛。
再看一例:
function a() {
return new Promise((resolve, reject) => {
// a.b;
setTimeout(() => {
reject(1);
})
// return resolve(3333333)
// return resolve(a.b)
}).then(v => {
console.log(v);
})
// .catch(e => {
// console.log('error1',e);
// })
}
async function test(params) {
try {
await a();
} catch (e) {
console.log('error2', e);
}
console.log(111);
}
test()
//output
// error2 1
// 111
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
报错的时候( setTimeout 里面的 reject ),线程执行已经进入 try catch 代码块,但是并未执行完成,这样的话当然可以捕获到异常。await 将代码执行停留在 try catch 代码块里面的缘故。
不要用 try catch 包裹 Promise , Promise 很强大,不用担心异常会往上抛!我们只需要给 Promise 增加 Promise#catch 就 OK 了
# catch finally
# 相关链接
- https://juejin.cn/post/6994594642280857630
- https://juejin.cn/post/7007031572238958629
- https://juejin.cn/post/7011299888465969166