前端一面之 同步 vs 异步

异步 vs 同步

先看一下 下面的 demo

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

执行结果

100
300
200
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);

new Promise((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');

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秒后执行。
只有当文件读取操作完成后,定时器才会有机会执行。
图解
在这里插入图片描述

这是一个简化的时序图,事件循环的主要步骤如下:
开始事件循环: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文章

  • 25
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
前端开发中,我们经常会遇到需要处理异步操作的情况。异步操作是指不会阻塞代码执行的操作,例如发送网络请求、读取文件等。而同步操作则是会阻塞代码执行的操作。 为了解决异步操作带来的问题,我们可以使用回调函数、Promise、async/await等方式将异步代码转换为同步代码。 1. 回调函数:通过在异步操作完成后执行指定的回调函数来处理异步结果。例如: ```javascript function fetchData(callback) { setTimeout(function() { const data = '异步数据'; callback(data); }, 1000); } fetchData(function(data) { console.log(data); }); ``` 2. Promise:Promise 是一种用于处理异步操作的对象。它可以表示一个异步操作的最终完成或失败,并返回结果或错误信息。例如: ```javascript function fetchData() { return new Promise(function(resolve, reject) { setTimeout(function() { const data = '异步数据'; resolve(data); }, 1000); }); } fetchData().then(function(data) { console.log(data); }); ``` 3. async/await:async/await 是基于 Promise 的一种异步编程模型,使得异步代码看起来像同步代码,更易于理解和维护。例如: ```javascript function fetchData() { return new Promise(function(resolve, reject) { setTimeout(function() { const data = '异步数据'; resolve(data); }, 1000); }); } async function fetchDataAsync() { const data = await fetchData(); console.log(data); } fetchDataAsync(); ``` 这些是常见的解决异步操作的方式,根据具体需求和项目情况,选择合适的方式来处理异步操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值