深入理解JavaScript异步编程机制与演进
本文深入解析JavaScript单线程模型下的异步执行机制,详细阐述事件循环、回调函数、事件监听、观察者模式及Promise状态机等核心概念,并介绍ES6对异步编程的支持,帮助开发者掌握避免阻塞、优化代码结构的关键技术。
异步机制与单线程模型
一、异步机制
JavaScript的运行环境采用单线程模式,这种设计的优势在于执行环境相对简单,无需处理多线程编程中常见的资源同步、死锁等复杂问题。然而,其弊端在于若某个任务执行耗时较长,后续任务将被迫等待,导致浏览器端出现界面假死、鼠标无响应等现象。因此,在浏览器环境中,耗时较长的操作应异步执行,以维持页面的交互响应性。异步执行与同步执行不同,后者要求程序执行顺序与任务排列顺序一致;而异步执行中,每个任务包含一个或多个回调函数(callback),前一个任务结束后,系统执行的是回调函数而非直接执行下一个任务,且后一个任务无需等待前一个任务结束即可启动,导致程序执行顺序与任务排列顺序不一致。既然JavaScript是单线程的,它是如何实现异步执行的?
Javascript线程模型和事件驱动
二、Javascript线程模型和事件驱动
JavaScript采用基于事件循环的并发模式,这与C语言和Java等语言有显著区别。
运行时的概念

栈:函数调用会形成堆栈帧。
function f(b){
var a = 12;
return a+b+35;
}
function g(x){
var m = 4;
return f(m*x);
}
g(21);
当调用函数g时,系统创建第一个包含g参数和局部变量的栈帧。若g函数内部调用f函数,则系统创建包含f参数和局部变量的第二个栈帧,并将其压入第一个栈帧之上。当f函数返回时,顶部的栈帧元素被弹出(仅保留g调用)。当g函数也返回时,栈变为空。
堆:堆是一个大型的非结构化内存区域,对象在此分配。队列:JavaScript运行环境包含一个消息队列,该队列是一系列待执行消息的列表。每条消息都与一个函数关联。当栈为空时,系统从消息队列中取出一条消息并处理,处理过程包括调用相关函数(从而生成初始化的栈帧)。当栈再次为空时,消息处理结束。事件循环
事件循环的名称源于其实现方式,通常如下所示:
while(queue.waitForMessage()){
queue.processNextMessage();
}
queue.waitForMessage同步等待一条消息。
1、运行到完成:每条消息必须完全处理完毕后,其他消息才会被处理。这种机制确保函数无法被中断,只能等待其他函数执行完毕(包括修改数据的操作)。这与C语言不同,例如在C中,若一个函数在线程中运行,它可以在任意点暂停,而在另一个线程中运行其他代码。这种模式的缺点是,若某条消息处理时间过长,Web应用程序将无法响应用户交互(如点击或滚动)。浏览器可通过弹出“脚本运行时间过长”对话框来缓解此问题。最佳实践是保持消息处理简短,若可能,将一条消息拆分为多条消息。 2、添加消息:在网页浏览器中,事件可随时添加,只要事件发生且绑定了事件监听器。若无事件监听,事件将被丢弃。例如点击元素并绑定点击事件。调用setTimeout时,传入第二个参数(时间),系统会将一条消息添加到队列中。若队列中无其他消息,该消息将立即处理;若有其他消息,则setTimeout的消息需等待其他消息处理完毕。因此,第二个参数表示最小时间,而非保证时间。 3、多个运行环境间的通信:Web Worker或跨域iframe拥有独立的栈、堆和消息队列。不同运行环境只能通过postMessage方法发送消息进行通信,该方法会将一条消息添加到另一个运行环境的队列中,前提是后者监听消息事件。
从不阻塞:事件循环模型是JavaScript的一个有趣特性,它从不阻塞。假设浏览器中有一个专门用于事件调度的实例(可能是一个线程,称为事件分发线程event dispatch thread),该实例的工作是一个无限循环,从事件队列中取出事件,处理所有与事件关联的回调函数(event handler)。请注意,回调函数在JavaScript的主线程中运行,而非事件分发线程,以确保事件处理不会发生阻塞。通过事件和回调进行的I/O操作是典型表现,因此当应用等待索引数据库查询或XHR请求返回时,它仍可处理其他事情,如用户输入。
回调函数及其局限性
三、回调
回调是JavaScript的基础,函数作为参数传递。示例如下:
f1();
f2();
f3();
若f1中执行大量耗时操作,且f2需在f1之后执行,则程序可改为回调形式。如下:
function f1(callback){
setTimeout(function () {
// f1的大量耗时任务代码并的到三个结果i,l,you.
console.log("this is function1");
var i = "i", l = "love", y = "you";
if (callback && typeof(callback) === "function") {
callback(i,l,y);
}
}, 50);
}
function f2(a, b, c) {
alert(a + " " + b + " " + c);
console.log("this is function2");
}
function f3(){console.log("this is function3");}
f1(f2);
f3();
运行结果:
this is function3
this is function1
i love you
this is function2
采用这种方式,我们将同步操作转化为异步操作,f1不会阻塞程序运行,相当于先执行主要逻辑,将耗时操作推迟执行。 回调函数的优点是简单、轻量级(无需额外库)。缺点是各部分高度耦合(Coupling),流程混乱,且每个任务只能指定一个回调函数。若某操作需经过多个非阻塞IO操作,每个结果通过回调传递,将产生意大利面条式(spaghetti)代码。
operation1(function(err, result) {
operation2(function(err, result) {
operation3(function(err, result) {
operation4(function(err, result) {
operation5(function(err, result) {
// do something useful
})
})
})
})
})
事件监听与观察者模式
四、事件监听
另一种思路是采用事件驱动模式。任务执行不取决于代码顺序,而取决于某个事件是否发生。
// plain, non-jQuery version of hooking up an event handler
var clickity = document.getElementById("clickity");
clickity.addEventListener("click", function (e) {
//console log, since it's like ALL real world scenarios, amirite?
console.log("Alas, someone is pressing my buttons…");
});
// the obligatory jQuery version
$("#clickity").on("click", function (e) {
console.log("Alas, someone is pressing my buttons…");
});
也可自定义事件进行监听,关于自定义事件,属于另一部分内容。这种方法的优点是易于理解,可绑定多个事件,每个事件可指定多个回调函数,且能“去耦合”(Decoupling),有利于实现模块化。缺点是整个程序需变为事件驱动型,运行流程变得不清晰。
五、观察者模式
假定存在一个“信号中心”,某任务执行完成后,向信号中心“发布”(publish)一个信号,其他任务可向信号中心“订阅”(subscribe)该信号,从而知晓何时开始执行。这称为“发布/订阅模式”(publish-subscribe pattern),又称“观察者模式”(observer pattern)。
var pubsub = (function(){
var q = {}
topics = {},
subUid = -1;
//发布消息
q.publish = function(topic, args) {
if(!topics[topic]) {return;}
var subs = topics[topic],
len = subs.length;
while(len--) {
subs[len].func(topic, args);
}
return this;
};
//订阅事件
q.subscribe = function(topic, func) {
topics[topic] = topics[topic] ? topics[topic] : [];
var token = (++subUid).toString();
topics[topic].push({
token : token,
func : func
});
return token;
};
return q;
//取消订阅就不写了,遍历topics,然后通过保存前面返回token,删除指定元素
})();
//触发的事件
var f2 = function(topics, data) {
console.log("logging:" + topics + ":" + data);
console.log("this is function2");
}
function f1(){
setTimeout(function () {
// f1的任务代码
console.log("this is function1");
//发布消息'done'
pubsub .publish('done', 'hello world');
}, 1000);
}
pubsub.subscribe('done', f2);
f1();
上面代码的运行结果为:
this is function1
logging:done:hello world
this is function2
观察者模式的实现方法多样,也可直接借用第三方库。这种方法的性质与“事件监听”类似(观察者模式和自定义事件非常相似),但明显优于后者。观察者模式和事件监听均具有良好的去耦性,且拥有消息中心,通过对消息中心的处理,可良好地监控程序运行。
Promises对象与状态机
六、Promises对象
Promises的概念由CommonJS小组成员在Promises/A规范中提出。Promises逐渐被用作管理异步操作回调的方法,但因其设计,其用途远不止于此。Promise允许我们以同步方式编写代码,同时给予我们代码的异步执行能力。
function f1(){
var def = $.Deferred();
setTimeout(function () {
// f1的任务代码
console.log("this is f1");
def.resolve();
}, 500);
return def.promise();
}
function f2(){
console.log("this is f2");
}
f1().then(f2);
上面代码的运行结果为:
this is f1
this is f2
上面引用的是jQuery对Promises/A的实现,jQuery中还有一系列方法,具体可参考:Deferred Object。关于Promises,强烈建议阅读You're Missing the Point of Promises。还有许多第三方库实现了Promises,如:Q、Bluebird、mmDeferred等。Promise(中文:承诺)实际上为一个有限状态机,共有三种状态:pending(执行中)、fulfilled(执行成功)和rejected(执行失败)。其中pending为初始状态,fulfilled和rejected为结束状态(结束状态表示promise的生命周期已结束)。状态转换关系为:pending->fulfilled,pending->rejected。随着状态的转换将触发各种事件(如执行成功事件、执行失败事件等)。下节具体讲述状态机实现js异步编程。
七、状态机
Promises的本质实际就是通过状态机来实现的,把异步操作与对象的状态改变挂钩,当异步操作结束的时候,发生相应的状态改变,由此再触发其他操作。这要比回调函数、事件监听、发布/订阅等解决方案,在逻辑上更合理,更易于降低代码复杂度。关于Promises可参考:JS魔法堂:剖析源码理解Promises/A规范 。
ES6对异步的支持
八、ES6对异步的支持
这是一项新技术,成为2015年的ECMAScript(ES6)标准的一部分。该技术的规范已经完成,但实施情况在不同的浏览器不同,在浏览器中的支持情况如下。

var f1 = new Promise(function(resolve, reject) {
setTimeout(function () {
// f1的任务代码
console.log("this is f1");
resolve("Success");
}, 500);
});
function f2(val){
console.log(val + ":" + "this is f2");
}
function f3(){
console.log("this is f3")
}
f1.then(f2);
f3();
以上代码在Chrome 版本43中的运行结果为:
this is f3
this is f1
Success:this is f2
以上就是针对javascript异步编程的了解学习,之后还有相关文章进行分享,不要错过哦。
D3.js 比例尺与坐标轴的配置详解
2026-11-05 16:54:46
JavaScript怎么遍历对象的Symbol键和不可枚举属性
2026-11-05 13:13:39
封装一个健壮的JS字符串判空工具函数
2026-11-05 12:16:14
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
2026-11-05 11:22:29
JavaScript立即执行函数(IIFE)是什么及基本语法
2026-11-05 10:51:19
