JS浏览器事件循环机制
进程和线程
- 进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。
- 线程是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的。
浏览器内核
浏览器是多进程的,浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。浏览器内核有多种线程在工作:
- GUI渲染线程:
- 负责渲染页面,解析HTML,CSS构成DOM树等,当页面重绘或回流时调用该线程。
- 和JS引擎线程互斥,当JS引擎线程在工作的时候,GUI渲染线程会被挂起,GUI更新被放入在JS任务队列中,等待JS引擎线程空闲的时候继续执行。
- JS引擎线程:
- 单线程工作,负责解析运行javascript脚本。
- 和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。
- 事件触发线程:
- 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。
- 定时器触发线程:
- 浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。
- 开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待JS引擎处理。
- http请求线程:
- http 请求的时候会开启一条请求线程。
- 请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。
JS引擎是单线程
JavaScript 引擎是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务。
JS事件循环机制
JavaScript事件循环机制分为浏览器和Node事件循环机制,这里主要讲的是浏览器部分。
Javascript 有一个main thread
主线程和call-stack
调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。这里先了解JS执行环境及作用域,可以看这篇:浅谈JS执行环境和作用域。
- JS调用栈
- JS 调用栈是一种后进先出的数据结构。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。
- 同步任务、异步任务
- JavaScript 单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行,异步任务则会在异步有了结果后将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。
- Event loop
- 调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环。
事件循环下的宏任务和微任务
通常我们把异步任务分为宏任务与微任务,它们的区分在于:
- 宏任务(macro-task):一般是JS引擎和宿主环境发生通信产生的回调任务,比如 setTimeout,setInterval是浏览器进行计时的,其中回调函数的执行时间需要浏览器通知到JS引擎,网络模块,I/O处理的通信回调也是。整体script代码、setTimeout、setInterval、setImmediate、DOM事件回调,ajax请求结束后的回调都是。执行顺序:整体script代码>setTimeout>setInterval>setImmediate
- 微任务(micro-task):一般是宏任务在线程中执行时产生的回调。比如process.nextTick、Promise、Object.observe(已废弃), MutationObserver(DOM监听),都是JS引擎自身可以监听到的回调。执行顺序:process.nextTick>Promise>Object.observe
在事件循环中,任务一般都是由宏任务开始执行的(JS代码的加载执行),在宏任务的执行过程中,可能会产生新的宏任务和微任务,这时微任务会被添加到当前任务队列,当前宏任务执行完,会执行任务队列中的微任务,当前队列的微任务执行完毕,再执行新的宏任务。
典型案例
- 案例说明一
setImmediate(function(){
console.log(1);
},0);
setTimeout(function(){
console.log(2);
},0);
new Promise(function(resolve){
console.log(3);
resolve();
console.log(4);
}).then(function(){
console.log(5);
});
console.log(6);
process.nextTick(function(){
console.log(7);
});
console.log(8);
复制代码
根据js的运行原理解释上面代码的执行顺序:
代码都是从script主程序开始执行;
创建setImmediate宏任务;
创建setTimeout宏任务;
执行到Promise,输出3、4,当前宏任务(script主程序)的程序还没有执行完,创建微任务Promise.then的回调;
执行console.log(6)输出6;
创建微任务process.nextTick;
执行console.log(8)输出8;
执行任务队列中的微任务,按照优先级:process.nextTick > Promise.then 依次输出 7,5;
微任务执行完毕,执行宏任务,按照优先级: setTimeout > setImmediate 依次输出2,1
// 3 4 6 8 7 5 2 1
复制代码
- 案例说明二
setTimeout(function(){
console.log(1);
},0)
async function async1(){
console.log(2);
await async2();
console.log(3);
}
async function async2(){
console.log(4);
}
async1();
new Promise(function(resolve,reject){
console.log(5);
resolve();
}).then(function(e2){
console.log(6);
})
console.log(7);
复制代码
根据js的运行原理解释上面代码的执行顺序:
代码都是从script主程序开始执行;
创建setTimeout宏任务;
执行到async1(),等待await返回值后(此时打印了2,4)继续执行主程序代码,await后的代码被阻断,加入任务队列等待执行;
执行Promise输出5,当前宏任务(script主程序)的程序还没有执行完,创建微任务Promise.then的回调;
执行console.log(7)输出7;
执行任务队列中的微任务,按照顺序,依次打印await后的代码输出3,Promise.then的回调6;
微任务执行完毕,执行宏任务setTimeout,打印1。
// 2 4 5 7 3 6 1
复制代码
注意async
函数里await
右边的语句会立即执行,下面的代码进行等待状态,await
返回值以后才会继续执行下一条语句。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END