JavaScript的异步编程
Kotori Y 22 Posts

异步I/O是计算机操作系统对输入输出的一种处理方式:发起I/O请求的线程不等I/O操作完成,就继续执行随后的代码,I/O结果用其他方式通知发起I/O请求的程序。

为什么需要异步

众所周知,JavaScript是单线程的,当遇到等待请求的时候,页面就会卡住,十分影响体验。

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<button class="button1">click</button>
<script>
const btn = document.querySelector('.button1');
btn.addEventListener('click', () => {
alert('You clicked me!'); // 阻塞, 模拟等待

let pElem = document.createElement('p');
pElem.textContent = 'This is a newly-added paragraph.';
document.body.appendChild(pElem);
});
</script>
</body>

在上面的例子中alert()函数就阻塞了下面函数(创造一个p标签)的运行。当服务器请求外部资源时,例如图片,如果要等请求完成再去执行下一步,这无疑是十分低效的,这时就需要用到异步的操作。在JavaScript代码中,主要有两种异步编程风格:老派callbacks,新派promise。下面就来分别介绍。

异步callbacks

异步callbacks 其实就是函数,只不过是作为参数传递给那些在后台执行的其他函数。当那些后台运行的代码结束,就调用callbacks函数,通知你工作已经完成。

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Demo2</title>
<style>
.my-block {
width: 100px;
height: 100px;
background: #1b85f1;
position: relative;
}
</style>
</head>
<body>
<div class="my-block"></div>
<script>
let elem2 = document.querySelector(".my-block")

function start() {
document.querySelector(".btn2").disabled = true

let task1 = setInterval(() => {
elem2.style.left = `${elem2.offsetLeft + 10}px`
if (elem2.offsetLeft >= 100) {
clearInterval(task1)

setTimeout(() => {
elem2.style.left = 0
document.querySelector(".btn2").disabled = false
}, 1000)
}
}, 100)
}

console.log("Demo 2 Start!")
</script>

<button onclick="start()" class="btn2">test</button>
</body>
</html>

在上述例子中,首先通过往setInterval()中传递回调函数使得方块没隔100ms右移10px,直到超过200px。之后再用过一个setTimeout恢复方块的初始位置。在这个简单的例子中,我们就可以看出callbacks方法的弊端——层层嵌套。当我们的任务稍微复杂一点的时候,使用这种方法无疑会使得可读性变得很差,增加后期维护的成本。即所谓的”回调地狱”。幸好,我们有更优雅的解决方法:Promise()

Promises

Promises是新派的异步代码,现代的web APIs经常用到。在JavaScript中通过构建一个Promise()then()来实现P异步,它需要接受一个回调函数,该回调函数提供了两个参数,分别为:成功状态(resolve)和失败状态(reject)。一般代码结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
new Promise((resolve, reject) => {
resolve("操作成功")
// reject("操作失败")
}).then(
value => {
console.log("操作成的业务处理 1")
},
reason => {
console.log("拒绝的业务处理 1")
}
).then(
value => {
console.log("操作成的业务处理 2")
},
reason => {
console.log("拒绝的业务处理 2")
}
// .then()...
)

Promise状态中转

可以将一个Promise的状态转移给另一个Promise, 并且这一状态是无法改变的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let p1 = new Promise((resolve, reject) => {
// resolve("success")
setTimeout(() => {
reject("fail")
}, 1000)
})

new Promise((resolve, reject) => {
resolve(p1)
}).then(
msg => {
console.log(msg)
},
reason => {
console.log("err", reason)
}
)

// output: err fail

可见Promise().then()返回的也是一个Promise()

then()返回值的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
let p1 = new Promise((resolve, reject) => {
resolve("success info")
// reject("rejected info")
}).then(
value => {
console.log(value) // 获取到成功的状态,控制台输出"success info"\
return new Promise((resolve, reject) => {
resolve("new info") // 将新的成功值以Promise的形式传出
})
}, null
).then(value => console.log(value))


简单说Promise()和then()总是成对出现的,上面的例子中第二个then()处理的是第8行返回的Promise。若没有return关键字,处理的就是第一个then()返回的Promise。

Promise的异常处理

Promise()的reject可以直接接受自定义的错误,并通过then()获取并处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
new Promise((resolve, reject) => {
// resolve("200 OK")
reject(new Error("oops"))
// throw new Error("oopa") // 同样有效
}).then(
value => {
console.log(value)
},
reason => {
console.log(reason)
}
)

// Error: oops
// at <anonymous>:3:10
// at new Promise (<anonymous>)
// at <anonymous>:1:1

但如果还捕获判断多个then()中的异常,可以使用catch方法一起捕获。

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
new Promise((resolve, reject) => {
reject("403")
}).then(
value => {
console.log(value)
},
/* 单独处理异常 */
reason => {
console.log(reason)
return new Promise((resolve, reject) => {
reject("404")
})
}
).then(
value => console.log(value)
).catch(
error => console.log(error)
)

// outoput:
// 403
// 404

// 如果注释掉reason回调,结果将只有403

finally方法

此外,Promise还提供了一个finally()方法,无论成功与否,该部分的代码始终会被执行。

Promise批量获取数据

Promise的all接口可以发送同时请求,所有请求都成功才成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("200 #1")
}, 1000)
})

let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("200 #2")
}, 1000)
})

Promise.all([p1, p2]).then(
value => {
console.log(value)
}
).catch(
err => {
console.log(err)
}
)

// ['200 #1', '200 #2']

若有一个发生错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("200 #1")
}, 1000)
})

let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("403 #2")
}, 1000)
})

Promise.all([p1, p2]).then(
value => {
console.log(value)
}
).catch(
err => {
console.log(err)
}
)

// 403 #2

注意,catch()返回的Promise对象默认是resolved状态

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
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("403 #1")
}, 1000)
}).catch(err => console.log(err))

let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("200 #2")
}, 1000)
})

Promise.all([p1, p2]).then(
value => {
console.log(value)
}
).catch(
err => {
console.log(err)
}
)

// 403 #1
// [undefined, '200 #2']

下面模拟一下结合使用.map()实现并发请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function simPost(usrname) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(usrname.length)
}, 1000)
})
}

const users = ["Kotori", "Sorcha", "Catkin", "Juno"]

Promise.all(
users.map(usr => simPost(usr))
).then(
value => console.log(value)
)

// [6, 6, 6, 4]

还有一个allSettled()接口,用法和all()类似,不过这个接口无论子任务是否成功,都返回成功状态。

race()接口将返回先结束的状态,还是拿上面那个例子

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
function simPost(usrname) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(usrname.length)
}, 1000)
})
}

const users = ["Kotori", "Sorcha", "Catkin", "Juno"]

const promise = users.map(usr => simPost(usr));
promise.push(
new Promise((resolve, reject) => {
setTimeout(() => {
reject("请求超时")
}, 500)
})
)

Promise.race(
promise
).then(
value => console.log(value)
).catch(
error => console.log(error)
)

// [6, 6, 6, 4]

Promise的队列

即按顺序运行Promise

map方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function simPost(usrname) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(usrname.length)
}, 1000)
})
}

const users = ["Kotori", "Sorcha", "Catkin", "Juno"]

let promise = Promise.resolve()
users.map(usr => {
promise = promise.then(() => {
return simPost(usr).then(value => console.log(value))
})
})

// 6
// 6
// 6
// 4

reduce方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function simPost(usrname) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(usrname.length)
}, 1000)
})
}

const users = ["Kotori", "Sorcha", "Catkin", "Juno"]

users.reduce((promise, usr) => {
return promise.then(() => {
return simPost(usr).then(value => console.log(value))
})
}, Promise.resolve())

// 6
// 6
// 6
// 4

// 代码更加简洁

代码优化

下面对Demo 2进行优化

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Demo 3</title>
<style>
.my-block-3 {
width: 100px;
height: 100px;
background: #1b85f1;
position: relative;
}
</style>
</head>
<body>
<div class="my-block-3"></div>
<script>
let elem3 = document.querySelector(".my-block-3")

function start() {
document.querySelector(".btn3").disabled = true

const interval = (delay, callback) => {
return new Promise(resolve => {
let task1 = setInterval(() => {
callback(task1, resolve)
}, delay)
})
}

const sleep = (delay) => new Promise(resolve => {
setTimeout(() => {
resolve()
}, delay)
})

interval(100, (task, resolve) => {
if (elem3.offsetLeft >= 100) {
clearInterval(task)
resolve()
}
elem3.style.left = `${elem3.offsetLeft + 10}px`
}).then(() => {
sleep(1000).then(() => elem3.style.left = 0)
.then(() => document.querySelector(".btn3").disabled = false)
})

}
</script>

<button onclick="start()" class="btn3">test</button>
</body>
</html>

可读性和可维护性得到大幅的提升,但是,这么多的”then”,似乎也算不上”优雅”,这时候就需要async/await语法糖来进一步简化代码了。

async与await

简单说,async和await分别是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

// with promise and then

const sleep = (delay) => new Promise(resolve =>
setTimeout(() => {
resolve()
}, delay)
)

function foo() {
sleep(1000)
.then(() => console.log(1))
.then(() => sleep(1000))
.then(() => console.log(2))
}

foo()
// 1
// 2



// with async and await


async function newSleep(delay) {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, delay)
})
}

async function bar() {
await newSleep(1000)
console.log(1)

await newSleep(1000)
console.log(2)
}

// 1
// 2

通过上面的例子很好地说明了使用语法糖的好处,它使得异步的代码看上去像同步的代码,极大的提高了可读性。

class和await结合

没啥好说的,直接贴代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Student {
constructor(name) {
this.name = name
}
async simPost() {
let len = await new Promise((resolve, reject) => {
setTimeout(() => {
return resolve(this.name.length)
}, 1000)
})
console.log(len)
}
}

me = new Student("Kotori")
me.simPost()

// 6

结语

JS异步编程中我所用到的就是这些了,至于JS的任务执行顺序就是另一个问题了,后面找个时间来填吧

参考资料

  1. 异步JavaScript简介-MDN Web Docs

  2. 你应该学习的 JS 异步编程与Promise…-后盾人编程哔哩哔哩投稿视频

  • Post title:JavaScript的异步编程
  • Post author:Kotori Y
  • Create time:2021-11-12 00:46
  • Post link:https://blog.iamkotori.com/2021/11/12/JavaScript的异步编程/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments