JavaScript 异步编程:从单线程模型到 Promise 详解

作者:拾光时间:2026-10-25 13:35:30浏览:372

本文深入解析 JavaScript 的单线程模型与异步任务机制,详细阐述事件循环(Event Loop)的工作原理。重点讲解 Promise 对象的三种状态(Pending、Fulfilled、Rejected)及构造函数用法,对比回调函数、事件监听与发布/订阅模式。同时分析 then 方法的链式调用差异,并区分微任务与宏任务的执行时序,帮助开发者掌握现代 JS 异步编程的核心概念。

1、单线程模型

单线程模型意味着 JavaScript 仅在单一线程中执行代码。这意味着同一时刻只能处理一个任务,其余任务需排队等待。需注意的是,JavaScript 引擎内部包含多个线程,但单个脚本代码仅在主线程上运行,其他线程仅在后台进行辅助配合。

JavaScript 采用单线程而非多线程,主要源于历史原因。自诞生起,JavaScript 即被设计为单线程,旨在避免浏览器逻辑过于复杂。若采用多线程,需处理资源共享及结果冲突问题,这对于网页脚本语言而言过于繁琐。

若 JavaScript 存在两个线程,一个负责向 DOM 节点添加内容,另一个负责删除该节点,浏览器将难以确定以哪个线程的操作为准,甚至需要引入锁机制。因此,为简化复杂性,JavaScript 自始坚持单线程模型,这一核心特征未来亦不会改变。

2、同步任务和异步任务

程序中的任务可划分为两类:同步任务(synchronous)与异步任务(asynchronous)。

同步任务指那些未被引擎挂起、需在主线程上按序执行的任务。只有前一个任务执行完毕后,后一个任务才能开始执行。

异步任务指那些被引擎暂时搁置、不进入主线程而进入任务队列的任务。只有当引擎判定异步任务可执行时(例如 Ajax 请求已从服务器获取结果),该任务(以回调函数形式)才会进入主线程执行。排在异步任务之后的代码无需等待异步任务结束即可立即运行,即异步任务不具备“堵塞”效应。

以 Ajax 操作为例,开发者可将其视为同步或异步任务。若为同步任务,主线程将等待 Ajax 返回结果后再继续执行;若为异步任务,主线程在发出请求后直接向下执行,待 Ajax 结果返回后,再执行对应的回调函数。

3、任务队列和事件循环

JavaScript 运行时,除主线程外,引擎还维护一个任务队列(task queue),用于存放需处理的异步任务。(实际上,根据异步类型可能存在多个队列,此处为便于理解假设仅有一个)。主线程首先执行所有同步任务,待同步任务全部完成后,再检查任务队列中的异步任务。

若异步任务满足执行条件,它将重新进入主线程开始执行,此时它表现为同步任务。执行完毕后,下一个异步任务再进入主线程。一旦任务队列清空,程序即结束执行。异步任务通常以回调函数形式书写,一旦重新进入主线程,便执行对应的回调函数。

若异步任务未指定回调函数,则不会进入任务队列,也就无法重新进入主线程,因为缺少指定后续操作的机制。JavaScript 引擎通过不断检查来确认异步任务是否有结果及是否可进入主线程。只要同步任务执行完毕,引擎便会检查挂起的异步任务是否满足进入主线程的条件。这种循环检查机制被称为事件循环(Event Loop)。

维基百科将其定义为:“事件循环是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。

4、异步操作的模式

4.1回调函数

把f2写成f1的回调函数。

function f1(callback) {
  // ...
  callback();
}
 
function f2() {
  // ...
}
 
f1(f2);

回调函数的优势在于简单、易于理解和实现,但劣势是不利于代码阅读与维护,各部分间高度耦合(coupling),导致程序结构混乱、流程难以追踪(尤其在多个回调函数嵌套时),且每个任务仅能指定一个回调函数。

4.2事件监听

f1.on('done', f2);
 
function f1() {
  setTimeout(function () {
    // ...
    f1.trigger('done');
  }, 1000);
}

f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2

该方法的优点在于易于理解,可绑定多个事件,每个事件可指定多个回调函数,且能“去耦合”(decoupling),利于实现模块化。缺点是整个程序需变为事件驱动型,运行流程变得不清晰,阅读代码时难以看清主流程。

4.3 发布/订阅

事件可理解为“信号”,若存在“信号中心”,某任务执行完成即向信号中心“发布”(publish)信号,其他任务可向信号中心“订阅”(subscribe)该信号,从而知晓何时开始执行。这称为“发布/订阅模式”(publish-subscribe pattern),亦称“观察者模式”(observer pattern)。

f2向信号中心jQuery订阅done信号。

jQuery.subscribe('done', f2);
 
function f1() {
  setTimeout(function () {
    // ...
    jQuery.publish('done');
  }, 1000);
}

上述代码中,jQuery.publish('done')的意思是,f1执行完成后,向信号中心jQuery发布done信号,从而引发f2的执行。

f2完成执行后,可以取消订阅(unsubscribe)。

jQuery.unsubscribe('done', f2);

该方法性质与“事件监听”类似,但明显优于后者。因为可通过查看“消息中心”,了解存在多少信号及每个信号的订阅者数量,从而监控程序运行状态。

5、Promise 对象的状态

Promise 对象通过自身状态来控制异步操作。Promise 实例具有三种状态。

  • 异步操作未完成(pending)
  • 异步操作成功(fulfilled)
  • 异步操作失败(rejected)

上述三种状态中,fulfilled和rejected合在一起称为resolved(已定型)。

这三种状态的变化途径仅有两种。

  • 从“未完成”到“成功”
  • 从“未完成”到“失败”

一旦状态发生变化,即凝固不再改变,这也是 Promise 名称的由来,其英语意为“承诺”,一旦承诺生效,便不得再改变。这意味着 Promise 实例的状态变化仅可能发生一次。

因此,Promise 的最终结果仅有两种。

  • 异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled。
  • 异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected。

6、Promise 构造函数

JavaScript 提供原生的Promise构造函数,用来生成 Promise 实例。

var promise = new Promise(function (resolve, reject) {
  // ...
 
  if (/* 异步操作成功 */){
    resolve(value);
  } else { /* 异步操作失败 */
    reject(new Error());
  }
});

上述代码中,Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个由 JavaScript 引擎提供的函数,无需自行实现。

resolve函数的作用是,将Promise实例的状态从“未完成”变为“成功”(即从pending变为fulfilled),在异步操作成功时调用,并将异步操作的结果作为参数传递出去。reject函数的作用是,将Promise实例的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误作为参数传递出去。

下面是一个例子。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}
 
timeout(100)

上述代码中,timeout(100)返回一个 Promise 实例。100毫秒以后,该实例的状态会变为fulfilled。

7、then() 用法辨析

Promise 的用法,简单说就是一句话:使用then方法添加回调函数。但是,不同的写法有一些细微的差别,请看下面四种写法,它们的差别在哪里?

// 写法一
f1().then(function () {
  return f2();
});
 
// 写法二
f1().then(function () {
  f2();
});
 
// 写法三
f1().then(f2());
 
// 写法四
f1().then(f2);

为便于讲解,下面这四种写法都再用then方法接一个回调函数f3。写法一的f3回调函数的参数,是f2函数的运行结果。

f1().then(function () {
  return f2();
}).then(f3);

写法二的f3回调函数的参数是undefined。

f1().then(function () {
  f2();
  return;
}).then(f3);

写法三的f3回调函数的参数,是f2函数返回的函数的运行结果。

f1().then(f2())
  .then(f3);

写法四与写法一只有一个差别,那就是f2会接收到f1()返回的结果。

f1().then(f2)
  .then(f3);

8、Promise 优缺点

优点:让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数;再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。

而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。

缺点:编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then,必须自己在then的回调函数里面理清逻辑。

9、微任务

Promise 的回调函数属于异步任务,会在同步任务之后执行。

new Promise(function (resolve, reject) {
  resolve(1);
}).then(console.log);
 
console.log(2);
// 2
// 1

上述代码会先输出2,再输出1。因为console.log(2)是同步任务,而then的回调函数属于异步任务,一定晚于同步任务执行。

但是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。

setTimeout(function() {
  console.log(1);
}, 0);
 
new Promise(function (resolve, reject) {
  resolve(2);
}).then(console.log);
 
console.log(3);
// 3
// 2
// 1

上述代码的输出结果是321。这说明then的回调函数的执行时间,早于setTimeout(fn, 0)。因为then是本轮事件循环执行,setTimeout(fn, 0)在下一轮事件循环开始时执行。

到此这篇关于Promise异步编程模式的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持本站。

作者最新文章