本文深入解析JavaScript异步编程的演进,从Generator函数的执行机制、yield暂停与传参特性,到基于Promise的自动执行器实现,最终过渡到ES2017标准的async/await语法。通过对比传统回调、Promise链式调用及Generator方案,阐述async/await如何实现类同步的扁平化异步代码编写体验。
Generator 异步方案
相较于传统的回调函数处理异步调用的方式,Promise 的主要优势在于支持链式调用,从而解决了回调地狱(Callback Hell)的问题。然而,即便消除了嵌套,链式调用中依然充斥着大量的回调函数,其可读性仍未能达到传统同步代码的标准。若采用如下所述的异步代码编写方式,代码结构将更加简洁且易于阅读。
// like sync mode
try{
const value1 = ajax('/api/url1')
console.log(value1)
const value2 = ajax('/api/url1')
console.log(value2)
const value3 = ajax('/api/url1')
console.log(value3)
const value4 = ajax('/api/url1')
console.log(value4)
const value5 = ajax('/api/url1')
console.log(value5)
}catch(err){
console.log(err)
}ES2015 标准引入了生成器函数(Generator Function)。其语法与普通函数的区别在于,在 function 关键字之后与函数名之前,需添加一个“*”符号作为生成器函数的标识。
调用生成器函数时,函数体并不会立即执行,而是返回一个生成器对象。只有当手动调用该对象的 next 方法时,函数体才会开始执行。在函数内部,可以使用 yield 关键字向外返回一个值,并通过 next 方法的返回值获取该值。此外,返回值对象中还包含一个 done 属性,用于指示生成器是否已执行完毕。
yield 关键字不会像 return 那样终止函数的执行,它仅暂停函数的执行。当外部再次调用 next 方法时,函数将从 yield 所在的位置继续向下执行。
function * foo () {
console.log('start')
yield 'foo'
}
const generator = foo()
const result = generator.next()
若在调用 next 方法时传入参数,该参数将作为上一次 yield 表达式的返回值。
function * foo () {
console.log('start')
// 我可以在这里接收next传入的参数
const res = yield 'foo'
console.log(res) // 这是我传入的参数
}
const generator = foo()
const result = generator.next('这是我传入的参数')
console.log(result) // { value: 'foo', done: false }
如果调用生成器函数的 throw 方法,该方法会在生成器函数内部抛出一个异常。
function * foo () {
console.log('start')
// 我可以在这里接收next传入的参数
try {
const res = yield 'foo'
console.log(res) // 这是我传入的参数
} catch (err) {
console.log(err.message) // 抛出错误
}
}
const generator = foo()
const result = generator.next('这是我传入的参数')
console.log(result)
generator.throw(new Error('抛出错误'))结合生成器函数与 Promise 来实现异步编程,可以获得更佳的开发体验。
function ajax(url) {
return new Promise((resove, reject) => {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
// 新方法可以直接接受一个j对象
xhr.responseType = 'json'
xhr.onload = function () {
if (this.status === 200) {
resove(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
function* main() {
const user1 = yield ajax('/json1.json')
console.log(user1)
const user2 = yield ajax('/json2.json')
console.log(user2)
const user3 = yield ajax('/json3.json')
console.log(user3)
}
const g = main()
const result = g.next()
result.value.then(data => {
const result2 = g.next(data)
if (result2.done) return
result2.value.then(data2 => {
const result3 = g.next(data2)
if (result3.done) return
result3.value.then(data3 => {
g.next(data3)
})
})
})显然,生成器的执行器可以通过递归方式来实现调用逻辑。
const g = main()
function handleResult(result) {
if (result.done) return
result.value.then(data => {
handleResult(g.next(data))
}, err => {
g.throw(err)
})
}
handleResult(g.next())由于生成器函数的调用模式具有相似性,我们可以封装一个通用的执行器函数。
function co(generator) {
const g = generator()
function handleResult(result) {
if (result.done) return
result.value.then(data => {
handleResult(g.next(data))
}, err => {
g.throw(err)
})
}
handleResult(g.next())
}
co(main)社区中已经存在较为完善的此类执行器库,例如 co。在 2015 年之前,co 方案非常流行。随着 ES2017 中 async/await 语法糖的推出,Generator 方案的普及度有所下降。使用 Generator 方法最显著的变化是使异步调用回归到了扁平化的结构。
async/await
引入 Generator 后,JavaScript 异步编程的体验已接近同步代码,但手动编写执行器函数较为繁琐。ES2017 标准新增了 async 函数,它同样提供了扁平化的编程体验,并且是语言层面标准的异步编程语法。实际上,async 函数是 Generator 函数更便捷的语法糖,因此在语法上与 Generator 函数类似。
async function main() {
try {
const user1 = await ajax('/json1.json')
console.log(user1)
const user2 = await ajax('/json2.json')
console.log(user2)
const user3 = await ajax('/json3.json')
console.log(user3)
} catch (error) {
console.log(error)
}
}
main()async 函数返回一个 Promise 对象,这有利于对整体代码流程进行更清晰的控制。
promise.then(() => {
console.log('all completed')
}).catch(err => {
console.log(err)
})至此,关于 JS 异步编程中 Generator 与 async/await 语法糖的详解介绍完毕。如需了解更多相关内容,可搜索本站历史文章或继续浏览后续文章,感谢大家的支持!