前言
本人前端小白,最近我想把学习的知识做成笔记发表出来,希望大家有错误可以指出来。
单线程
JavaScript 是一种单线程编程语言,这意味着它一次只能执行一个任务。为了处理可能需要较长时间的任务(如网络请求、文件读写等)而不阻塞主线程,JavaScript 引入了同步(Synchronous)和异步(Asynchronous)执行的概念。
(一)同步与异步
1.什么是同步(Synchronous)?
在同步模式下,代码按顺序逐行执行。当前面的代码行执行完毕后,才能执行后面的代码行。如果在执行一个函数或者语句时需要等待某些操作完成(如等待一个网络响应),那么JavaScript 引擎会阻塞该线程,直到操作完成。
例如:
console.log('开始');
console.log('中间');
console.log('结束');
2.什么是异步(Asynchronous)?
异步操作允许JavaScript 执行不需要等待某些操作完成的代码。异步操作通常涉及到以下几种方式:
- 回调函数(Callbacks) :这是最传统的异步处理方式,你定义一个函数,当异步操作完成时,该函数会被调用。通常有计时器(setimeout,setinterval)
console.log('开始');
setTimeout(() => {
console.log('中间的异步操作');
}, 1000);
console.log('结束');
//结果为:开始 结束 中间的异步操作
在上面的代码中,setTimeout 是一个常见的异步JavaScript函数,它将在1000毫秒后执行回调函数。控制台会首先打印“开始”和“结束”,然后在大约1秒后打印“中间的异步操作”。
- Promises:这是ES6引入的一种更优雅的异步处理方式。Promise代表了一个最终可能完成也可能失败的操作,并且它提供了更强大的错误处理。
console.log('开始');
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('中间的异步操作');
}, 1000);
}).then((message) => {
console.log(message);
});
console.log('结束');
//结果:开始 结束 中间的异步操作
- async/await:这是ES2017引入的语法,允许用同步的方式写异步代码。
async function asyncFunction() {
console.log('开始');
const message = await new Promise((resolve) => {
setTimeout(() => {
resolve('中间的异步操作');
}, 1000);
});
console.log(message);
console.log('结束');
}
asyncFunction();
在asyncFunction中,await关键字“暂停”了函数的执行,直到Promise解决(或者拒绝)。尽管代码看起来是同步的,但实际上它是异步执行的。
(二)process.nextTick与setimmediate方法
-
process.nextTick:在同步代码执行之后,异步代码执行之前执行
-
setimmediate:异步执行之后执行
所以代码执行顺序:
- 同步代码
- nextTick
- 异步代码
- setImmediate
(三)事件循环
JavaScript 的运行环境是单线程的,这意味着它一次只能执行一个任务。为了实现非阻塞的行为,JavaScript 引入了事件循环(Event Loop)机制。事件循环是JavaScript引擎处理异步任务的方式。以下是事件循环的基本工作原理:
执行栈(Call Stack)
当JavaScript代码开始执行时,它会进入执行栈(也称为调用栈)。这是一个后进先出(LIFO)的数据结构,用于存储在代码执行期间调用的所有函数。
任务队列(Task Queue)
当异步事件发生时(比如一个定时器到期,或者网络请求完成),对应的回调函数不会立即执行,而是被放入任务队列(也称为回调队列)中。任务队列是一个先进先出(FIFO)的数据结构。
事件循环(Event Loop)
事件循环不断地检查执行栈是否为空。如果执行栈为空,事件循环会从任务队列中取出第一个任务并将其放入执行栈中执行。这个过程会不断重复,这就是所谓的“事件循环”。 以下是事件循环的详细步骤:
- 执行同步代码:JavaScript引擎从全局代码开始,按顺序执行所有同步代码。
- 遇到异步任务:当遇到异步任务(如
setTimeout,setInterval,Promise,XMLHttpRequest等)时,JavaScript引擎会将其挂起,并继续执行栈中的其他代码。 - 异步任务完成:异步任务完成后,它的回调函数会被放入任务队列中。
- 检查执行栈:事件循环不断地检查执行栈是否为空。
- 执行任务队列中的任务:一旦执行栈为空,事件循环会从任务队列中取出第一个回调函数,并将其放入执行栈中执行。
- 重复过程:事件循环重复步骤4和5,直到任务队列为空。
(四)宏任务(Macro Task)与微任务(Micro Task)
在事件循环中,任务队列实际上分为宏任务队列和微任务队列:
- 宏任务(Macro Task):包括整体代码script,setTimeout,setInterval等。
- 微任务(Micro Task):包括Promise的回调,process.nextTick(Node.js环境)等。 事件循环的每个循环周期称为一个tick,在每个tick结束时,JavaScript引擎会先检查微任务队列,如果有微任务,则会执行它们直到微任务队列为空,然后才会执行下一个宏任务。
示例
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
执行结果:
script start
script end
promise1
promise2
setTimeout
解释:
console.log('script start');和console.log('script end');是同步代码,按顺序执行。setTimeout的回调函数被放入宏任务队列。Promise的回调函数被立即放入微任务队列。- 在执行栈为空后,事件循环先执行微任务队列中的
promise1和promise2。 - 最后,在下一个tick中,事件循环从宏任务队列中取出
setTimeout的回调并执行。