遮罩层没有消失

我们请求数据时,通常会先开启一个 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)
}

不同的地方就是:

  1. ​async​换成*
  2. ​await​换成了yield

async 内置执行器

async 通常有如下特性:

  • async​ 返回一个Promise
  • 只要有一个await 失败,就不会在执行下面的代码,直接进入​reject​
  • await​ 报错,例如访问一个不存在的变量let d2 = await xx,也会进入​reject​

请看示例:

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 }

作者:彭加李

欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。