遮罩層沒有消失
我們請求資料時,通常會先開啟一個 loading,資料回來後,做一些處理,然後再将 loading 關閉。
但有時也會出現 loading 沒有關閉的情況。就像這樣:
async function request() {
console.log('開啟遮罩')
let json = await requestUserList() // {1}
// 處理資料...
console.log('關閉遮罩');
}
request()
// 模拟 ajax 請求使用者清單
// code 等于 2000 則為成功,否則是失敗
function requestUserList(json = { code: 2001, msg: '參數不正确', data: {} }) {
if (Object.is(json.code, 2000)) {
Promise.resolve(json)
} else {
Promise.reject(`請求失敗:${json.msg}`)
}
}
由于請求失敗,進入 reject,于是 await(行 {1}) 後面的代碼将不在執行。
async 的本質
async
的本質是生成器。
Tip: 如果沒有接觸過生成器,可以看筆者的另一篇文章 - 疊代器 (Iterator) 和 生成器 (Generator)
接下來我将做一個實驗,首先用
async
寫一個小功能,再用生成器實作相同的功能。請看示例:
// 公共代碼
let seed = 0
// 模拟ajax。一秒後傳回資料
function fetchData(isResolve = true) {
return new Promise((resolve, reject) => {
let method = isResolve ? resolve : reject
setTimeout(() => method({
code: 200,
data: seed++
}), 1000)
})
}
// async 版本
async function fn1() {
let d1 = await fetchData()
console.log(d1)
let d2 = await fetchData()
console.log(d2)
}
fn1()
/*
輸出:
{ code: 200, data: 0 }
{ code: 200, data: 1 }
*/
用生成器實作相同功能:
// 生成器版本
function fn2() {
// 生成器
function* generator() {
let d1 = yield fetchData()
console.log(d1)
let d2 = yield fetchData()
console.log(d2)
}
// 任務執行器
return run(generator)
}
// 任務執行器。
// 由于執行 yield 會導緻暫停目前函數,并等待下一次 next() 的調用,
// 是以我們可以建立一個函數,讓其自動幫我們依次調用 next()
function run(genF) {
return new Promise((resolve, reject) => {
let 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(
v => step(() => gen.next(v)),
e => step(() => gen.throw(e))
)
}
step(() => gen.next())
})
}
fn2()
/*
輸出:
{ code: 200, data: 0 }
{ code: 200, data: 1 }
*/
我們對比下核心功能:
// async 版本
async function fn1() {
let d1 = await fetchData()
console.log(d1)
let d2 = await fetchData()
console.log(d2)
}
// 生成器版本
function* generator() {
let d1 = yield fetchData()
console.log(d1)
let d2 = yield fetchData()
console.log(d2)
}
不同的地方就是:
- async 換成
*
- await 換成了
yield
async 内置執行器
async 通常有如下特性:
-
傳回一個async
Promise
- 隻要有一個
失敗,就不會在執行下面的代碼,直接進入 rejectawait
-
報錯,例如通路一個不存在的變量await
,也會進入 rejectlet d2 = await xx
請看示例:
async function fn1() {
let d1 = await fetchData()
console.log('d1: ', d1);
let d2 = await xx
console.log('d2: ', d2);
}
fn1().catch(() => { console.log('error') })
// d1: { code: 200, data: 0 }
// error
這些特性由 async 内置執行器提供。我們通過上面提到的
run()
方法來分析:
// 任務執行器
function run(genF) {
// 傳回一個 promise
return new Promise((resolve, reject) => {
let gen = genF()
function step(nextF) {
let next
try {
next = nextF()
} catch (e) { // 語句報錯,會被捕獲,并進入到 reject
return reject(e)
}
// 處理疊代器結束的情況
if (next.done) {
return resolve(next.value)
}
// Promise.resolve(11) 等價于 new Promise(resolve => resolve(11))
// 如果給 Promise.resolve() 方法傳入一個 Promise,那麼這個 Promise 會被直接傳回
Promise.resolve(next.value).then( // {1}
v => step(() => gen.next(v)),
// 生成器抛出錯誤,會讓 promise 進入 reject
e => step(() => gen.throw(e)) // {2}
)
}
step(() => gen.next())
})
}
這裡有 2 處需要專門說一下:
Promise.resolve(next.value)
和
gen.throw(e)
。請接着看:
gen.throw(e)
運作
gen.throw(e)
會讓 Promise 進入 reject。
我們将這個現象提取出來做個實驗:建立一個 Promise,在内部的一個函數作用域内在定義一個生成器,并讓其抛出錯誤
let aPromise = new Promise((resolve, reject) => {
(function () {
function* fn1() {
yield 1
yield 2
}
let gen = fn1()
// 抛出錯誤
gen.throw()
}())
})
aPromise.then(() => {
console.log('then')
}).catch(() => {
console.log('catch')
})
// catch
實驗表明:Promise 中的生成器,哪怕内嵌在函數作用域内,一旦生成器抛出錯誤,Promise 将進入 reject。
Promise.resolve(next.value)
Promise.resolve(value)
,隻接受一個參數并傳回一個 Promise,如果給 Promise.resolve() 方法傳入一個 Promise,那麼這個 Promise 會被直接傳回。
它總是傳回一個 promise 對象。這個特性很重要。而且
await 1
(等價于
await Promise.resolve(1)
) 就使用了此特性。
在不知道輸入是同步還是異步的時候,很有用。比如我要統計一個輸入花費時間:
// 可能會報錯
const recordTime = (makeRequest) => {
const timeStart = Date.now();
makeRequest().then(() => { // throws error for sync functions (.then is not a function)
const timeEnd = Date.now();
console.log('time take:', timeEnd - timeStart);
})
}
// 正常運作
const recordTime = async (makeRequest) => {
const timeStart = Date.now();
await makeRequest(); // works for any sync or async function
const timeEnd = Date.now();
console.log('time take:', timeEnd - timeStart);
}
使用 promise 還是 async/await
7 Reasons Why JavaScript Async/Await Is Better Than Plain Promises 這篇文章講述了 7 個
async/await
比
Promise
更好的方面。大部分我是贊同的,比如:
- 更簡潔
const makeRequest = () =>
getJSON()
.then(data => {
console.log(data)
return "done"
})
const makeRequest = async () => {
console.log(await getJSON())
return "done"
}
- 更具可讀性
// 條件 - 有很多條件時,我們會這麼寫:
const makeRequest = () => {
return getJSON()
.then(data => {
if (data.needsAnotherRequest) {
return makeAnotherRequest(data)
.then(moreData => {
console.log(moreData)
return moreData
})
} else {
console.log(data)
return data
}
})
}
// VS
const makeRequest = async () => {
const data = await getJSON()
if (data.needsAnotherRequest) {
const moreData = await makeAnotherRequest(data)
console.log(moreData)
return moreData
} else {
console.log(data)
return data
}
}
// 中間值:請求依賴中間值,可能會這麼寫
const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return promise2(value1)
.then(value2 => {
// do something
return promise3(value1, value2)
})
})
}
// VS
const makeRequest = async () => {
const value1 = await promise1()
const value2 = await promise2(value1)
return promise3(value1, value2)
}
在前面我們已經知道
async/await
的本質就是
生成器
執行器
,而執行器(
run()
方法)是通過 Promise 實作。是以 async 也就是 Promise 的文法糖,并且是一個實作了固定功能的文法糖。
是以,就靈活程度來講,Promise 可能更好。
是以,這兩個東西就不是替換關系,而是有他們各自使用的場景。
以下是我的一些個人習慣:
- 隻有一個異步請求,并且需要處理錯誤情況,傾向
Promise
fetchData(false).then(() => {
// do ...
}).catch(() => {
console.log('error')
})
try {
let p = await fetchData(false)
// do ...
} catch (e) {
console.log('error')
}
-
可以使用async/await
處理同步和異步錯誤try...catch
async () => {
try {
let p1 = await fetchData()
// 依賴于第一個請求的參數值
let p2 = await fetchData(p1.role === 1 ? option1 : option2)
// 其他複雜邏輯
} catch (err) {
// do something...
}
}
- 邏輯比較複雜,比如異步嵌套、條件、中間值等等,使用
async
容易忽略的幾點
await 與并行
如果多個請求沒有依賴關系,就讓他們同時發出。
下面這段代碼是串行:
async function foo() {
let a = await createPromise(1)
let b = await createPromise(2)
}
可以通過下面兩種方法改為并行:
// 方式一
async function foo() {
let p1 = createPromise(1)
let p2 = createPromise(2)
// 至此,兩個異步操作都已經發出
await p1
await p2
}
// 方式二
async function foo() {
let [p1, p2] = await Promise.all([createPromise(1), createPromise(2)])
}
Tip:更多細節請看 async -> await 與并行。
await 與 forEach
請問這段代碼是并行還是串行?
new Array(3).fill(0).forEach(async () => { // {1}
let data = await fetchData()
console.log('data: ', data);
})
答案:并行。
一秒後,控制台同時輸出:
data: { code: 200, data: 0 }
data: { code: 200, data: 1 }
data: { code: 200, data: 2 }
注:如果将
async
(行{1})删除,将會報錯(
SyntaxError: await is only valid in async function
)。
如果需要将上述代碼改為
串行
,可以這樣寫:
async function requestData() {
for (let item of new Array(3).fill(0)) {
let data = await fetchData()
console.log('data: ', data);
}
}
requestData()
// 每過一秒,将會輸出一條記錄
data: { code: 200, data: 0 }
data: { code: 200, data: 1 }
data: { code: 200, data: 2 }
亦或這樣:
async function requestData() {
let allP = []
for (let item of new Array(3).fill(0)) {
allP.push(await fetchData())
}
console.log('allP: ', allP);
}
requestData()
// 三秒後一次性輸出
allP: [
{ code: 200, data: 0 },
{ code: 200, data: 1 },
{ code: 200, data: 2 }
]
async 與 return
請問下面這段代碼将輸出什麼:
// 一半會拒絕
async function halfRefused() {
console.log('0');
// 等待3秒
await new Promise(r => setTimeout(r, 3000));
console.log('1');
const flag = Math.random() >= 0.5
if (flag) {
return 'peng';
}
throw Error('li');
}
async function fn() {
try {
halfRefused();
}
catch (e) {
return 'caught';
}
}
fn().then(v => console.log('resolve', v), v => console.log('reject', v))
console.log('20');
首先輸出:
0
20
resolve undefined
過三秒後輸出:
1
或
1
UnhandledPromiseRejectionWarning: Error: li
運作 fn() 函數:它不會等待,總是進入 resolve,并傳回 undefined。通常,這是錯誤的代碼。
增加 await
async function fn() {
try {
await halfRefused();
}
catch (e) {
return 'caught';
}
}
輸出:
0
20
// 等 3 秒
1
resolve undefined
或
0
20
// 等 3 秒
1
resolve caught
會等待,總是進入 resolve,填充 undefined 或 caught。
增加 return
async function fn() {
try {
return halfRefused();
}
catch (e) {
return 'caught';
}
}
0
20
// 等 3 秒
1
resolve peng
0
20
// 等 3 秒
1
reject Error: li
會等待,resolve "peng" 或 reject "li",catch 塊不會執行
增加 return await
async function fn() {
try {
return await halfRefused();
}
catch (e) {
return 'caught';
}
}
等同于:
async function fn() {
try {
return await halfRefused();
}
catch (e) {
return 'caught';
}
}
0
20
// 等 3 秒
1
resolve peng
0
20
// 等 3 秒
1
resolve caught
會等待,總會進入 resolve,catch 塊會執行。
錯誤堆棧
在
vue-cli + element-ui
這種項目(已配置
source-map
)中測試,發現下面兩種寫法報錯都隻能定位到行{2}。
async
并不能定位到出錯的那一行(行{1}):
const makeRequest = () => {
return fetchData()
.then(() => fetchData())
.then(() => fetchData())
.then(() => fetchData())
.then(() => fetchData())
.then(() => {
throw new Error('oops')
})
}
const makeRequest = async () => {
await fetchData()
await fetchData()
await fetchData()
await fetchData()
await fetchData()
throw new Error('oops') // {1}
}
makeRequest()
.catch(err => {
// {2}
console.log(err)
})
調試
async
有時會比
Promise
更容易調試。請看示例:
const makeRequest = () => {
return fetchData()
.then(() => fetchData())
.then(() => fetchData())
// 不允許這麼寫
.then(() => debugger fetchData())
// 可以
.then(() => {
debugger
fetchData()
})
.then(() => {
throw new Error('oops')
})
}
const makeRequest = async () => {
await fetchData()
await fetchData()
await fetchData()
await fetchData()
// 最佳
debugger
await fetchData()
throw new Error('oops')
}
yield 傳參注意點
直接看示例:
function* fn2() {
let a = yield 1
// 沒有傳參,a 的值也不會是 1
console.log('a: ', a);
let b = yield a + 2
// 傳入 3,是以輸出 6
yield b + 3
}
let gen = fn2()
console.log('gen.next(): ', gen.next())
console.log('gen.next(): ', gen.next())
console.log('gen.next(): ', gen.next(3))
// gen.next(): { value: 1, done: false }
// a: undefined
// gen.next(): { value: NaN, done: false }
// gen.next(): { value: 6, done: false }
作者:彭加李
出處:https://www.cnblogs.com/pengjiali/p/15775855.html
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接。