今天就讲 async/await
这组 API
async/await
是 ES7
的标准,Promise
是 ES6
标准,async/await
这套 API 也是用来帮助我们写异步代码的,它是构建在 Promise
之上的,有点像 Okhttp
和 Retrofit
的关系
什么是 async ?
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await doSomeThing()
}
catch (rejectedValue) {
// …
}
}
大概长上面这个样子。async
一般不单独使用,而是和 await
一起使用,一个 async
函数内部可能有 零个 或者 多个 await
。
这段代码即使没有学过 Promise
也很容易看懂。这就是 async
函数的优势所在。
async
函数被调用的时候,会立即返回一个 Promise
。
await
await
不能单独使用,如果在非 async
函数内部被调用会报错。await
后面一般跟一个 Promise
,也可以是其他的,比如一个数值,或者一个变量,或者一个函数。如果 await
后面不是一个 Promise
就会返回一个已经 resolve
的 Promise
。
当 async
函数执行到 await
的时候,会暂停整个 async
函数的执行进程并出让其控制权,只有当其等待的基于 Promise
的异步操作被兑现或被拒绝之后才会恢复进程。
当然,async
函数也会返回一个 Promise
,也就是说,await
后面也可以跟一个 async
函数。
async/await
这套 API ,感觉有 Kotlin
中的 协程 和 挂起函数 内味。
为什么要使用 async?
隐藏 Promise ,更易于理解
假设我们想请求一个接口,然后把响应的数据打印出来,并且捕获异常。用 `Promise` 大概是这样写:
function logFetch(url) {
return fetch(url)
.then(response => response.text())
.then(text => {
console.log(text)
}).catch(err => {
console.error('fetch failed', err)
})
}
如果用 async 函数来写,大概是这个样子:
async function logFetch(url) {
try {
const response = await fetch(url)
console.log(await response.text())
}
catch (err) {
console.log('fetch failed', err)
}
}
虽然代码的行数差不多,但是代码看起来更加简洁,少了很多 then
的嵌套。请求一个接口数据,然后打印,就像你看到的,很简单。
就像刚才的那个例子,如果没学过 Promise
也不影响你看懂和理解 async
代码。这个好像没什么,但是对于一个大型的项目,人员的技术水平参差不齐,不能保证所有人都熟悉和理解 Promise
,我们要做的是尽可能的降低代码的理解难度,使大部分人都能看懂
用同步的思路写异步逻辑
`async/await` 最大的优势就是我们可以用同步的思路来写异步的业务逻辑,所以代码整体看起来更加容易看懂。我们举个例子。
上面的代码还是比较简单,我们再举一个复杂一点的例子。
我们想获取一个网络资源的大小,如果使用 Promise
大概可能是这个样子:
function getResponseSize(url) {
return fetch(url).then(response => {
const reader = response.body.getReader()
let total = 0
return reader.read().then(function processResult(result) {
if (result.done) return total
const value = result.value
total += value.length
console.log('Received chunk', value)
return reader.read().then(processResult)
})
})
}
即使你学过 Promise
,这个代码也并不是很好理解,更别说是没有学过 Promise
的同学了。因为中间有一个循环的过程,而且这个执行的过程的异步的,并不想我们之前学到的一个链式调用能解决的。当然,你也可以抽取一下,使代码更简洁一点。
const processResult = (result) =>{
if (result.done) return total
const value = result.value
total += value.length
console.log('Received chunk', value)
return reader.read().then(processResult)
}
function getResponseSize(url) {
return fetch(url).then(response => {
const reader = response.body.getReader()
let total = 0
return reader.read().then(processResult)
})
}
但是多了一个方法,勉勉强强吧。但是我们看一下 async
函数是怎么处理的。
async function getResponseSize(url) {
const response = await fetch(url)
const reader = response.body.getReader()
let result = await reader.read()
let total = 0
while (!result.done) {
const value = result.value
total += value.length
console.log('Received chunk', value)
// get the next result
result = await reader.read()
}
return total
}
OHHHHHHHHH
这个是不是看起来就更加流畅了?因为 await
表达式会阻塞运行,甚至可以直接阻塞循环,所以整体看起来像同步的代码,也更符合直觉,更容易读懂这个代码。
小心 await 阻塞
由于 await
能够阻塞 async
函数的运行,所以代码看起来更像同步的代码,更容易阅读和理解。但是要小心 await
阻塞,因为有些阻塞是不必要的,不恰当使用可能会影响代码的性能。
假如我们要把一个网络数据和本地数据合并,错误的实例可能是这样子:
async function combineData(url, file) {
let networkData = await fetch(url)
let fileData = await readeFile(file)
console.log(networkData + fileData)
}
其实我们不用等一个文件读完了,再去读下个文件,我们可以两个文件一起读,读完之后再进行合并,这样能提高代码的运行速度。我们可以这样写:
async function combineData(url, file) {
let fetchPromise = fetch(url)
, readFilePromise = readFile(file)
, networkData = await fetchPromise
, fileData = await readFilePromise
console.log(networkData + fileData)
}
这样的话,就可以同时 网络请求 和 读取文件 了,可以节省很多时间。这里主要是利用了 Promise
一旦创建就立刻执行的特点,不懂的同学可以复习一下上一节的内容。
当然,如果你熟悉 Promise
的话,可以直接使用 Promise.all
的方式来处理,或者 await
后面跟 Promise.all
这里就不展开讲了。
异常处理
try...catch
在
async
函数中,异常处理一般是try...catch
,如果没有进行try...catch
,await
表达式一旦reject
,async
函数返回的Promise
就会reject
。
其实结合 Promise
来看,如果一个 Promise
状态敲定为 reject
,并且后续的 then
没有传入 reject
函数,或者没有 catch
,那么就会抛出异常。从这个角度来看,在 async
函数中用 try...catch
来包住 await
表达式,可能就是 catch
住这个异常,并且把这个 reject
信息传到 catch
里面。
这里就不举例子了。
链接:juejin.cn/post/696700…
来源:掘金