JavaScript异步编程演进:从回调到Async/Await

作者:小括号时间:2026-10-30 02:32:22浏览:311

本文梳理了JavaScript异步编程的发展脉络,从早期的回调函数(Callbacks)及其引发的回调地狱问题,到Promise/A+标准的引入,再到生成器函数与co库的尝试,最终演进为ES7中标准的Async/Await语法。文章详细解析了各阶段的技术特点、优缺点及最佳实践,帮助开发者理解异步机制的演变逻辑。

JavaScript异步编程演进概述

《The Evolution of Asynchronous JavaScript》一文系统梳理了JavaScript异步函数的演进历史,涵盖了从最初的回调函数,到Promise/A+规范、生成器函数,直至当前主流的Async函数的发展路径。感谢景庄对该外文资料的翻译,以下内容基于该译文展开。

接下来,我们将共同回顾JavaScript异步函数在过去几年中的发展历程。

回调函数Callbacks的局限

回调函数Callbacks

讨论异步处理,必然要从回调函数说起。

异步JavaScript

在JavaScript中,异步编程主要依赖于函数作为一等公民的特性来实现。这意味着我们可以将函数作为参数传递给另一个函数(即高阶函数),并在其内部调用该传入的函数,这种机制即为回调函数。回调函数的主要作用是在函数内部执行特定动作,它通常不返回任何值(避免使用return)。例如:


Something.save(function(err) {  
 if (err) { 
  //error handling 
  return; // 没有返回值 
 } 
 console.log('success'); 
}); 

上述示例展示了错误优先的回调函数(error-first callbacks)模式,这是Node.js的核心特征之一。Node.js的核心模块及NPM仓库中的大多数模块在开发时均遵循这一规范。

然而,过度依赖回调函数会带来诸多挑战:

若代码组织不当,极易陷入“回调地狱”(callback hell),导致代码可读性极差,难以维护。同时,错误处理代码容易被遗漏。

此外,回调函数无法通过return语句返回值,也无法使用throw关键字抛出异常。

正是由于上述局限性,JavaScript社区一直在探索使异步开发更简便的可行方案。

一种早期的解决方案是async模块。对于长期与回调打交道的开发者而言,若需实现并行执行、串行执行,或使用异步函数映射数组元素,操作往往十分复杂。Caolan McMahon编写的async模块正是为了解决这些问题。

借助async模块,代码编写方式可简化为:


async.map([1, 2, 3], AsyncSquaringLibrary.square,  
 function(err, result){ 
 // result will be [1, 4, 9] 
}); 

尽管async模块提供了一定便利,但代码可读性依然有限,因此Promise应运而生。

Promise标准的引入

Promise

当前的JavaScript异步标准可追溯至2012年,并在ES6中成为可用特性。然而,“Promise”这一术语并非JavaScript社区首创,它源自Daniel P. Friedman于1976年发表的一篇论文。

Promise代表一个异步操作的最终结果。

若使用Promise实现前述功能,代码风格如下:


Something.save()  
 .then(function() { 
  console.log('success'); 
 }) 
 .catch(function() { 
  //error handling 
 }) 

可以看出,Promise内部依然利用了回调函数。在then和catch方法中传入的回调函数,分别在Promise状态变为resolved或rejected时执行。Promise的优势在于支持链式调用以完成一系列任务,例如:


saveSomething()  
 .then(updateOtherthing) 
 .then(deleteStuff)  
 .then(logResults); 

当原生Promise功能不足时,可借助如bluebird等Promise库。这些库通常提供比原生方案更丰富的功能,且不受限于Promise/A+标准。

为何不直接使用糖语法(sugar methods)?建议先阅读《Promise: The Extension Problem》一文。更多关于Promise的详细信息,可参考Promise/A+标准。

若大部分库仅暴露回调接口,该如何使用Promise?

解决方法很简单,只需使用Promise包裹包含回调的函数调用即可。例如:

原始的回调风格代码可能如下:


function saveToTheDb(value) { 
  db.values.insert(value, function (err, user) { 
    if (err) throw err; 
 
    // todo: insert user to db 
  }); 
} 

将其改造为支持Promise风格的调用后:


function saveToTheDb(value) {  
  return new Promise(function(resolve, reject) { 
    db.values.insert(value, function(err, user) { // remember error first ;) 
      if (err) { 
        return reject(err); // don't forget to return here 
      } 
      resolve(user); 
    }) 
  } 
} 

目前已有相当多的库和框架同时支持回调风格和Promise风格的API。若需对外提供库,最佳实践是同时提供两种接口。可通过以下方式实现:


function foo(cb) {  
 if (cb) { 
  return cb(); 
 } 
 return new Promise(function (resolve, reject) { 
 
 }); 
} 

或者更简单地,从仅提供Promise风格接口开始,并使用如callbackify等工具实现向后兼容。Callbackify的实现原理与上述代码类似,但采用了更通用的方法,建议阅读其源代码。

生成器函数与co库

生成器Generators/ yield

JavaScript生成器是ES6(即ES2015)引入的新特性,相对较新。设想以下场景:

在函数执行过程中,可在某一点暂停执行,执行其他操作后,再携带新值返回该函数继续执行。这正是JavaScript生成器函数旨在解决的问题。调用生成器函数时,它不会立即执行,而是返回一个迭代器,需手动调用next方法进行迭代。迭代器会遍历每个中断点。


function* foo () {  
 var index = 0; 
 while (index < 2) { 
  yield index++; //暂停函数执行,并执行yield后的操作 
 } 
} 
var bar = foo(); // 返回的其实是一个迭代器 
console.log(bar.next());  // { value: 0, done: false }  
console.log(bar.next());  // { value: 1, done: false }  
console.log(bar.next());  // { value: undefined, done: true } 

若希望更轻松地使用生成器编写异步代码,可使用由tj开发的co库。

Co是一个基于生成器的流程控制工具,适用于Node.js和浏览器。借助Promise,可使用更优雅的方式编写非阻塞代码。使用co改写前述示例:


co(function* (){  
 yield Something.save(); 
}).then(function() { 
 // success 
}) 
.catch(function(err) { 
 //error handling 
}); 

如何实现并行操作?答案比想象中简单,如下(本质即Promise.all):


yield [Something.save(), Otherthing.save()];  
Async/ await

Async/Await标准语法

ES7(尚未正式标准化)引入了Async函数的概念。目前若需使用,需借助babel等语法转换器将其编译为ES5代码(注意:此处讨论的是async关键字,而非NPM中的async包)。

简言之,使用async关键字,可轻松实现此前使用生成器和co函数完成的工作(当然,不包括那些hack手段)。

既然ES7引入了async关键字,yield是否变得不再重要?

实际上,使用yield处理异步只是一种hack。yield的本质是懒序列(lazy sequences)和迭代器。而await能完美分离这两点:让yield回归其最初目的,使用await执行异步操作。

Async函数底层基于Promise实现,这也是async函数返回Promise的原因。

因此,使用async函数完成前述工作,可重新编写为:


async function save(Something) {  
 try { 
  await Something.save(); // 等待await后面的代码执行完,类似于yield 
 } catch (ex) { 
  //error handling 
 } 
 console.log('success'); 
} 

如所见,使用async函数需在函数声明前添加async关键字。此后,可在函数内部使用await关键字,其作用与之前的yield类似。

使用async函数完成并行任务的方式与yield类似,唯一区别是Promise.all不再隐式执行,需显式调用:


async function save(Something) {  
  await Promise.all[Something.save(), Otherthing.save()] 
} 

Koa也支持async函数。若使用Koa,现在即可借助babel启用这一特性。


import koa from koa;  
let app = koa(); 
app.experimental = true; 
app.use(async function (){  
  this.body = await Promise.resolve('Hello Reader!') 
}) 
app.listen(3000);  

以上内容分享了JavaScript异步函数的发展历程,希望对大家有所帮助。

作者最新文章