JavaScript 事件循环机制
为什么 JavaScript 是单线程?
js作为主要运行在浏览器的脚本语言,js主要用途之一是操作DOM。在js高程中举过例子🌰,如果js同时有两个线程,同时对同一个dom进行操作,这时浏览器应该听哪个线程的,如何判断优先级?
为了避免这种问题,js必须是一门单线程语言,并且在未来这个特点也不会改变。
执行栈与任务队列
因为js是单线程语言,当遇到异步任务(如ajax操作等)时,不可能一直等待异步完成,再继续往下执行,在这期间浏览器是空闲状态,显而易见这会导致巨大的资源浪费。
主线程跟执行栈是不同概念,执行栈是js引擎内部的概念,主线程是js引擎外部概念。 主线程执行栈执行同步任务,异步任务会进入任务队列,任务队列中的任务会进入执行栈执行。 遇到异步事件后,主线程会先执行同步任务,同步任务执行完毕后,主线程会从任务队列中取出异步任务执行。 任务队列中的任务会依次执行,直到任务队列为空。
示例代码:
javascript
let a = () => {
setTimeout(() => {
console.log('任务队列函数1')
}, 0)
for (let i = 0; i < 5000; i++) {
console.log('a的for循环')
}
console.log('a事件执行完')
}
let b = () => {
setTimeout(() => {
console.log('任务队列函数2')
}, 0)
for (let i = 0; i < 5000; i++) {
console.log('b的for循环')
}
console.log('b事件执行完')
}
a();
b();
执行顺序分析:
- 先执行script同步代码
- 输出a的for循环
- 输出b的for循环
- 执行异步任务
- 输出a事件执行完
- 输出b事件执行完
- 输出任务队列函数1
- 输出任务队列函数2
js异步执行的原理:
- 所有任务都在主线程上执行,形成一个执行栈
- 主线程执行栈执行同步任务,遇到异步任务,将异步任务放入任务队列
- 同步任务执行完毕后,主线程会从任务队列中取出异步任务执行
- 任务队列中的任务会依次执行,直到任务队列为空
宏任务与微任务
异步任务分为宏任务(macrotask)与微任务(microtask),不同的API注册的任务会依次进入自身对应的队列中。
宏任务包括:
- script(整体代码)
- setTimeout/setInterval
- UI 渲染
- I/O
- postMessage
- MessageChannel
- setImmediate(Node.js环境)
微任务包括:
- Promise
- MutaionObserver
- process.nextTick(Node.js环境)
面试题示例:
javascript
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})
console.log(4);
// 输出顺序:2,4,3,1
执行顺序分析:
- 先执行script同步代码
- 输出2(Promise构造函数中的代码是同步的)
- 输出4
- 执行微任务
- 输出3(Promise.then)
- 执行下一个宏任务
- 输出1(setTimeout)
Event Loop 事件循环
事件循环的基本概念
事件循环是 JavaScript 实现异步的核心机制。它负责执行代码、收集和处理事件以及执行队列中的子任务。
事件循环的执行顺序
- 执行同步代码(这属于宏任务)
- 执行微任务队列(microtask queue)
- 执行宏任务队列(macrotask queue)
- 重复 2-3 步骤
完整的事件循环过程
javascript
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
console.log('promise创建'); // 同步代码!
resolve();
})
.then(() => {
console.log('promise1');
})
.then(() => {
console.log('promise2');
});
console.log('script end');
// 正确的输出顺序:
// script start
// promise创建
// script end
// promise1
// promise2
// setTimeout
- Promise 构造函数是同步的,then/catch/finally 的回调才会进入微任务队列
详细执行过程:
首先执行宏任务中的同步代码
- 打印 'script start'
- 遇到 setTimeout,将其回调放入宏任务队列
- 遇到 Promise,将 then 回调放入微任务队列
- 打印 'script end'
执行所有微任务
- 执行第一个 then,打印 'promise1'
- 第二个 then 进入微任务队列
- 执行第二个 then,打印 'promise2'
执行下一个宏任务
- 执行 setTimeout 回调,打印 'setTimeout'
注意事项
- 微任务优先级高于宏任务
- 新产生的微任务会立即执行
- requestAnimationFrame 在重新渲染前执行
- 浏览器完成一次事件循环后可能会进行重新渲染
实际应用示例:
javascript
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
浏览器渲染时机
- 一般是在一次事件循环结束后
- 如果有 requestAnimationFrame,会在重新渲染前执行
- CSS 变动和 DOM 变动可能会触发重新渲染
Promise 核心逻辑实现
原生Promise:
javascript
const promise = new Promise((resolve,reject)) => {
resolve('success')
reject('error')
}
promise.then(value => {
console.log('resolve',value)
},reason => {
console.log('reject',reason)
})