异步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("操作成功") }).then( value => { console.log("操作成的业务处理 1") }, reason => { console.log("拒绝的业务处理 1") } ).then( value => { console.log("操作成的业务处理 2") }, reason => { console.log("拒绝的业务处理 2") } )
|
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) => { setTimeout(() => { reject("fail") }, 1000) })
new Promise((resolve, reject) => { resolve(p1) }).then( msg => { console.log(msg) }, reason => { console.log("err", reason) } )
|
可见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") }).then( value => { console.log(value) return new Promise((resolve, reject) => { resolve("new info") }) }, 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) => { reject(new Error("oops")) }).then( value => { console.log(value) }, reason => { console.log(reason) } )
|
但如果还捕获判断多个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) )
|
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) } )
|
若有一个发生错误
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) } )
|
注意,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) } )
|
下面模拟一下结合使用.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) )
|
还有一个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) )
|
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)) }) })
|
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())
|
代码优化
下面对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
|
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()
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) }
|
通过上面的例子很好地说明了使用语法糖的好处,它使得异步的代码看上去像同步的代码,极大的提高了可读性。
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()
|
结语
JS异步编程中我所用到的就是这些了,至于JS的任务执行顺序就是另一个问题了,后面找个时间来填吧
参考资料
异步JavaScript简介-MDN Web Docs
你应该学习的 JS 异步编程与Promise…-后盾人编程哔哩哔哩投稿视频