浅析前端JS的同步和异步任务(宏任务+微任务)+具体宏微任务对应操作+经典面试输出题

1.在了解同步任务、异步任务之前,我们需要知道,JavaScript是单线程
js 复制代码
JavaScript 是单线程的主要原因是出于设计和历史的考虑,尤其是在它最初被创建的时候,用于在浏览器中操作DOM,这样设计具有如下几点特性

1.简单性: 单线程模型使得 JavaScript 更加简单和易于学习。它避免了多线程编程中可能出现的复杂性,如死锁、竞态条件等问题。

2.协作模型: JavaScript是一门事件驱动的语言,采用基于事件循环的协作模型。浏览器中的用户交互、网络请求、定时器等事件都被添加到事件队列中,而 JavaScript 引擎通过事件循环逐一处理这些事件。

3.DOM 操作: JavaScript 最初是为了操作浏览器中的 DOM(文档对象模型)而设计的。在多线程的情况下,如果允许多个线程同时修改 DOM,可能导致复杂的同步问题,增加了编程的复杂性。

4.浏览器渲染: 浏览器渲染引擎是单线程的,它负责处理 DOM、CSSOM 构建和页面渲染。如果 JavaScript 是多线程的,可能导致与渲染引擎的竞争条件,影响页面渲染性能。
虽然 JavaScript 本身是单线程的,但通过异步编程模型(如回调函数、Promise、Async/Await),JavaScript 可以有效地处理并发任务,使得在等待 I/O 操作完成的时候,不会阻塞其他代码的执行。
2.现在就可以说到JavaScript的相关任务,JavaScript执行任务主要分为同步任务和异步任务,那么什么又是同步任务,什么又是异步任务呢
同步(Synchronous):
  1. 阻塞执行: 同步代码是按照从上到下的顺序执行的,每一行代码都必须等待上一行代码执行完毕后才能执行。这种方式称为阻塞执行,因为后续的代码要等待前面的代码执行完成。
  2. 顺序执行: 同步代码是按照书写的顺序一行一行地执行,每一行的执行都依赖于上一行的执行结果。
  3. 调用栈: 同步代码的执行过程体现在调用栈中,每一个函数调用都会在调用栈中形成一个执行上下文,执行上下文的执行顺序遵循调用栈的原则。
js 复制代码
console.log('Start');

function synchronousFunction() {
  console.log('Inside function');
}

synchronousFunction();

console.log('End');

在上述例子中,每一行代码都会按照顺序执行,不会有其他代码插入或打断。

输出结果顺序

Start
Inside function
End
异步(Asynchronous):
  1. 非阻塞执行: 异步代码在执行时不会等待上一行代码完成,而是将这部分代码放入异步队列,继续执行后续的代码。这使得后续的代码不需要等待异步操作完成,可以继续执行。
  2. 回调函数: 异步操作通常使用回调函数来处理异步结果。回调函数会在异步操作完成后被调用,以处理结果或执行进一步的操作。
  3. 事件循环: 异步代码的执行过程体现在事件循环中。当异步操作完成后,会将对应的回调函数放入事件队列,然后由事件循环负责执行这些回调函数。
案例:
js 复制代码
console.log('Start');

setTimeout(() => {
  console.log('Inside setTimeout');
}, 0);

console.log('End');
输出结果:
js 复制代码
Start
End
Inside setTimeout
而在 JavaScript 中,异步任务主要包括以下几类:
1.定时器任务(Timers):
  • setTimeoutsetInterval 用于在一定时间后执行回调函数或以固定间隔重复执行回调函数。
js 复制代码
setTimeout(() => {
  console.log('Delayed task');
}, 1000);

setInterval(() => {
  console.log('Repeated task');
}, 2000);
2.事件监听任务(Event Listeners):
  • 通过 addEventListener 等方法注册的事件处理函数,当特定事件发生时,这些函数将被异步执行。
js 复制代码
document.addEventListener('click', () => {
  console.log('Click event');
});
3.Promise 的 thencatch
  • 使用 Promise 可以创建异步任务,并通过 thencatch 处理异步操作的成功或失败。
js 复制代码
const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    resolve('Promise resolved');
  }, 1000);
});

promise.then(result => {
  console.log(result);
}).catch(error => {
  console.error(error);
});
4.XHR(XMLHttpRequest)和 Fetch:
  • 发起网络请求是一种异步操作,通过 XHR 或 Fetch API 发起的请求会在后台执行,并在请求完成时触发回调函数。
js 复制代码
// 使用 Fetch API
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));
5.WebSockets:
  • 通过 WebSockets 建立的长连接也属于异步任务,当有消息到达时触发相应的事件处理函数。
js 复制代码
const socket = new WebSocket('wss://example.com');

socket.onmessage = event => {
  console.log('Received message:', event.data);
};
6.异步函数(Async Functions):
  • 使用 asyncawait 关键字创建的异步函数。
js 复制代码
async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  console.log(data);
}

fetchData();

总体而言,异步任务涵盖了涉及时间、事件、网络请求等各种场景,JavaScript 提供了多种机制来处理这些异步操作,使得开发者能够更加方便地编写异步代码。

3.其实异步任务也可分为宏任务和微任务

简单来说:他们的区别主要在于执行顺序

js 复制代码
微任务先执行》》》宏任务再执行
详细理解来说
宏任务(Macrotask):
1. 执行时机: 宏任务代表较大的、离散的工作单元,通常包括整体的 script 代码、setTimeout、setInterval、I/O 操作等。宏任务的执行时机是在当前执行栈为空时,也就是说,在主线程执行栈执行完毕后才会执行宏任务。
2. 执行顺序: 宏任务按照它们进入执行环境的顺序执行,新的宏任务会被添加到当前宏任务队列的末尾。不同宏任务之间存在优先级,例如 setTimeout 定时器的时间到期、用户交互事件、I/O 操作等都会产生新的宏任务。
微任务(Microtask):
1. 执行时机: 微任务代表较小的任务单元,它们总是在当前宏任务执行结束之后、下一个宏任务开始之前执行。微任务包括 Promise 的处理等。
2. 执行顺序:微任务按照它们被添加到队列的顺序执行,新的微任务会被添加到队列的末尾。微任务队列在每一个宏任务执行结束后都会被清空,确保所有微任务都被执行。
示例:
js 复制代码
console.log('Start');

setTimeout(() => {
  console.log('Timeout (Macrotask)');
}, 0);

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

console.log('End');

输出结果:

js 复制代码
Start
End
Promise (Microtask 1)
Promise (Microtask 2)
Timeout (Macrotask)

在这个例子中,主线程执行同步代码,然后执行当前宏任务中的微任务(Promise 的 .then()),最后执行下一个宏任务中的定时器(setTimeout)。微任务的执行在宏任务之间,确保微任务优先于下一个宏任务执行。

4.接下来我会具体把所有的宏微任务都列出来

宏任务(Macrotask):

  1. setTimeout 和 setInterval: 定时器操作会被放入宏任务队列。

    js 复制代码
    // 宏任务
    setTimeout(() => {
      console.log('Timeout (Macrotask)');
    }, 0);
  2. I/O 操作: 诸如文件读写、网络请求等的 I/O 操作也会产生宏任务。

    js 复制代码
    // 宏任务
    readFile('example.txt', (err, data) => {
      if (err) throw err;
      console.log(data);
    });
  3. 事件处理: 事件处理器中的代码是宏任务。

    js 复制代码
    // 宏任务
    button.addEventListener('click', () => {
      console.log('Button Clicked (Macrotask)');
    });

微任务(Microtask):

  1. Promise 的 .then().catch() Promise 是微任务的主要来源,通过 .then().catch() 注册的回调函数是微任务。

    js 复制代码
    // 微任务
    Promise.resolve().then(() => {
      console.log('Promise (Microtask)');
    });
  2. Async/Await: 使用 await 关键字的异步函数返回的 Promise 也会产生微任务。

    js 复制代码
    // 微任务
    async function asyncFunction() {
      console.log('Async Function (Microtask)');
    }
    
    asyncFunction();
  3. MutationObserver: 使用 MutationObserver 监听 DOM 变化的回调也是微任务。

    js 复制代码
    // 微任务
    const observer = new MutationObserver(() => {
      console.log('DOM Mutation (Microtask)');
    });
    
    observer.observe(targetNode, config);
  4. process.nextTick: process.nextTick` 也属于微任务。

    js 复制代码
    // 微任务
    process.nextTick(() => {
      console.log('process.nextTick (Microtask)');
    });
最后来做两道题输出顺序的题吧,看看是否了解了他的执行顺序了

下面是一个简单的代码题,用于加强对宏任务和微任务的理解:

js 复制代码
console.log('Start');

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

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

console.log('End');

这段代码中包含了一个宏任务(setTimeout)和一个微任务(Promise)。以下是输出的解释:

  1. 首先,执行同步代码,输出 StartEnd
  2. 接着,遇到 setTimeout,它是一个宏任务,会被放入宏任务队列中,但由于设置了时间为 0,所以并不会立即执行。
  3. 然后,遇到 Promise.resolve().then(),这是一个微任务,会被放入微任务队列中,等待当前宏任务执行完毕后执行。
  4. 继续执行同步代码,输出 End
  5. 当整个宏任务执行完毕时,会开始执行微任务队列。此时,输出 Promise
  6. 最后,由于 setTimeout 的时间为 0,它会被移动到宏任务队列的末尾,输出 Timeout

因此,最终的输出顺序是:

Start
End
Promise
Timeout
再来一道复杂的题吧
js 复制代码
console.log('Start');

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

new Promise((resolve, reject) => {
  console.log('Promise 1');
  resolve();
})
  .then(() => {
    console.log('Promise 2');
    return new Promise((resolve) => {
      console.log('Promise 3');
      resolve();
    });
  })
  .then(() => {
    console.log('Promise 4');
  });

setTimeout(() => {
  console.log('Timeout 2');
  new Promise((resolve) => {
    console.log('Promise 5');
    resolve();
  }).then(() => {
    console.log('Promise 6');
  });
}, 0);

console.log('End');
  1. 同步代码按照顺序执行,输出 StartPromise 1
  2. 执行第一个 setTimeout,它是一个宏任务,将 Timeout 1 放入宏任务队列。
  3. 打印同步代码,输出 End
  4. 执行 Promise 的 .then(),输出 Promise 2Promise 3Promise 4
  5. 执行第二个 setTimeout,也是一个宏任务,将 Timeout 2 放入宏任务队列。
  6. 执行微任务队列中的 Promise 的 .then(),输出 Promise 5Promise 6
  7. 最后,执行宏任务队列中的 Timeout 1Timeout 2

请注意,微任务会在当前宏任务执行完毕后立即执行,而宏任务则需要等待当前宏任务执行完毕后,再从宏任务队列中取出一个宏任务执行。这就是为什么 Promise 5Promise 6Timeout 1Timeout 2 之前输出的原因。

js 复制代码
Start
Promise 1
End
Promise 2
Promise 3
Promise 4
Timeout 1
Timeout 2
Promise 5
Promise 6
好勒,收工!!
相关推荐
LCG元1 小时前
【面试问题】JIT 是什么?和 JVM 什么关系?
面试·职场和发展
迷雾漫步者1 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人5 小时前
前端知识补充—CSS
前端·css