0


前端一面之 同步 vs 异步

异步 vs 同步

先看一下 下面的 demo

console.log(100)setTimeout(function(){
    console.log(200)},1000)
console.log(300)

执行结果

100300200
console.log(100)alert(200)// 1秒钟之后点击确认
console.log(300)

这俩到底有何区别?——
第一个示例中间的步骤根本没有阻塞接下来程序的运行,
而第二个示例却阻塞了后面程序的运行。
前面这种表现就叫做 异步(后面这个叫做 同步 ),
即不会阻塞后面程序的运行。

异步和单线程

JS 需要异步的根本原因是 JS 是单线程运行的,即在同一时间只能做一件事,不能“一心二用”。

一个 Ajax 请求由于网络比较慢,请求需要 5 秒钟。
如果是同步,这 5 秒钟页面就卡死在这里啥也干不了了。
异步的话,就好很多了,5 秒等待就等待了,其他事情不耽误做,
至于那 5 秒钟等待是网速太慢,不是因为 JS 的原因。

讲到单线程,我们再来看个真题:

讲解一下下面代码的执行结果

var a =true;setTimeout(function(){
    a =false;},100)while(a){
    console.log('while执行了')}

这是一个很有迷惑性的题目,不少候选人认为100ms之后,
由于a变成了false,所以while就中止了,
实际不是这样,因为JS是单线程的,
所以进入while循环之后,
没有「时间」(线程)去跑定时器了,
所以这个代码跑起来是个死循环!

异步任务

异步任务是在主线程执行的同时,
通过回调函数或其他机制委托给其他线程或事件来处理的任务。
在执行异步任务时,主线程不会等待任务完成,
而是继续执行后续代码。包括:
在这里插入图片描述

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

console.log('End');

在上述例子中,setTimeout 是一个异步任务,
它会在1秒后将回调函数推入任务队列
而主线程不会等待这个1秒,
而是继续执行后面的 console.log(‘End’)。
当主线程的同步任务执行完成后
它会检查任务队列,
将异步任务的回调函数推入执行栈,最终输出 ‘Timeout callback’。

任务队列类型

任务队列分为宏任务队列(macrotask queue)微任务队列(microtask queue)两种。
JavaScript 引擎遵循事件循环的机制,
在执行完当前宏任务后,
会检查微任务队列,执行其中的微任务,
然后再取下一个宏任务执行。
这个过程不断循环,形成事件循环。

  1. 宏任务队列

所有同步任务
I/O 操作, 文件读写 数据库读写等等
setTimeout、setInterval
setImmediate(Node.js环境)
requestAnimationFrame
事件监听回调函数

  1. 微任务(Microtasks)是一些较小粒度、高优先级的任务,包括:

Promise的then、catch、finally
async/await中的代码
Generator函数
MutationObserver
process.nextTick(Node.js 环境)

任务执行过程

首先,必须要明确,在JavaScript中,所有任务都在主线程上执行
任务执行过程分为同步任务和异步任务两个阶段
异步任务的处理经历两个主要阶段
Event Table(事件表)和 Event Queue(事件队列)。
Event Table存储了宏任务的相关信息
包括事件监听和相应的回调函数。
当特定类型的事件发生时,对应的回调函数被添加到事件队列中,等待执行。
例如,你可以通过addEventListener来将事件监听器注册到事件表上:

document.addEventListener('click',function(){
  console.log('Hello world!');});

微任务与 Event Queue 密切相关。
当执行栈中的代码执行完毕后,JavaScript引擎会不断地检查事件队列
如果队列不为空就将队列中的事件一个个取出,并执行相应的回调函数。

任务队列的执行流程可概括为:
同步任务在主线程排队执行,异步任务在事件队列排队等待进入主线程执行。
遇到宏任务则推进宏任务队列,遇到微任务则推进微任务队列。
执行宏任务,执行完毕后检查当前层的微任务并执行。
继续执行下一个宏任务,执行对应层次的微任务,直至全部执行完毕。

console.log(1);setTimeout(()=>{
    console.log(2);},0);

console.log(3);newPromise((resolve)=>{
    console.log(4);resolve();
    console.log(5);}).then(()=>{
    console.log(6);});

console.log(7);

执行顺序解析:1 => 3 => 4 => 5 => 7 => 6 => 2。

  1. 创建Promise实例是同步的,所以1、3、4、5、7是同步执行的。
  2. then方法是微任务,放入微任务队列中,在当前脚本执行完毕后立即发生。
  3. 同步任务执行完毕后,执行微任务队列中的微任务。
  4. 最后,setTimeout放入宏任务队列,按照先进先出的原则执行。

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

const fs =require('fs');functionreadFileSync(){// 打印"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');functionreadFileAsync(){
    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');functionreadFileSync(){
    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秒后执行。
只有当文件读取操作完成后,定时器才会有机会执行。
图解
在这里插入图片描述

这是一个简化的时序图,事件循环的主要步骤如下:
开始事件循环:JavaScript引擎开始执行代码。
同步任务队列:引擎首先检查同步任务队列(实际上是调用栈中的任务),执行队列中的任务。
执行同步任务:引擎开始执行同步任务,直到队列为空。
检查微任务队列:同步任务执行完毕后,引擎检查微任务队列。
执行微任务:如果有微任务(如Promise的.then()回调),引擎会执行这些微任务。
检查宏任务队列:微任务执行完毕后,引擎检查宏任务队列。
执行宏任务:如果有宏任务(如setTimeout回调),引擎会执行这些宏任务。执行完毕后再次检查微任务队列。
等待新任务:如果宏任务执行完毕,引擎会等待新的任务(如新的异步操作或宏任务)。
循环:引擎会不断地循环执行上述步骤。
在这里插入图片描述
分析一下
setTimeout 为定时器, 加入宏任务中
new Promise(function(resolve){ console.log(“promise1”); resolve()}) , 这里的 promise1 是同步执行
在这里插入图片描述

总结一下

有关于执行顺序
① 分清同步 、 异步代码
注意: new Promise() 里面参数(函数)是同步执行的, 但是.then() 是加入微任务队列中
② 区别哪写是加入宏任务, 哪些加入微任务
宏任务: setTimeout、setInterval 事件监听
微任务: then后面 await 后面
③ 记住在执行宏任务前, 一定需要清空微任务队列
注意: 有一些代码中时微任务里面添加微任务, 上面的案例一

可能有一些同学不是很理解上面的第一个注意点,下面详细讲一下
在浏览器中打印一下 Promise

console.dir( Promise )

在这里插入图片描述
new Promise(): 其实 Promise 可以理解成一个构建函数,
通过 new Promise() 的方式, 创建实例
打印 Promise 我们可以知道 Promise.prototype 定义了一些方法
而 new Promise().then() 可以理解成实例对象的调用构造函数原型方法
如果你还是不懂,可以看一下我有关于原型以及Promise文章

标签: 前端

本文转载自: https://blog.csdn.net/m0_69551472/article/details/140355926
版权归原作者 爱敲代码小黑 所有, 如有侵权,请联系我们删除。

“前端一面之 同步 vs 异步”的评论:

还没有评论