浅析前端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
好勒,收工!!
相关推荐
zhougl99627 分钟前
html处理Base文件流
linux·前端·html
花花鱼30 分钟前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_33 分钟前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo2 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
TDengine (老段)2 小时前
TDengine 中的关联查询
大数据·javascript·网络·物联网·时序数据库·tdengine·iotdb
杉之3 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端4 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡4 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木5 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!5 小时前
优选算法系列(5.位运算)
java·前端·c++·算法