本文深入解析JavaScript异步编程的三种核心模式。首先介绍基础的回调函数机制及其导致的回调地狱问题;接着阐述ES6中Promise规范如何优雅处理异步结果与错误;最后详解Generator生成器函数配合yield关键字,如何进一步简化异步流程控制,避免深层嵌套。
JavaScript异步编程概述
本文通过具体实例详细解析JavaScript中的异步编程技术,旨在为开发者提供参考与学习思路。
“异步函数”这一术语有时略显名不副实,因为常规调用中,程序必须等待函数返回结果后才会继续执行后续代码。在JavaScript语境下,若称某函数为“异步”,意指该函数会触发另一个函数的执行,且后者从事件队列中获取任务。若此后续函数作为参数传递给前者,则被称为“回调函数”。
回调函数机制与局限
回调函数
回调函数是JavaScript异步编程中最基础且常用的实现方式。
通过采用回调机制,同步操作被转化为异步操作。主函数无需阻塞程序运行,而是先执行核心逻辑,将耗时较长的操作推迟执行,从而提升程序响应效率。
回调函数的优势在于实现简单、逻辑直观且易于部署;然而,其缺点也显而易见,即随着嵌套层级增加,代码可读性与维护性显著下降。
以下定义了一个名为delay的异步函数,其作用是延迟指定函数的执行,从而允许当前正在运行的程序继续执行。delay函数的具体实现如下:
function delay(time, callback) {
setTimeout(function () {
callback("Slept for "+time);
}, time);
}
若需连续执行两次delay操作,代码应如何编写?
delay(1000, function(msg) {
console.log(msg);
delay(1200, function (msg) {
console.log(msg);
}
})
//...waits 1000ms
// > "Slept for 1000"
//...waits another 1200ms
// > "Slept for 1200"
通过上述方式可确保两个delay操作按顺序依次执行。然而,若嵌套层级过多,便会形成所谓的“回调地狱”。当异步任务数量庞大时,维护大量嵌套的回调函数将变得极其困难。
Promise对象的应用
Promise
Promise是ES6标准中纳入的重要规范,目前各大主流框架均已实现了相关方法。
可以将Promise理解为一种“承诺”机制。若函数A调用函数B,B返回一个Promise对象给A,A即可据此制定执行计划:当B成功返回结果时,A执行方案1;若B未能返回预期结果(即拒绝),A则执行方案2。这种机制将所有潜在风险控制在A的可控范围内。
以下是ES6中Promise用法的示例代码:
'use strict';
var promiseCount = 0;
function testPromise() {
var thisPromiseCount = ++promiseCount;
var log = document.getElementById('log');
log.insertAdjacentHTML('beforeend', thisPromiseCount +') Started (<small>Sync code started</small>)<br/>');
// 我们创建一个新的Promise,期望3秒后得到结果
var p1 = new Promise(
//当Promise解决或拒绝时该函数被调用
function(resolve, reject) {
log.insertAdjacentHTML('beforeend', thisPromiseCount +') Promise started (<small>Async code started</small>)<br/>');
// 创建异步操作
window.setTimeout(
function() {
// 满足Promise
resolve(thisPromiseCount);
}, Math.random() * 2000 + 1000);
});
// 当Promise被满足时执行
p1.then(
// 输出信息和值
function(val) {
log.insertAdjacentHTML('beforeend', val +') Promise fulfilled (<small>Async code terminated</small>)<br/>');
})
.catch(
// 当Promise被拒绝时执行
function(reason) {
console.log('Handle rejected promise ('+reason+') here.');
});
log.insertAdjacentHTML('beforeend', thisPromiseCount +') Promise made (<small>Sync code terminated</small>)<br/>');
}
快速连续执行上述函数,可获得如下结果:

上述示例表明,1和2两个异步操作完成后,后续代码按正常顺序执行完毕。关于Promise的更多详细用法,可参考MDN文档。
许多JavaScript框架也提供了Promise相关方法,此处以jQuery为例进行说明。
$("button").bind( "click", function() {
$("p").append( "Started...");
$("div").each(function( i ) {
$( this ).fadeIn().fadeOut( 1000 * (i+1) );
});
$( "div" ).promise().done(function() {
$( "p" ).append( " Finished! " );
});
});
从代码可见,当$("div")选择器关联的所有任务执行完毕后,将自动调用最后的done操作。
jQuery中的Promise同样可以代表多种执行结果,针对不同结果会分别调用相应的回调函数。以ajax调用为例:在1.5版本之前,代码必须编写为如下形式:
$.get('/getdata',{
success:onSuccess,
failure:onFailure,
always:onAlways
});
而在1.5及更高版本中,由于引入了Promise对象,代码可简化为如下形式:
var promise = $.get('/getdata');
promise.done(onSuccess);
promise.fail(onFailure);
promise.always(onAlways);
这种变化的优势何在?为何必须在触发ajax调用后再附加回调?若ajax需实现多种效果,如触发动画、插入HTML、锁定输入等,仅由负责发出请求的应用代码处理所有这些效果显然不够优雅。通过仅传递Promise对象,代码结构显得更加简洁清晰。
更多详细内容请参考jQuery官方文档。
尽管Promise机制十分优雅,但它主要解决了回调地狱问题。真正简化JavaScript异步编程的,还是Generator(生成器)技术。
Generator生成器详解
Generator
生成器是ES6标准中引入的语法特性。
那么,生成器究竟是什么?让我们先观察以下代码:
function* quips(name) {
yield "hello " + name + "!";
yield "i hope you are enjoying the blog posts";
if (name.startsWith("X")) {
yield "it's cool how your name starts with X, " + name;
}
yield "see you later!";
}
没错,这就是JavaScript代码。它是否与你以往认知的JavaScript有所不同?此处的函数被称为“生成器函数”。
生成器函数与普通函数的区别主要体现在以下方面:
普通函数以function关键字开头,而生成器函数则以function*开头。
生成器函数中包含一个特殊关键字yield,其作用是暂停函数执行。配合next方法调用,可实现函数的逐步执行。
以下展示next方法的具体使用方式:
> var iter = quips("jorendorff");
[object Generator]
> iter.next()
{ value: "hello jorendorff!", done: false }
> iter.next()
{ value: "i hope you are enjoying the blog posts", done: false }
> iter.next()
{ value: "see you later!", done: false }
> iter.next()
{ value: undefined, done: true }
可见,每次调用next方法后,生成器函数便会执行至下一个yield位置处。
接下来讲解如何利用生成器函数替代回调函数。仍以最初多次延迟delay操作为例。
首先,我们需要定义一个生成器:
function* myDelayedMessages() {
/* delay 1000 ms and print the result */
/* delay 1200 ms and print the result */
}
随后,我们需要设置delay时间以执行指定操作,此处暂定为空函数。
function* myDelayedMessages() {
console.log(delay(1000, function(){}));
console.log(delay(1200, function(){}));
}
接着,我们使用yield关键字:
function* myDelayedMessages() {
console.log(yield delay(1000, function(){}));
console.log(yield delay(1200, function(){}));
}
最后,我们需要指定一个名为run的函数来调用生成器函数的next方法,并将空函数替换为参数resume:
function run(generatorFunction) {
var generatorItr = generatorFunction(resume);
function resume(callbackValue) {
generatorItr.next(callbackValue);
}
generatorItr.next()
}
最终执行代码如下:
run(function* myDelayedMessages(resume) {
console.log(yield delay(1000, resume));
console.log(yield delay(1200, resume));
})
//...waits 1000ms
// > "Slept for 1000"
//...waits 1200ms
// > "Slept for 1200"
通过上述方式,我们成功避免了回调地狱问题。
如需了解更多JavaScript相关内容,可查看本站专题:《javascript面向对象入门教程》、《JavaScript中json操作技巧总结》、《JavaScript切换特效与技巧总结》、《JavaScript查找算法技巧总结》、《JavaScript动画特效与技巧汇总》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》。
希望本文所述内容能对大家的JavaScript程序设计有所帮助。