Event Loop,事件环,线程进程。这些概念对初识前端的同学来说可能会一头雾水。而且运行js代码的运行环境除了浏览器还有node。因此不同环境处理Event Loop又变得不同,十分容易混淆。如果你有这样的疑问。下文将给你一个清晰的解释。
首先我们简化一下概念,把进程,线程,事件环,这些概念梳理一下。清晰了概念后面用到的时候就会有共鸣。
拿出在教科书里的概念: 1、调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位; 2、并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行; 3、拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源; 4、系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
浏览器的进程
眼见为实,我们才能说浏览器是多线程的。那我们用可视化的角度看一下浏览器的这个进程和线程结构
从图中看黄色的圆角框里包裹的都是进程。蓝色的直角框里包裹的都是浏览器渲染引擎(浏览器内核)所包含的线程。对应上面的概念“一个进程可以有多个线程,但至少有一个线程”。前三个进程刚刚在1-3里都说过了。 介绍了刚刚那么多我们前端的操作其实都是在3中浏览器渲染引擎中处理。真正干活的就是线程。对应上面的概念“处理机分给线程,即真正在处理机上运行的是线程”。
接下来看一下浏览器引擎(进程)中包含哪些线程
UI渲染线程 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
注意:UI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),UI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
js引擎线程(JS解析线程) 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎) JS引擎线程负责解析Javascript脚本,运行代码。 JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都__只有一个JS线程在运行JS程序__
同样注意:UI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
事件触发线程 __归属于浏览器__而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助) 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
注意:由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
定时触发器线程 传说中的setInterval与setTimeout所在线程 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确) 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
注意:W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
异步http请求线程 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
以上线程,每个拿出来都可以详细的说上一篇。Event Loop涉及到的JS引擎的一些运行机制的分析。我们可以将这些线程理解为,
js渲染引擎的Event Loop如下图
这时候拿出几道题看一下会更清晰 题目1:
setTimeout(function(){
console.log(0)
},500)
setTimeout(function(){
console.log(1)
},1000)
setTimeout(function(){
console.log(2)
},2000)
for(;;){
}
上面这段代码用于不会有输出,同步代码死循环阻塞了执行栈。虽然定时后回调加入执行队列,但是异永远不会执行。 题目二:
setTimeout(function(){
console.log('setTimeout1');
Promise.resolve().then(()=>{
console.log('then1');
});
},0)
Promise.resolve().then(()=>{
console.log('then2');
Promise.resolve().then(()=>{
console.log('then3');
})
setTimeout(function(){
console.log('setTimeout2');
},0)
})
答案:then2 then3 setTimeout1 then1 setTimeout2 首先在题目中出现了es6的promise,他的出现让原来我们理解的__事件环产生了一些不同__。 为什么呢?因为Promise里有了一个一个新的概念:microtask 此时JS中分为__两种任务类型__:macrotask和microtask,在ECMAScript中,microtask称为jobs,macrotask可称为task
首先说明,是以__浏览器为处理环境__下的执行逻辑 浏览器环境下的微任务和宏任务有哪些 宏任务:setTimeout setImmediate MessageChannel 微任务:Promise.then MutationObserver 记住两点:
然后题目入手分析宏任务和微任务的执行
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。他的目标就是解析js代码,让他能运行起来。
和浏览器环境下类似,他有一个解析js的主线程,其他线程作为辅助,但是因为不涉及操作dom,ui线程就不存在了。(各个线程的概念参考浏览器环境下的线程) 单线程在浏览器运行环境中的弊端体现在阻塞页面执行。 那么node作为后端服务,单线程有什么利弊? 优点:
缺点:
事件环的整体还是不变的,执行栈,消息队列,api。不同的是,node下的消息队列有所不同
分析一下node下的消息队列
从一个题目入手感受一下node环境和浏览器环境下的不同
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise');
});
}, 0)
setTimeout(() => {
console.log('timeout2');
}, 0)
浏览器下的结果:timeout1 promise timeout2 node下的结果:timout1 timeout2 promise
node环境下的微任务和宏任务有哪些 宏任务:setTimeout setImmediate 微任务:Promise.then process.nextTick 题目三可以很好的分析node环境下的任务执行 node环境下运行流程
注意:同样是微任务,process.nextTick,优于promise.then先执行
Promise.resolve().then(() => {
console.log('then')
})
process.nextTick(() => {
console.log('nextTick')
});
//nextTick then
注意:同样是宏任务,setTimeout和setImediate执行的先后顺序是不确定的,依赖于执行栈执行的速度。
setImmediate(function () {
console.log('setImmediate')
});
setTimeout(function () {
console.log('setTimeout')
}, 0); // ->4
但是在如下场景下是有固定输出的
let fs = require('fs');
fs.readFile('./gitignore', function () { // io的下一个事件队列是check阶段
setImmediate(function () {
console.log('setImmediate')
});
setTimeout(function () {
console.log('setTimeout')
}, 0); // ->4
})
给个提示,读文件是io操作,io执行之后首先要check,check之后或没有check内容再去检查定时队列。 那么结果就留给大家自行分析了。
希望这篇文章能给初识js的你一个清晰的大框,也是梳理我自己的知识。可能我理解的也很粗浅,有错误的地方,希望大家帮忙指正。