日期:2020 年 7 月 10 日
js 事件循环机制
前言
众所周知,JavaScript 是一门 单线程 的编程语言,这就意味着它一次只能完成一件事情,如果有多个任务,就必须要排队一个一个来
而我们知道,任务有同步的也有异步的,按照上面的说法,如果某个任务耗时过长的话很可能就会造成页面的阻塞,但实际使用中我们发现其实它能很好地处理异步的问题,这其中就是 js 事件循环机制在起作用
进程、线程
进程 是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的
线程 是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的
浏览器执行线程
浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等
JS引擎线程 作为其中的线程之一,其特点是单线程工作,负责解析运行 JavaScript 脚本,JS 运行耗时过长就会导致页面阻塞
JS 事件循环机制
JavaScript 事件循环机制分为浏览器和 Node 事件循环机制,两者的实现技术不一样,这里主要讲的是浏览器部分
Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行
执行栈
栈,是一种数据结构,具有先进后出的原则
JS 中的执行栈就具有这样的结构,当引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并压入执行栈,每遇到一个函数调用,就会往栈中压入一个新的上下文,引擎执行栈顶的函数,执行完毕,弹出当前执行上下文
任务队列
任务队列是先进先出的数据结构
同步任务、异步任务
JS 单线程中的任务分为同步任务和异步任务,同步任务会按照他们在执行栈的顺序排队进入主线程执行,异步任务则会暂时挂起,等有结果时则会把他对应的回调函数添加到任务队列中等待主线程空闲时,也就是栈被清空时,被读取到栈中等待主线程执行
过程大致是这样:
宏任务(macro-task)和微任务(micro-task)
除了广义的同步任务和异步任务,JS 中的任务还可以细分为宏任务和微任务
宏任务:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
微任务:process.nextTick, Promise的then回调, Object.observe, MutationObserver
加入上面两个概念之后事件循环的过程:
第一次事件循环中,JavaScript 引擎会把整个 script 代码当成一个宏任务执行,执行完成之后,再检测本次循环中是否寻在微任务,存在的话就依次从微任务的任务队列中读取执行完所有的微任务,再读取宏任务的任务队列中的任务执行,再执行所有的微任务,如此循环
实例
看个例子:
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),所以就先执行这个任务,整个代码块会作为第一个宏任务进入主线程,遇到 console 直接输出 start,遇到 setTimeout 放入宏任务队列,遇到 promise.then() 放入微任务队列记为 then1, 又遇到一个记为 then2, 遇到 console 直接输出end
到这里,第一个回合跑完,事件队列出现了1个宏任务,2个微任务,先执行微任务,分别输出 promise1、promise2
最后执行宏任务,输出 setTimeout
再来一个:
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 0);
}).then(function() {
console.log('then1')
})
console.log('script end');
同样的,整个 script 会作为第一个宏任务进入主线程,遇到 console 直接输出 start,继续往后遇到 setTimeout 扔进宏任务队列记为 time1,再走遇到 promise , 我们知道 promise 在创建时是立即执行的, 所以这里先输出 promise1,然后又遇到 setTimeout 同样放进宏任务队列记为 time2, 再往后遇到 then 放进微任务队列,再走就遇到 console 了直接输出 end
这样一个回合算完了,这时任务队列出现了 2 个宏任务, 1个微任务,先执行微任务,输出 then1
然后执行宏任务,但要注意这两个计时器一个是 10 ms ,一个是 0, 所以第二个先执行完输出 timeout2, 然后第一个才输出 timeout1
本文参考如下文档结合个人理解所书:
Tasks, microtasks, queues and schedules