首页 > 基础资料 博客日记

超硬核五千字!彻底讲明白JavaScript中的异步和同步,以及JavaScript代码执行顺序

2024-10-21 05:00:09基础资料围观78

文章超硬核五千字!彻底讲明白JavaScript中的异步和同步,以及JavaScript代码执行顺序分享给大家,欢迎收藏Java资料网,专注分享技术知识

同步操作和异步操作是编程中处理任务的两种不同方式,它们主要区别在于控制流和对程序执行的影响。不知道大家是怎么理解JavaScript中的同步和异步的?JavaScript的代码执行顺序是怎么样?下面这段代码是同步还是异步的?

console.log('Start');
const response = await fetch('https://juejin.cn');
const data = await response.json();
console.log('Data received:', data);
console.log('End');

同步操作 (Synchronous)

定义: 同步操作会阻塞当前线程或进程,直到该操作完成。这意味着当一段代码执行同步操作时,后面的代码必须等待该操作完成才能继续执行。在同步操作中,任务按顺序执行,一个任务必须完成后才能执行下一个任务。这种方式会阻塞程序的执行,直到当前任务完成。

特点:

  • 简单直观,代码按照从上到下的顺序执行。
  • 在执行耗时操作时会阻塞其他任务,可能导致UI无响应(在前端JavaScript中)。

JavaScript示例:

console.log('Start');
function synchronousTask() {
    for (let i = 0; i < 1000000000; i++) { /* 耗时任务 */}
    console.log('Task done');
}
synchronousTask();
console.log('End');

在这个例子中,循环是一个简单的模拟耗时操作。当这段代码执行时,console.log('Start'); 先执行并打印 StartsynchronousTask(); 开始执行,直到循环结束,才会打印出 “End”。在此期间,无法处理其他任务或用户交互。

异步操作 (Asynchronous)

定义: 异步操作允许程序在等待某个操作完成的同时继续执行其他任务,不阻塞当前线程。异步操作通常用于处理I/O操作、网络请求、定时器等潜在的长时间操作。

特点:

  • 程序可以继续执行其他任务,提高应用的响应性和性能,特别是在处理I/O密集型任务时。
  • 代码逻辑更复杂,需要回调函数、Promises或async/await来处理异步流程。

我们从代码中理解异步,这里使用一个定时器,JavaScript示例:

console.log('Start');

function asynchronousTask() {
    setTimeout(() => {
        console.log('Task done');
    }, 1000);
}

asynchronousTask();

console.log('End');

在这段代码中,执行顺序如下所示:

  1. console.log('Start'); 先执行并打印 Start
  2. asynchronousTask(); 开始执行,setTimeout 设置了一个异步任务,这个任务在 1 秒后执行,但不会阻塞程序
  3. console.log('End'); 立即执行并打印 End
  4. 1 秒后,异步任务完成,console.log('Task done'); 执行并打印 Task done

但是一般在写代码的时候,我们的异步操作肯定不是通过定时器完成的,下面给大家展示两种异步操作写法,分别如下所示:

1、使用 Promise 处理异步操作

console.log('Start');

function asynchronousTask() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Task done');
        }, 1000);
    });
}

asynchronousTask().then(message => {
    console.log(message);
});

console.log('End');

在这个例子中:

  1. asynchronousTask 返回一个 Promise,它在 1 秒后解决(resolve)
  2. console.log('Start');console.log('End'); 立即执行
  3. 1 秒后,Promise 被解决,console.log(message); 打印 Task done

2、使用 async/await 处理异步操作

console.log('Start');

async function runAsyncTask() {
    const message = await asynchronousTask();
    console.log(message);
}

runAsyncTask();

console.log('End');

在这个例子中:

  1. async 函数 runAsyncTask 内部使用 await 等待 asynchronousTask 完成,但不会阻塞外部代码
  2. console.log('Start');console.log('End'); 立即执行
  3. 1 秒后,asynchronousTask 完成,console.log(message); 打印 Task done

现在是不是对同步和异步的概念有了基本的认识,咱们再回到文章最开始的代码,结论是那段代码不是同步的,而是异步的。在上面代码中,遇到 await 等待结果阻塞代码执行了,为什么不说它是同步?

为了更好地理解,可以将这段代码放在一个异步函数中执行:

async function fetchData() {
    console.log('Start');

    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log('Data received:', data);

    console.log('End');
}

fetchData();

我们先理解一下这个关键词:await关键字会暂停当前的异步函数,等待异步操作完成,但不会阻塞外部代码,不会阻塞事件循环

在这个例子中,fetchData是一个异步函数,尽管await关键字会暂停它的执行,但它不会阻塞整个程序的执行。因此,这段代码是异步的,而不是同步的。假设fetchData函数后面有一个输出,它可能先执行的。

await 仅会暂停当前异步函数的执行,让出控制权给事件循环,以便处理其他任务。

当一个异步函数遇到 await 时,它会暂停该函数的执行,等待 Promise 被解决(resolved)或者被拒绝(rejected)。然而,这种暂停并不等同于传统的同步阻塞,因为其他任务依然可以执行。

咱们接下来通过一段代码来展示什么是阻塞整个程序的执行。在JavaScript中,阻塞代码通常是指那些在执行时会阻塞事件循环,导致其他任务无法执行的代码。常见的例子包括使用同步的I/O操作或长时间运行的计算。

以下是一个使用同步I/O操作的例子,这种操作会阻塞整个程序的执行:

const fs = require('fs');

function readFileSync() {
    // 打印"Start"到控制台
    console.log('Start');

    // 同步读取文件的内容
    // 在文件读取完成之前,这行代码会阻塞事件循环,其他任务无法执行
    const data = fs.readFileSync('/opt/xiaodou.txt', 'utf8');
    // 打印读取到的文件内容到控制台
    console.log('File data:', data);
    // 打印"End"到控制台
    console.log('End');
}

readFileSync();

在这个例子中,fs.readFileSync是一个同步的文件读取操作,它会阻塞事件循环,直到文件读取完成。在读取文件的过程中,其他任务无法执行,整个程序的执行被阻塞。

这段代码的执行顺序是严格线性的,整个程序会在读取文件时被阻塞,直到读取操作完成。

为了对比,可以看一下使用异步I/O操作的代码,这样的代码不会阻塞事件循环:

const fs = require('fs');

function readFileAsync() {
    console.log('Start');

    // 读取文件的异步操作,这不会阻塞事件循环
    fs.readFile('/opt/xiaodou.txt', 'utf8', (err, data) => {
        if (err) {
            console.error('Error reading file:', err);
            return;
        }
        console.log('File data:', data);
    });

    console.log('End');
}

readFileAsync();

在这个例子中,fs.readFile是一个异步的文件读取操作,它不会阻塞事件循环,程序可以继续执行其他任务。

现在对JavaScript中的同步是不是有一些理解:导致整个程序的执行被阻塞是同步,只是暂停当前函数执行是异步,使用await关键字时,它会暂停当前异步函数的执行,等待异步操作完成,但不会阻塞事件循环,其他任务可以继续执行。

我们来看一下阻塞整个程序的代码,不仅会阻塞当前函数,还会阻塞整个事件循环,影响所有其他任务的执行,咱们现在在上面的同步函数中增加一个定时器,那么你猜猜定时器还能定时执行吗?

修改后的代码如下所示:

const fs = require('fs');

function readFileSync() {
    console.log('Start');

    // 设置一个定时器,计划在1秒后执行
    setTimeout(() => {
        console.log('Timer executed');
    }, 1000);

    // 同步读取文件的操作,这会阻塞事件循环
    const data = fs.readFileSync('/opt/xiaodou.txt', 'utf8');
    console.log('File data:', data);

    console.log('End');
}

readFileSync();

在这个例子中,fs.readFileSync是一个同步操作,它会阻塞事件循环,假设文件在1秒后无法读取完成,那么定时器无法在预定的1秒后执行。只有当文件读取操作完成后,定时器才会有机会执行。

事件循环是啥?

在上面我们一直提到事件循环,事件循环(Event Loop)是JavaScript运行时的一个重要机制,它允许非阻塞的异步编程,即使JavaScript是单线程的。事件循环负责管理和调度异步操作的执行,使得异步任务能够在任务完成后被处理,而不会阻塞主线程。

事件循环的工作原理

调用栈(Call Stack)

  • JavaScript引擎有一个调用栈,这是一个LIFO(后进先出)数据结构,用于跟踪正在执行的函数。
  • 当一个函数被调用时,它会被推入调用栈,当函数执行完成后,它会从调用栈中弹出。

任务队列(Task Queue)

  • 任务队列是一个FIFO(先进先出)数据结构,用于存储等待执行的回调函数。
  • 异步操作(如定时器、网络请求、事件处理等)完成后,其回调函数会被放入任务队列中。

事件循环(Event Loop)

  • 事件循环不断地检查调用栈是否为空。
  • 如果调用栈为空,并且任务队列中有待处理的任务,事件循环会将任务队列中的第一个任务移到调用栈中执行。

是不是感觉很难理解呢?接下来咱们结合代码理解一这一块的内容。

1、简单的示例

还是使用定时器的代码示例,说明一下事件循环的工作原理:

console.log('Start');

setTimeout(() => {
    console.log('Timer executed');
}, 1000);

console.log('End');

执行过程

调用栈 任务队列 事件循环 console.log('Start') 输出 "Start" setTimeout 设置定时器(1秒后回调) console.log('End') 输出 "End" 1秒后 超时回调函数 将回调函数推入调用栈 执行回调函数 输出 "Timer executed" 调用栈 任务队列 事件循环

输出结果

Start
End
Timer executed

2、复杂的代码

让我们看一个更复杂的例子,包含多个异步操作:

console.log('Start');

setTimeout(() => {
    console.log('Timeout 1');
}, 500);

setTimeout(() => {
    console.log('Timeout 2');
}, 100);

Promise.resolve().then(() => {
    console.log('Promise 1');
});

Promise.resolve().then(() => {
    console.log('Promise 2');
});

console.log('End');

执行过程

调用栈 任务队列 微任务队列 事件循环 console.log('Start') 输出 "Start" setTimeout (500ms) 超时回调 1 (500ms) setTimeout (100ms) 超时回调 2 (100ms) Promise.resolve().then Promise 回调 1 Promise.resolve().then Promise 回调 2 console.log('End') 输出 "End" 检查微任务队列 执行 Promise 回调 1 输出 "Promise 1" 执行 Promise 回调 2 输出 "Promise 2" 100ms 后 执行超时回调 2 输出 "Timeout 2" 500ms 后 执行超时回调 1 输出 "Timeout 1" 调用栈 任务队列 微任务队列 事件循环

输出结果

Start
End
Promise 1
Promise 2
Timeout 2
Timeout 1

同学们,是不是发现个有趣的东西,这里多了一个微任务队列,那么微任务队列是什么?和任务队列是什么关系?

微任务和宏任务又是啥?

在JavaScript中,微任务(microtasks)和宏任务(macrotasks)是事件循环中处理异步任务的两种类型。它们代表了不同优先级的任务队列,事件循环会按照特定的顺序执行这些任务。

宏任务(Macro Task)

这个实际上就是咱们之前提到的任务队列中的任务。宏任务是那些在每个执行栈执行完毕后,从事件队列中取出并执行的任务。宏任务的执行频率低于微任务,因为它们需要等待整个执行栈清空后再执行。它们通常包括以下几类操作:

  • setTimeout
  • setInterval
  • setImmediate()(Node.js特有)
  • requestAnimationFrame()(浏览器特有)
  • I/O 操作
  • UI渲染(浏览器)
  • 其他一些异步任务

宏任务会被推入到任务队列中,事件循环会从任务队列中依次取出宏任务并执行。简单记忆就是:需要较长时间才来完成的任务,毕竟宏伟的工程往往需要很长的时间

微任务(Microtasks)

微任务是一些需要在当前任务执行完毕后立即执行的任务。它们通常是由Promises、process.nextTick(Node.js特有)或者MutationObserver等机制产生的。微任务队列优先级高于宏任务队列,这意味着在每个宏任务执行完毕后,事件循环会优先处理微任务队列中的所有任务,然后再处理下一个宏任务。它们通常包括以下几类操作:

  • Promises的.then().catch().finally()回调
  • process.nextTick()(Node.js)
  • queueMicrotask()(浏览器和Node.js)
  • MutationObserver(用于监听DOM变化)

微任务会被推入到微任务队列中,事件循环在每次执行完一个宏任务后,会检查并执行所有微任务,直到微任务队列为空。简单速记:微任务是指哪些需要尽快被完成的任务,小事要尽快做,拖久了就变大事了

干说有点费劲,给同学们来张流程图描述事件循环的基本原理:

有任务
无任务
有微任务
无微任务
有宏任务
无宏任务
开始事件循环
同步任务队列
执行同步任务
检查微任务队列
任务执行完毕
执行微任务
检查宏任务队列
执行宏任务
等待新任务

这是一个简化的时序图,事件循环的主要步骤如下:

  1. 开始事件循环:JavaScript引擎开始执行代码。

  2. 同步任务队列:引擎首先检查同步任务队列(实际上是调用栈中的任务),执行队列中的任务。

  3. 执行同步任务:引擎开始执行同步任务,直到队列为空。

  4. 检查微任务队列:同步任务执行完毕后,引擎检查微任务队列。

  5. 执行微任务:如果有微任务(如Promise的.then()回调),引擎会执行这些微任务。

  6. 检查宏任务队列:微任务执行完毕后,引擎检查宏任务队列。

  7. 执行宏任务:如果有宏任务(如setTimeout回调),引擎会执行这些宏任务。执行完毕后再次检查微任务队列。

  8. 等待新任务:如果宏任务执行完毕,引擎会等待新的任务(如新的异步操作或宏任务)。

  9. 循环:引擎会不断地循环执行上述步骤。

同学们这时候会不会有很多疑问,如果觉得上面的文字还不好理解,咱们接下来跟着代码走一遍。

代码示例讲解

咱们还是在上面代码的基础上做修改,这里增加一个包含await的方法,代码如下所示:

console.log('Start'); // 同步任务,立即执行

setTimeout(() => {
    console.log('Timeout 1'); // 宏任务,500ms后执行
}, 500);

setTimeout(() => {
    console.log('Timeout 2'); // 宏任务,100ms后执行
}, 100);

async function asyncFunction() {
    console.log('Async Function Start'); // 同步任务,立即执行
    await Promise.resolve().then(() => { 
        console.log('Inside Async Function - Promise Resolved'); // 微任务,放入微任务队列 
    });
    console.log('Async Function End'); // 微任务,放入微任务队列
}

asyncFunction(); // 调用异步函数

Promise.resolve().then(() => {
    console.log('Promise 1'); // 微任务,放入微任务队列
});

Promise.resolve().then(() => {
    console.log('Promise 2'); // 微任务,放入微任务队列
});

console.log('End'); // 同步任务,立即执行

时序图

调用栈 微任务队列 宏任务队列 console.log('Start') setTimeout(..., 500) setTimeout(..., 100) asyncFunction() console.log('Async Function Start') await Promise.resolve().then(...) console.log('Inside Async Function - Promise Resolved') console.log('Async Function End') Promise.resolve().then(...) console.log('Promise 1') Promise.resolve().then(...) console.log('Promise 2') console.log('End') 调用栈为空,检查微任务队列 console.log('Inside Async Function - Promise Resolved') console.log('Async Function End') console.log('Promise 1') console.log('Promise 2') 微任务队列为空,检查宏任务队列 100ms后 console.log('Timeout 2') 500ms后 console.log('Timeout 1') 调用栈 微任务队列 宏任务队列

详细解释一下代码中的任务都有哪些:

同步任务立即执行:

  • console.log('Start'); 立即执行,输出 “Start”。
  • setTimeout(..., 500):注册一个500ms后的宏任务。
  • setTimeout(..., 100):注册一个100ms后的宏任务。
  • 调用 asyncFunction()
    • console.log('Async Function Start'); 立即执行,输出 “Async Function Start”。
    • await Promise.resolve().then(...):注册一个微任务。
  • Promise.resolve().then(...):注册两个微任务。
  • console.log('End'); 立即执行,输出 “End”。

微任务执行:

  • console.log('Inside Async Function - Promise Resolved');(在 asyncFunction 中的微任务)
  • console.log('Async Function End');(在 asyncFunction 中的微任务)
  • console.log('Promise 1');(在主代码块中的微任务)
  • console.log('Promise 2');(在主代码块中的微任务)

宏任务执行:

  • Timeout 2:100ms后输出 “Timeout 2”。
  • Timeout 1:500ms后输出 “Timeout 1”。

代码运行输出结果如下所示:

Start
Async Function Start
End
Inside Async Function - Promise Resolved
Async Function End
Promise 1
Promise 2
Timeout 2
Timeout 1

脑洞大开,搞事情

咱们能写一个阻塞事件循环的函数吗?

现在我们已经了解了基本原理,那么我们肯定可以搞点事情,我们可以使用循环阻止其他任务的执行,如下所示,这个函数会在指定的时间内阻塞事件循环,使得其他任务(包括异步操作和定时器)无法执行。

function blockEventLoop(duration) {
    const startTime = Date.now();
    while (Date.now() - startTime < duration) {
        // 这个循环会一直运行,直到达到指定的持续时间
    }
    console.log(`Blocked for ${duration} ms`);
}

// 示例用法
console.log('Start');

setTimeout(() => {
    console.log('Timeout executed');
}, 1000);

blockEventLoop(3000);  // 阻塞事件循环3秒

console.log('End');

我们来分析一下执行过程

  1. 开始执行console.log('Start'); 被推入调用栈,执行并输出 “Start”,然后从调用栈中弹出。

  2. 设置定时器setTimeout 被推入调用栈,设置一个1秒后的定时器。定时器的回调函数被注册,并计划在1秒后执行。最后setTimeout 从调用栈中弹出。

  3. 阻塞事件循环blockEventLoop(3000) 被推入调用栈,开始执行。进入while循环,持续3秒,期间事件循环被阻塞,无法处理其他任务。3秒后,while循环结束,输出 “Blocked for 3000 ms”。最后blockEventLoop 从调用栈中弹出。

  4. 继续执行console.log('End'); 被推入调用栈,执行并输出 “End”,然后从调用栈中弹出。

  5. 事件循环处理定时器:由于事件循环被阻塞了3秒,1秒后的定时器未能在预定时间执行。事件循环恢复后,定时器的回调函数被移到调用栈中执行,输出 “Timeout executed”。

代码输出结果如下所示,不知道你对了吗?

Start
Blocked for 3000 ms
End
Timeout executed

注意事项

阻塞事件循环会导致所有其他任务无法执行,不能瞎搞,尤其是在处理用户界面或需要高响应性的应用程序中。不然会被用户骂的。

拓展:JavaScript和Java在同步和异步编程上的差异

因为我之前是做Java开发,所以在这里增加一块两者的对比,有兴趣的可以看一看,JavaScript和Java在处理同步和异步操作方面确实有一些差异,主要体现在它们的运行时环境和编程模型上。

JavaScript中的同步和异步

JavaScript是单线程的,这意味着它一次只能执行一个任务。为了处理异步操作,JavaScript依赖于事件循环(Event Loop)机制。事件循环允许JavaScript在等待异步操作(如网络请求、定时器等)完成时继续执行其他任务,从而实现非阻塞的异步编程。

  • 同步操作:在调用栈中按顺序执行,每个操作必须等待前一个操作完成。
  • 异步操作:通过回调函数、Promise、async/await等机制实现,异步操作完成后会将回调函数放入任务队列,由事件循环调度执行。

Java中的同步和异步

Java是多线程的,支持并发编程。Java中的异步操作通常通过多线程实现,每个线程可以独立执行任务,从而实现并发和并行处理。Java也有丰富的并发工具,如ExecutorServiceFutureCompletableFuture等。

  • 同步操作:在一个线程中按顺序执行,每个操作必须等待前一个操作完成。
  • 异步操作:通过多线程、线程池、回调机制等实现,异步操作可以在不同的线程中并发执行。

Java中有没有事件循环?

严格来说,Java没有像JavaScript那样的内置事件循环机制。Java的异步操作主要依赖于多线程和并发工具,而不是事件循环。然而,在某些特定场景和框架中,Java也可以实现类似事件循环的机制。例如:

  • Swing和JavaFX:Java的GUI框架(如Swing和JavaFX)使用事件调度线程(Event Dispatch Thread,EDT)处理用户界面事件。这类似于事件循环,但仅用于处理GUI事件。
  • Netty:Netty是一个异步事件驱动的网络应用框架,它实现了自己的事件循环机制,用于处理网络I/O操作。

Java中的异步编程

让咱们看一个Java中的异步编程示例,使用CompletableFuture来实现异步操作:

import java.util.concurrent.CompletableFuture;

public class AsyncExample {
    public static void main(String[] args) {
        System.out.println("Start");

        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(1000); // 模拟异步操作
                System.out.println("Async Task Completed");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        future.thenRun(() -> System.out.println("Callback Executed"));

        System.out.println("End");

        // 等待异步任务完成
        future.join();
    }
}

执行过程

  1. 开始执行System.out.println("Start"); 输出 “Start”。

  2. 异步任务:使用CompletableFuture.runAsync启动一个异步任务,在另一个线程中执行。异步任务模拟执行1秒钟,然后输出 “Async Task Completed”。

  3. 注册回调:使用thenRun注册一个回调函数,当异步任务完成后执行,输出 “Callback Executed”。

  4. 继续执行System.out.println("End"); 输出 “End”。

  5. 等待异步任务完成:使用future.join()等待异步任务完成,确保程序不会过早退出。

输出结果如下所示:

Start
End
Async Task Completed
Callback Executed

现在你对JavaScript中的同步和异步有了解了吗?不知道大家还有什么疑问?


文章来源:https://blog.csdn.net/weixin_43242942/article/details/139730533
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐

标签云