单线程语言JavaScript是怎么实现异步的?

2024-08-0208:45:33编程语言入门到精通Comments522 views字数 8339阅读模式

异步的概念

异步编程的核心在于它允许程序在等待某些操作完成的同时继续执行其他任务。例如,当你发起一个网络请求时,你不需要等待这个请求完成就可以继续执行后续的代码。一旦网络请求完成,JavaScript 会通过某种机制通知你的代码去处理结果。 这种方式可以提高程序的效率,因为它避免了在等待I/O操作(如网络请求、文件读写等)时的闲置。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

JavaScript是怎么实现异步的?

JavaScript 是一种单线程语言,但它通过以下几种机制实现了异步编程:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

  1. 回调函数(Callback):这是最基本的异步模式。当一个异步操作开始时,你可以传递一个回调函数作为参数。当操作完成时,这个回调函数会被调用。
  2. 事件循环(Event Loop):JavaScript 运行时环境维护了一个事件队列。当异步操作完成时,它们会将回调函数放入队列中。事件循环会不断地检查这个队列,并在主线程空闲时执行队列中的回调函数。
  3. Promises:Promise 是一个表示异步操作最终完成或失败的对象。它允许你以链式的方式处理异步操作的结果,避免了回调地狱(Callback Hell)。
  4. async/await:这是基于 Promise 的语法糖,使得异步代码看起来和同步代码类似。async 函数返回一个 Promise,而 await 表达式可以暂停 async 函数的执行,直到等待的 Promise 被解决或拒绝。
  5. Web Workers:允许在后台线程运行脚本,不干扰用户界面的响应。
  6. Timers:如 setTimeoutsetInterval,它们允许你设置延迟或周期性执行的函数。
  7. Streams:允许逐步处理数据流,而不是一次性处理整个数据集。

JavaScript 的异步特性使得它在处理高并发和I/O密集型应用时非常有用,尤其是在Web开发中。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

回调函数

定义

回调函数是一个作为参数传递给其他函数的函数,它将在某个事件发生或某些条件满足时被调用。在JavaScript中,回调函数通常用于处理异步操作的结果,如文件读写、网络请求等。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

回调函数的使用场景

  • 异步操作:当需要执行一个可能需要一些时间的操作时,如读取文件、数据库查询或网络请求。
  • 事件处理:在事件驱动的编程中,回调函数用于响应用户行为,如点击、滚动等。

回调函数的工作原理

  1. 函数作为参数:首先,定义一个函数,这个函数将作为参数传递给另一个函数。
  2. 执行异步操作:主函数开始执行异步操作,并将回调函数作为参数保存起来。
  3. 操作完成:当异步操作完成时,主函数会调用之前保存的回调函数。
  4. 执行回调:回调函数被调用,通常在这个函数中处理异步操作的结果。

回调函数的优缺点

  • 优点
    • 非阻塞:允许程序在等待异步操作完成时继续执行其他任务。
    • 解耦:将异步操作的处理逻辑与主逻辑分离,提高代码的模块化。
  • 缺点
    • 回调地狱:当多个异步操作嵌套时,代码可读性差,难以维护。
    • 错误处理:需要在每个回调中单独处理错误,增加了代码的复杂性。

事件循环

定义

事件循环是一个运行机制,它允许JavaScript引擎在单线程中处理同步任务和异步任务。事件循环不断地检查事件队列,并在主线程空闲时执行队列中的回调函数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

事件循环的组成部分

  1. 调用栈(Call Stack) :存储正在执行的函数调用。
  2. 事件队列(Event Queue) :存储异步任务的回调函数。
  3. 事件循环机制:不断检查调用栈是否为空,如果为空,则从事件队列中取出第一个任务,放入调用栈执行。

事件循环的工作流程

  1. 执行同步代码:当JavaScript代码运行时,同步任务会被推入调用栈执行。
  2. 执行异步任务:异步任务(如setTimeout、Promise、DOM事件等)会注册回调函数到事件队列中,但不会立即执行。
  3. 事件循环检查:一旦调用栈为空,事件循环会检查事件队列。
  4. 执行回调函数:事件循环从事件队列中取出第一个任务,推入调用栈执行。
  5. 重复循环:事件循环不断重复上述过程,直到调用栈和事件队列为空。

宏任务(Macro Tasks)和微任务(Micro Tasks)

  • 宏任务:包括 setTimeout、setInterval、I/O、UI渲染等。
  • 微任务:包括 Promise 的回调、MutationObserver 等。
  • 事件循环在每次迭代中,会先清空所有微任务队列,然后再处理下一个宏任务。

事件循环的优缺点

  • 优点
    • 非阻塞:允许JavaScript在等待异步操作时继续执行其他任务。
    • 简化编程模型:开发者不需要直接管理线程或进程。
  • 缺点
    • 单线程限制:长时间的同步任务可能会阻塞主线程,影响性能。
    • 回调地狱:过度使用回调函数可能导致代码难以理解和维护。

示例

javascript
console.log('Script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
    console.log('promise1');
}).then(function() {
    console.log('promise2');
});

console.log('Script end');

输出顺序将是:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

Script start
Script end
promise1
promise2
setTimeout

这个示例展示了事件循环如何处理同步代码、宏任务(setTimeout)和微任务(Promise)。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

Promises

定义

Promise 是一个代表异步操作最终完成(或失败)的对象。一个 Promise 对象代表了一个可能还不可用的值,或者一个在未来某个时间点才可用的最终结果。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

Promises 的状态

  • Pending(等待状态) :初始状态,既不是成功,也不是失败状态。
  • Fulfilled(已成功) :意味着操作成功完成。
  • Rejected(已失败) :意味着操作失败。

Promises 的工作原理

  1. 创建 Promise:使用 new Promise 构造函数创建一个新的 Promise 对象,并提供一个执行器函数(executor function),这个函数将在 Promise 被创建后立即执行。
  2. 执行器函数:接受两个参数,通常命名为 resolve 和 rejectresolve 用于在异步操作成功时调用,reject 用于在异步操作失败时调用。
  3. 链式调用:Promise 提供了 .then() 和 .catch() 方法来添加处理成功的回调(成功处理)和处理失败的回调(失败处理)。.then() 可以链式调用,每个 .then() 可以返回一个新的 Promise,使得异步操作可以顺序执行。
  4. 错误处理.catch() 方法用于捕获 Promise 链中的错误,并且它必须放在链的最后。

Promises 的使用场景

  • 异步操作:如网络请求、文件读写等。
  • 顺序执行:当需要按顺序执行多个异步操作时。

示例

javascript复制
let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        let result = 'Operation completed';
        resolve(result); // 异步操作成功
    }, 1000);
});

promise.then((result) => {
    console.log(result); // 输出 'Operation completed'
}).catch((error) => {
    console.error(error);
});

Promises 的原理剖析

Promises 的实现依赖于 JavaScript 引擎的事件循环和任务队列。当创建一个新的 Promise 时,其执行器函数会被放入事件循环的宏任务队列中。一旦调用栈清空,执行器函数会被执行,此时可以调用 resolvereject文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

  • 如果调用 resolve,Promise 状态变为 Fulfilled,并且注册在 .then() 中的回调函数会被加入微任务队列。
  • 如果调用 reject,Promise 状态变为 Rejected,并且注册在 .catch() 中的回调函数会被加入微任务队列。

Promises 的链式调用特性允许开发者以一种更加线性和可读的方式编写异步代码,而不是嵌套回调函数。此外,Promises 还支持 .all().race() 等方法,用于处理多个 Promise 的情况。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

async/await

async/await 的定义

  • async:关键字,用于声明一个异步函数。异步函数总是返回一个 Promise。
  • await:关键字,只能在异步函数内部使用。它用于等待一个 Promise 解决(fulfilled)或拒绝(rejected),暂停异步函数的执行直到 Promise 完成。

async/await 的工作原理

  1. 声明异步函数:使用 async 关键字声明一个函数,该函数的返回值自动被封装成一个 Promise。
  2. 使用 await:在异步函数内部,使用 await 关键字等待一个 Promise 的解决或拒绝。await 会暂停函数的执行,直到等待的 Promise 完成。
  3. 处理结果:一旦 Promise 解决,await 表达式的结果就是 Promise 的解决值。如果 Promise 被拒绝,会抛出一个错误,可以通过 try...catch 语句捕获。
  4. 错误处理:使用 try...catch 语句来捕获由 await 表达式抛出的错误。

示例

javascript
async function fetchData() {
    try {
        let response = await fetch('https://api.example.com/data');
        let data = await response.json();
        console.log(data);
    } catch (error) {
        console.error('Error fetching data:', error);
    }
}

async/await 的优点

  • 可读性:代码看起来像同步代码,易于理解和维护。
  • 错误处理:可以使用传统的 try...catch 语句来处理错误。
  • 避免回调地狱:不需要嵌套回调函数,代码结构更清晰。

原理剖析

async/await 背后的原理是 Promise。当一个函数被声明为 async,它的返回值会被自动封装成一个 Promise。当在异步函数内部使用 await 时,会发生以下几件事:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

  1. 暂停执行:异步函数的执行会暂停在 await 表达式处,等待 Promise 的解决或拒绝。
  2. 微任务队列:一旦 Promise 状态改变(解决或拒绝),由于 await 表达式返回的是一个微任务,这个微任务会被加入到事件循环的微任务队列中。
  3. 恢复执行:当事件循环的调用栈为空时,微任务队列中的微任务会被执行,异步函数会恢复执行,并返回 Promise 的解决值或抛出错误。

注意事项

  • 只在异步函数中使用 awaitawait 只能在 async 函数内部使用。
  • 性能考虑:过度使用 await 可能导致性能问题,因为每个 await 都会创建一个微任务,如果大量使用,可能会影响性能。
  • 避免死锁:在某些情况下,如在 Node.js 的某些版本中,过度使用 await 可能会导致事件循环的死锁。

Web Workers

定义

Web Workers 是一种运行在浏览器后台的 JavaScript。它们允许开发者在不干扰用户界面的情况下,执行长时间的脚本或计算密集型任务。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

Web Workers 的特点

  • 多线程:Web Workers 允许在主线程之外的线程上运行脚本。
  • 通信:主线程和 Worker 之间通过消息传递进行通信,使用 postMessage 方法发送消息,并通过事件监听器接收消息。
  • 独立性:Worker 有自己的全局作用域,与主线程是隔离的,不能直接访问 DOM 或者全局变量。

Web Workers 的工作原理

  1. 创建 Worker:使用 new Worker('worker.js') 创建一个新的 Worker 实例,其中 'worker.js' 是 Worker 脚本的路径。
  2. 发送消息:使用 postMessage 方法向 Worker 发送消息,消息可以是任何可以序列化成字符串的 JavaScript 对象。
  3. 接收消息:Worker 通过监听 message 事件接收消息,并处理接收到的数据。
  4. 终止 Worker:使用 terminate 方法可以终止 Worker 的运行。

示例

主线程代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

javascript
var myWorker = new Worker('worker.js');

myWorker.postMessage('Hello, worker!'); // 发送消息给 Worker

myWorker.onmessage = function(e) {
    console.log('Message received from worker:', e.data);
};

Worker 线程代码(worker.js):文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

javascript
self.onmessage = function(e) {
    console.log('Message received from main script:', e.data);
    var result = e.data.split('').reverse().join(''); // 处理数据
    self.postMessage(result); // 发送消息回主线程
};

Web Workers 的优点

  • 性能提升:通过在后台线程中处理计算密集型任务,可以避免阻塞主线程,提高应用性能。
  • 响应性:保持用户界面的流畅响应,改善用户体验。

原理剖析

Web Workers 利用浏览器的多线程能力来实现并行计算。每个 Worker 运行在一个单独的全局上下文中,这意味着它们有自己的 self 对象,而不是 window 对象。Worker 线程不能直接操作 DOM,因为它们不共享同一个全局作用域。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

在实现上,浏览器为每个 Worker 创建一个新的 JavaScript 执行环境,并在后台线程中运行 Worker 脚本。主线程和 Worker 线程之间的通信是通过结构化克隆算法进行的,确保了数据的深拷贝,同时保持了数据类型和结构的一致性。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

注意事项

  • 同源策略:Worker 脚本必须遵守同源策略,即只能加载与主页面相同的源的脚本。
  • 性能考虑:虽然 Worker 可以提高性能,但创建过多的 Worker 可能会导致资源竞争和上下文切换的开销。
  • 调试:调试 Worker 脚本可能比调试主线程中的脚本更复杂,因为它们运行在不同的执行环境中。

Timers

定义

Timers 是 JavaScript 提供的一组函数,用于设置定时任务。主要有两种类型:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

  • 一次性定时器setTimeout,在指定的毫秒数后执行一次函数。
  • 重复性定时器setInterval,每隔指定的毫秒数执行一次函数。

Timers 的工作原理

  1. 设置定时器:调用 setTimeout 或 setInterval 函数,并传入要执行的函数和时间间隔(以毫秒为单位)。
  2. 计时开始:一旦调用了 Timer 函数,计时开始。
  3. 执行函数:当计时结束后,如果是 setTimeout,则执行一次传入的函数;如果是 setInterval,则每隔指定的时间间隔重复执行函数。
  4. 调度机制:Timer 函数将任务添加到浏览器或 Node.js 的任务队列中,由事件循环机制调度执行。

Timers 的特点

  • 非阻塞:Timers 不会阻塞 JavaScript 的主线程,它们在后台运行。
  • 异步执行:Timers 指定的函数将在主线程空闲时异步执行。

示例

javascript
// 一次性定时器
setTimeout(function() {
    console.log('This message is displayed after 2 seconds.');
}, 2000);

// 重复性定时器
setInterval(function() {
    console.log('This message is displayed every 2 seconds.');
}, 2000);

Timers 的优点

  • 简单易用:Timers 提供了一种简单的方式来执行定时任务。
  • 灵活性:可以设置一次性或重复性的任务。

原理剖析

Timers 的实现依赖于 JavaScript 运行时的事件循环和任务调度机制。以下是 Timers 工作的几个关键点:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

  1. 任务队列:当调用 setTimeout 或 setInterval 时,浏览器或 Node.js 将传入的函数封装成任务,添加到任务队列中。
  2. 计时器:系统内部的计时器开始计时,等待指定的时间间隔。
  3. 事件循环:一旦时间间隔到达,如果当前调用栈为空,事件循环会从任务队列中取出任务并执行;如果调用栈不为空,任务将等待直到调用栈清空。
  4. 重复执行:对于 setInterval,执行完毕后,如果间隔时间再次到达,且调用栈为空,任务会再次被取出执行,形成循环。

注意事项

  • 最小延迟:由于事件循环和浏览器的调度机制,setTimeout 和 setInterval 的实际执行时间可能会比设定的时间稍长。
  • 精确度:对于需要高精度定时的任务,Timers 可能不是最佳选择,因为它们的执行可能会受到页面加载、渲染和其他任务的影响。
  • 清除定时器:使用 clearTimeout 和 clearInterval 可以取消定时器,避免不必要的执行。

Streams

定义

Streams API 允许你以流式传输的方式读取和写入数据,这意味着数据可以按块(chunks)逐步处理,而不需要等待整个数据集加载完成。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

Streams 的类型

  • 可读流(Readable Streams) :可以从中读取数据。
  • 可写流(Writable Streams) :可以向其中写入数据。
  • 双工流(Duplex Streams) :既可以读取也可以写入数据。
  • 转换流(Transform Streams) :读取数据,转换后写入新数据。

Streams 的工作原理

  1. 创建 Stream:通过构造函数或特定的方法创建一个 Stream 对象。
  2. 监听事件:为 Stream 对象添加事件监听器,如 readabledataend 和 error 等。
  3. 管道(Piping) :可以使用 pipe 方法将一个 Stream 的输出连接到另一个 Stream 的输入。
  4. 处理数据:当 Stream 产生数据时,触发相应的事件,如 data 事件,开发者可以在事件监听器中处理数据。

示例

javascript
// 创建一个可读流
const readableStream = new ReadableStream({
    start(controller) {
        controller.enqueue(new TextEncoder().encode("Hello"));
        controller.enqueue(new TextEncoder().encode("World"));
        controller.close(); // 表示没有更多数据
    }
});

// 处理数据
const reader = readableStream.getReader();

reader.read().then(({ value, done }) => {
    if (done) return;
    console.log(new TextDecoder().decode(value)); // 输出 "Hello"
    return reader.read();
}).then(({ value, done }) => {
    if (done) return;
    console.log(new TextDecoder().decode(value)); // 输出 "World"
});

Streams 的优点

  • 效率:可以高效地处理大量数据,无需一次性加载到内存。
  • 灵活性:支持多种类型的 Stream,适用于不同的使用场景。
  • 链式处理:通过管道可以轻松地将多个 Stream 连接起来,实现复杂的数据处理流程。

原理剖析

Streams 的实现依赖于 JavaScript 的异步和事件驱动特性:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html

  1. 事件循环:Streams 的事件(如 dataend)是异步触发的,由 JavaScript 的事件循环机制处理。
  2. 背压机制:当数据消费速度跟不上生产速度时,Stream 可以自动或手动实现背压(backpressure),以避免内存溢出。
  3. 队列管理:Stream 控制器(如 ReadableStreamDefaultController)管理着一个内部队列,用于存储待处理的数据块。
  4. 错误处理:Stream 支持错误传播机制,当一个 Stream 遇到错误时,可以自动将错误传递给连接的 Stream。

注意事项

  • 兼容性:Streams API 是相对较新的标准,可能需要考虑浏览器或 Node.js 版本的兼容性。
  • 错误处理:在使用 Streams 时,需要妥善处理可能出现的错误,避免数据丢失或程序崩溃。
  • 资源管理:使用完 Stream 后,应确保正确关闭或取消,释放相关资源。
作者:脏十三
来源:稀土掘金
文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/64707.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/ymba/64707.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定