看过很多关于JS Event Loop的文章和视频,在面试的时候,这个问题也逃不脱的,事件循环涉及的内容还是有点多的,这里自己在梳理下,以便加深自己的理解和记忆。
了解进程和线程
每当我们运行应用程序时,操作系统都会创建该应用程序的实例对象,该实例对象就是应用程序的进程,操作系统会按照进程为单位为应用程序分配资源,比如内存,这样程序才能够在计算机的操作系统中运行起来。
线程被包裹在进程之中,是进程中的实际运作单位,一条线程指的就是进程中的一个单一顺序的控制流。也就是说,应用程序要做的事情都存储在线程之中。可以这样认为,一条线程就是一个待办列表,供 CPU 执行。
浏览器就是多进程的
在浏览器中,当打开一个Tab页,就相当于创建了一个独立的浏览器进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。
进程与线程的区别
1、进程是 CPU 资源分配的最小单位;线程是 CPU 调度的最小单位
2、一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
浏览器渲染(Render)进程有哪些线程
浏览器内核是多线程的,一个浏览器通常由GUI 渲染线程
、JavaScript 引擎线程
、事件触发线程
、定时触发器线程
、异步 http 请求线程
这几个常驻线程组成。
在内核控制下各线程相互配合,才能在浏览器窗口可视文档中输出我们能看到的图像。
下面就来说说这些线程的作用:
GUI 渲染线程
- 主要负责页面的渲染,解析 HTML、CSS,构建 DOM 树,布局和绘制等。
- 当界面需要重绘或者由于某种操作引发回流时,将执行该线程。
- 该线程与 JS 引擎线程互斥,当执行 JS 引擎线程时,GUI 渲染会被挂起,GUI更新会被保存在一个队列中,当等到JS引擎空闲时才会去执行GUI更新。
JavaScript 引擎线程
- 该线程也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
- 浏览器无论什么时候只有一个js线程在运行js程序,JS是单线程的,由于每个Tab页面都是一个独立的进程,且每个页面浏览器渲染进程都会有一个JS引擎线程,因此每个Tab页面之间的JS引擎线程是互不影响的。
- 主要负责处理 JavaScript 脚本,执行代码。也就是负责执行准备好待执行的事件,即定时器计数结束,或者异步请求成功并正确返回时,将依次进入任务队列,等待 JS 引擎线程的执行。
- 该线程与 GUI 渲染线程 互斥,当 JS引擎线程执行 JavaScript脚本时间过长,将导致页面渲染的阻塞,页面会出现白屏页。
事件触发线程
- 对应的事件符合触发条件被触发时,该线程会将事件添加到待处理队列的队尾,交给JS引擎线程执行。
- 当setTimeout定时器计数结束、ajax等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将事件依次加入到任务队列的队尾,等待JS引擎线程的执行。
定时触发器线程
- 也就是
setInterval
与setTimeout
所在线程。 - 主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待 JS 引擎线程执行。
注意:
setTimeout
如果不设置时间或者设置时间小于4ms,则会默认为 4ms。
异步 http 请求线程
- 负责执行异步请求,比如ajax等一类的线程。
- 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待 JS 引擎线程执行。
JavaScript引擎是单线程
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?为了避免这种,所以JS是单线程的。
Js单线程
单线程的含义是js只能在一个线程上运行,也就说,js同时只能执行一个js任务,意味着js任务需要排队,如果前一个任务出现大量的耗时操作,后面的任务得不到执行,任务的积累会导致页面的“假死”。
js是单线程的,并不代表js引擎线程只有一个。js引擎有多个线程,一个主线程,其它的后台配合主线程。
Js执行任务方式
Js任务分两种:同步任务
、异步任务
。浏览器的一个渲染进程只分配给js一个主线程(JS引擎线程),用来执行代码,这意味着所有的任务最终都要进入主线程通过调用栈来执行。
同步任务
:同步任务都在主线程上排队执行的任务,形成一个函数调用栈
(执行栈),只有前一个任务执行完毕,才能执行后一个任务。
异步任务
:不进⼊主线程,⽽是进⼊任务队列
的任务,执行完毕之后会产生一个回调函数,并且通知主线程。当主线程上的任务执行完后,就会调取最早通知自己的回调函数,使其进入主线程中执行。
注意:
JS调用栈
就是函数执行的地方,是主线程在运行JavaScript代码的过程中形成的,遵循后进先出的规则。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。
任务队列
JavaScript运行时,除了一个运行线程,引擎还提供一个消息队列,里面是各种需要当前程序处理的消息。新的消息进入队列的时候,会自动排在队列的队尾。
当遇到一个异步事件后,JavaScript 引擎并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列。
任务队列的任务又分为宏任务(macrotask)
和微任务(microtask)
。
- 常见的
宏任务
:- script(整体代码)
- setTimout
- setInterval
- setImmediate(node.js环境)
- 进行异步的 I/O 操作
- UI交互事件
- Ajax
- 常见的
微任务
:- Promise.then
- async、await
- process.nextTick
- MutationObserver(HTML5新特性,监控某个节点)
事件循环
JavaScript从诞生就是单线程。但是单线程就导致有很多任务需要排队,只有一个任务执行完才能执行后一个任务。如果某个执行时间太长,就容易造成阻塞;为了解决这一问题,JavaScript引入了事件循环机制
。
事件循环是js代码所在运行环境(浏览器、nodejs)编译器的一种解析执行规则。事件循环不属于js代码本身的范畴,而是属于js编译器的范畴。
事件循环Event Loop执行顺序
- 执行同步代码(宏任务),就进入到了第一次事件循环
- 遇到异步宏任务,放入到宏任务队列里
- 遇到异步微任务,放入到微任务队列里
- 执行完所有同步代码
- 检查微任务任务队列,若不为空,就将微任务从队列中调入主线程执行
- 微任务代码执行完毕
- 检查是否需要执行UI重渲染,如果需要则重渲染UI
- 再次检查有无宏任务,若无宏任务,执行下一事件循环
浏览器中,JavaScript 引擎循环
地从任务队列中读取任务并执行,这种运行机制就叫做事件循环。
例子
上面说了那么多文绉绉的话,下面来看看实际的代码是怎么运行的?
例1
先来个简单的例子热热身:
console.log("start");
setTimeout(()=>{
console.log("setTimeout1");
Promise.resolve().then(data => {
console.log('Promise2');
});
},0);
setTimeout(()=>{
console.log("setTimeout2");
},0);
Promise.resolve().then(data=>{
console.log('Promise1');
});
console.log("end");
输出
start
end
Promise1
setTimeout1
Promise2
setTimeout2
JS引擎是如何执行这段代码的:
- 主线程上先执行同步代码,输出start
- 遇到第一个宏任务setTimeout,把它添加到宏任务队列中
- 遇到第二个宏任务setTimeout,把它添加到宏任务队列中
- 然后遇到Promise微任务,把它添加到微任务队列中
- 最后同步代码(宏任务)执行完成输出end
- 接下来就会检查微任务队列,发现此队列不为空,执行第一个promise的then回调,输出Promise1
- 此时微任务队列为空,进入下一个事件循环
- 检查宏任务队列,发现有setTimeout的回调函数,立即执行回调函数输出setTimeout1
- 检查微任务队列,发现队列不为空,执行promise的then回调,输出Promise2,这时微任务队列为空,进入下一个事件循环
- 检查宏任务队列,发现有setTimeout的回调函数, 立即执行回调函数输出setTimeout2
注意:
每一个宏任务执行完成,都回去检查微任务队列是否为空,如果不为空,就将微任务从队列中调入主线程执行,如果为空就进入下一次事件循环。
例2
现在来看个稍微复杂的例子:
console.log('start');
setTimeout(function () {
console.log('setTimeout1');
}, 0);
setTimeout(function () {
console.log('setTimeout2');
setTimeout(function () {
console.log('setTimeout3');
},0);
Promise.resolve().then(function () {
console.log('promise4');
});
}, 100);
Promise.resolve().then(function () {
console.log('promise1');
Promise.resolve().then(function () {
console.log('promise2');
});
}).then(function () {
console.log('promise3');
});
Promise.resolve().then(function () {
console.log('promise4');
});
console.log('end');
输出
start
end
promise1
promise4
promise2
promise3
setTimeout1
setTimeout2
promise4
setTimeout3
JS引擎是如何执行这段代码的:
- 和上面例1一样,先执行完主进程上的同步任务,输出start、end,在执行过程中遇到宏任务和微任务分别添加到各类任务队列中
- 同步任务执行完成,检查微任务队列是否为空,发现此队列不为空,执行第一个promise的then回调,输出promise1,这个时候发现有个Promise,添加到微任务队列
- 然后执行第二个promise的then回调,输出promise4
- 检查微任务队列,不为空,就跟着执行在第一个promise里面添加了的那个Promise,输出promise2
- 由于第一个promise的.then()后面跟了一个.then()依然是promise,所以后面这个.then()也会放到微任务队列继续执行,输出promise3
- 此时微任务队列为空,进入下一个事件循环, 检查宏任务队列,发现有setTimeout的回调函数,立即执行回调函数输出setTimeout1
- 检查微任务队列,队列为空,进入下一次事件循环
- 检查宏任务队列,发现有setTimeout的回调函数, 立即执行回调函数输出setTimeout2
- 接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中
- 检查微任务队列,发现此队列不为空,执行promise的then回调,输出promise4
- 此时微任务队列,进入下一个事件循环
- 检查宏任务队列,发现有setTimeout的回调函数,执行回调函数输出setTimeout3
- 代码执行结束
总结:以上就是我理解的有关浏览器EventLoop的相关内容了,希望对看到的小伙伴有帮助。