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):
- 阻塞执行: 同步代码是按照从上到下的顺序执行的,每一行代码都必须等待上一行代码执行完毕后才能执行。这种方式称为阻塞执行,因为后续的代码要等待前面的代码执行完成。
- 顺序执行: 同步代码是按照书写的顺序一行一行地执行,每一行的执行都依赖于上一行的执行结果。
- 调用栈: 同步代码的执行过程体现在调用栈中,每一个函数调用都会在调用栈中形成一个执行上下文,执行上下文的执行顺序遵循调用栈的原则。
js
console.log('Start');
function synchronousFunction() {
console.log('Inside function');
}
synchronousFunction();
console.log('End');
在上述例子中,每一行代码都会按照顺序执行,不会有其他代码插入或打断。
输出结果顺序
Start
Inside function
End
异步(Asynchronous):
- 非阻塞执行: 异步代码在执行时不会等待上一行代码完成,而是将这部分代码放入异步队列,继续执行后续的代码。这使得后续的代码不需要等待异步操作完成,可以继续执行。
- 回调函数: 异步操作通常使用回调函数来处理异步结果。回调函数会在异步操作完成后被调用,以处理结果或执行进一步的操作。
- 事件循环: 异步代码的执行过程体现在事件循环中。当异步操作完成后,会将对应的回调函数放入事件队列,然后由事件循环负责执行这些回调函数。
案例:
js
console.log('Start');
setTimeout(() => {
console.log('Inside setTimeout');
}, 0);
console.log('End');
输出结果:
js
Start
End
Inside setTimeout
而在 JavaScript 中,异步任务主要包括以下几类:
1.定时器任务(Timers):
setTimeout
和setInterval
: 用于在一定时间后执行回调函数或以固定间隔重复执行回调函数。
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 的 then
和 catch
:
- 使用 Promise 可以创建异步任务,并通过
then
和catch
处理异步操作的成功或失败。
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):
- 使用
async
和await
关键字创建的异步函数。
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):
-
setTimeout 和 setInterval: 定时器操作会被放入宏任务队列。
js// 宏任务 setTimeout(() => { console.log('Timeout (Macrotask)'); }, 0);
-
I/O 操作: 诸如文件读写、网络请求等的 I/O 操作也会产生宏任务。
js// 宏任务 readFile('example.txt', (err, data) => { if (err) throw err; console.log(data); });
-
事件处理: 事件处理器中的代码是宏任务。
js// 宏任务 button.addEventListener('click', () => { console.log('Button Clicked (Macrotask)'); });
微任务(Microtask):
-
Promise 的
.then()
和.catch()
: Promise 是微任务的主要来源,通过.then()
和.catch()
注册的回调函数是微任务。js// 微任务 Promise.resolve().then(() => { console.log('Promise (Microtask)'); });
-
Async/Await: 使用
await
关键字的异步函数返回的 Promise 也会产生微任务。js// 微任务 async function asyncFunction() { console.log('Async Function (Microtask)'); } asyncFunction();
-
MutationObserver: 使用 MutationObserver 监听 DOM 变化的回调也是微任务。
js// 微任务 const observer = new MutationObserver(() => { console.log('DOM Mutation (Microtask)'); }); observer.observe(targetNode, config);
-
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
)。以下是输出的解释:
- 首先,执行同步代码,输出
Start
和End
。 - 接着,遇到
setTimeout
,它是一个宏任务,会被放入宏任务队列中,但由于设置了时间为 0,所以并不会立即执行。 - 然后,遇到
Promise.resolve().then()
,这是一个微任务,会被放入微任务队列中,等待当前宏任务执行完毕后执行。 - 继续执行同步代码,输出
End
。 - 当整个宏任务执行完毕时,会开始执行微任务队列。此时,输出
Promise
。 - 最后,由于
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');
- 同步代码按照顺序执行,输出
Start
和Promise 1
。 - 执行第一个
setTimeout
,它是一个宏任务,将Timeout 1
放入宏任务队列。 - 打印同步代码,输出
End
。 - 执行 Promise 的
.then()
,输出Promise 2
、Promise 3
、Promise 4
。 - 执行第二个
setTimeout
,也是一个宏任务,将Timeout 2
放入宏任务队列。 - 执行微任务队列中的 Promise 的
.then()
,输出Promise 5
和Promise 6
。 - 最后,执行宏任务队列中的
Timeout 1
和Timeout 2
。
请注意,微任务会在当前宏任务执行完毕后立即执行,而宏任务则需要等待当前宏任务执行完毕后,再从宏任务队列中取出一个宏任务执行。这就是为什么 Promise 5
和 Promise 6
在 Timeout 1
和 Timeout 2
之前输出的原因。
js
Start
Promise 1
End
Promise 2
Promise 3
Promise 4
Timeout 1
Timeout 2
Promise 5
Promise 6