深度解析JavaScript 异步编程

192 阅读6分钟

一、回调函数(Callback)

原理:事件循环与任务队列

JavaScript 引擎通过事件循环机制实现异步执行。所有同步代码直接压入调用栈执行,而异步任务(如 setTimeout、I/O 操作)会被推入任务队列,等待主线程空闲后执行。

代码示例:基础用法与回调地狱

	// 基础回调:文件读取

	const fs = require('fs');

	fs.readFile('file.txt', 'utf8', (err, data) => {

	  if (err) throw err;

	  console.log('文件内容:', data); // 输出文件内容

	});

	console.log('读取中...'); // 此行先输出

	 

	// 回调地狱示例

	fs.readFile('file1.txt', 'utf8', (err1, data1) => {

	  fs.readFile('file2.txt', 'utf8', (err2, data2) => {

	    fs.readFile('file3.txt', 'utf8', (err3, data3) => {

	      console.log(data1, data2, data3);

	    });

	  });

	});

底层执行流程

  1. 同步阶段

    • 执行 fs.readFile,将异步任务注册到线程池。
    • 输出 "读取中..."。
  2. 异步阶段

    • 文件读取完成后,将回调函数推入任务队列。
    • 事件循环检测到主线程空闲,执行回调输出文件内容。

优缺点分析

  • ✅ 简单直接,无需复杂语法
  • ❌ 多层嵌套导致回调地狱(Callback Hell)
  • ❌ 错误处理需要手动传递(遵循"错误优先"原则)

二、事件监听(Event Listener)

原理:观察者模式与微任务队列

事件监听基于观察者模式,通过 on/emit 方法实现解耦。事件触发时,所有注册的监听器会被推入微任务队列,优先于宏任务队列执行。

代码示例:DOM 事件与自定义事件

	// DOM 事件监听

	document.getElementById('btn').addEventListener('click', () => {

	  console.log('按钮被点击');

	});

	 

	// 自定义事件系统

	class EventEmitter {

	  on(event, listener) {

	    this.listeners = this.listeners || {};

	    (this.listeners[event] || (this.listeners[event] = [])).push(listener);

	  }

	 

	  emit(event, ...args) {

	    Promise.resolve().then(() => {

	      (this.listeners[event] || []).forEach(listener => listener(...args));

	    });

	  }

	}

	 

	const emitter = new EventEmitter();

	emitter.on('data', (msg) => console.log('收到:', msg));

	emitter.emit('data', 'Hello Event'); // 1秒后输出

底层执行流程

  1. 注册监听器:将 data 事件回调存入 listeners 对象。

  2. 触发事件

    • emit 方法将回调函数包装为 Promise,推入微任务队列。
    • 微任务优先级高于宏任务(如 setTimeout),因此回调会在当前事件循环的微任务阶段执行。

优缺点分析

  • ✅ 解耦事件生产者与消费者
  • ✅ 支持多个监听器并行执行
  • ❌ 需手动管理事件生命周期(如取消监听)

三、发布/订阅模式(Pub/Sub)

原理:中介者模式与消息中心

发布/订阅模式通过一个中介(Broker)管理所有订阅关系,实现完全解耦。订阅者通过主题(Topic)订阅消息,发布者无需知道订阅者存在。

代码示例:实现 Pub/Sub 系统

	class PubSub {

	  constructor() {

	    this.topics = new Map();

	  }

	 

	  subscribe(topic, callback) {

	    if (!this.topics.has(topic)) this.topics.set(topic, []);

	    this.topics.get(topic).push(callback);

	    return () => this.unsubscribe(topic, callback);

	  }

	 

	  publish(topic, data) {

	    Promise.resolve().then(() => {

	      (this.topics.get(topic) || []).forEach(callback => callback(data));

	    });

	  }

	 

	  unsubscribe(topic, callback) {

	    const callbacks = this.topics.get(topic);

	    if (callbacks) {

	      this.topics.set(topic, callbacks.filter(cb => cb !== callback));

	    }

	  }

	}

	 

	// 使用示例

	const pubsub = new PubSub();

	const unsubscribe = pubsub.subscribe('news', (msg) => {

	  console.log('新闻:', msg); // 输出: 突发新闻!

	});

	pubsub.publish('news', '突发新闻!'); // 1秒后发布

	unsubscribe(); // 取消订阅

底层执行流程

  1. 订阅主题:将回调函数存入 topics Map 结构。

  2. 发布消息

    • 通过 Promise.resolve().then() 将回调推入微任务队列。
    • 微任务执行时,遍历所有订阅者并触发回调。

优缺点分析

  • ✅ 完全解耦发布者与订阅者
  • ✅ 支持动态订阅/取消订阅
  • ❌ 需管理主题生命周期,避免内存泄漏

四、Promise:链式调用与状态管理

原理:微任务与状态机

Promise 对象是一个状态机,拥有三种状态:pending(初始)、fulfilled(成功)、rejected(失败)。状态变更后不可逆,且会触发关联的 .then() 或 .catch() 方法。

代码示例:链式调用与错误处理

	// 创建 Promise 对象

	function fetchData(url) {

	  return new Promise((resolve, reject) => {

	    setTimeout(() => {

	      if (url === 'valid') resolve('数据加载成功');

	      else reject(new Error('无效 URL'));

	    }, 1000);

	  });

	}

	 

	// 链式调用

	fetchData('valid')

	  .then(data => {

	    console.log(data); // 输出: 数据加载成功

	    return fetchData('invalid');

	  })

	  .then(data => console.log(data))

	  .catch(err => console.error('错误:', err.message)); // 输出: 无效 URL

	 

	// 并行请求

	Promise.all([

	  fetch('https://api.example.com/users'),

	  fetch('https://api.example.com/posts')

	]).then(([users, posts]) => {

	  console.log('用户:', users, '帖子:', posts);

	});

底层执行流程

  1. 创建 Promise

    • 执行器函数(executor)立即执行,注册异步操作。
    • 异步操作完成后调用 resolve 或 reject 变更状态。
  2. 链式调用

    • .then() 和 .catch() 返回新的 Promise,实现链式调用。
    • 回调函数被推入微任务队列,保证异步顺序。

优缺点分析

  • ✅ 避免回调地狱,代码更扁平
  • ✅ 统一错误处理(.catch() 冒泡)
  • ❌ 需处理未捕获的 Promise 错误(可通过 unhandledrejection 事件监听)

五、生成器(Generators)与 yield:协程控制流

原理:协程与暂停执行

生成器通过 function* 定义,使用 yield 关键字暂停执行,将控制权交还给调用者。配合协程库(如 co)可实现同步风格的异步控制流。

代码示例:手动控制生成器

	// 定义生成器

	function* asyncTask() {

	  console.log('开始');

	  const data = yield fetchData('valid'); // 暂停执行

	  console.log('数据:', data); // 输出: 数据加载成功

	}

	 

	// 手动执行生成器

	const gen = asyncTask();

	const next = gen.next(); // 启动生成器,执行到第一个 yield

	 

	next.value

	  .then(data => gen.next(data)) // 恢复执行,传递结果

	  .catch(err => gen.throw(err)); // 抛出错误到生成器

	 

	// 使用 co 库自动执行

	const co = require('co');

	co(function* () {

	  const [user, posts] = yield Promise.all([

	    fetch('/user'),

	    fetch('/posts')

	  ]);

	  console.log('用户:', user, '帖子:', posts);

	});

底层执行流程

  1. 创建生成器

    • 执行 gen.next() 启动生成器,执行到第一个 yield 暂停。
    • 返回 {value: Promise, done: false}
  2. 恢复执行

    • 解析 yield 后的 Promise,通过 gen.next(data) 传递结果并恢复执行。

优缺点分析

  • ✅ 精确控制异步流程(如状态机)
  • ❌ 语法复杂,需额外工具(如 co
  • ❌ 已被 async/await 取代,现代项目不推荐使用

六、async/await:Promise 的语法糖

原理:编译时转换与微任务

async/await 是基于 Promise 的语法糖,通过编译器(如 Babel)转换为 Promise 链式调用。await 关键字会暂停函数执行,直到 Promise 解析。

代码示例:并行请求与错误处理

	// 定义异步函数

	async function fetchData() {

	  try {

	    const [user, posts] = await Promise.all([

	      fetch('/user'),

	      fetch('/posts')

	    ]);

	    console.log('用户:', user);

	    console.log('帖子:', posts);

	  } catch (err) {

	    console.error('请求失败:', err);

	  }

	}

	 

	// 调用异步函数

	fetchData();

	 

	// 顶层 await(ES2022)

	(async function () {

	  const data = await fetch('/api/data');

	  console.log('顶层 await:', data);

	})();

底层执行流程

  1. 编译转换

    	// Babel 转换后的代码(简化)
    
    	function fetchData() {
    
    	  return Promise.resolve().then(() => {
    
    	    return Promise.all([...]).then(([user, posts]) => {
    
    	      console.log(...);
    
    	    });
    
    	  });
    
    	}
    
  2. 执行阶段

    • await 将后续代码包装为 Promise,推入微任务队列。
    • 错误通过 try/catch 捕获,或冒泡到 .catch()

优缺点分析

  • ✅ 同步风格代码,提高可读性
  • ✅ 自然错误处理(try/catch
  • ❌ 需注意 await 位置(避免阻塞主线程)

综合对比与选择建议

方法适用场景核心优势推荐指数
回调函数简单任务、传统 API无需额外依赖★☆☆☆☆
事件监听用户交互、实时通信天然解耦★★★☆☆
发布订阅复杂系统、跨组件通信高度灵活,支持多对多★★★★☆
Promise网络请求、链式异步解决回调地狱,支持错误冒泡★★★★☆
生成器精细控制流程(历史方案)手动暂停/恢复执行★★☆☆☆
async/await现代异步代码(推荐)同步写法,易于调试★★★★★

选择策略

  1. 简单任务:优先使用 async/await 或 Promise。
  2. 事件驱动:使用事件监听(如 DOM 事件)。
  3. 跨模块通信:发布订阅模式。
  4. 遗留代码:回调函数或生成器(需谨慎)。

未来趋势

  • 结构化并发:TC39 提案中的 Promise.anyPromise.allSettled
  • 顶层 await:ES2022 支持模块顶层使用 await
  • 并发控制Promise.all 的变种如 Promise.allLimit(控制并行数)。

结语

在实际项目中,建议优先采用 async/await + Promise 的组合,结合发布订阅模式处理复杂通信,以实现高效、可维护的异步代码。