执行栈与事件循环初识
执行栈
执行栈: Execution Stack(Call Stack),一个数据结构,用于存放各种函数的执行环境,每一个函数执行之前,它的相关信息会加入到执行栈。函数调用之前,创建执行环境,然后加入到执行栈;函数调用之后,销毁执行环境。
JS引擎永远执行的是执行栈的最顶部。
看一下下面简单代码的执行过程:
function b(){
}
function a(){
b();
}
a();
首先,全局执行上下文Global Execution Context
会被建立,这时候会一并建立this
、global object (window)
,在函数开始执行的过程中,function a
和function b
由于JS提升机制的缘故会先被建立在内存中,接着才会开始逐行执行函数。
接着,代码会执行到a()这个部分,这时候,会建立a的执行上下文(execution context
),并且被放置到执行栈(execution stack
)中。在这个execution stack
中,最上面的execution context
会是正在被执行的a()。如下图:
function a()
的execution context
建立后,便会开始执行function a
中的内容。由于在function a()
里面有去执行function b()
,因此,在这个execution stack
中,接下来最上面会变成function b()
的execution context
。如下图:
当function b()
执行完之后,会从execution stack
中离开,继续逐行执行function a。当function a 执行完之后,一样会从execution stack中抽离,再回到Global Execution Context逐行执行。如下图:
当function b()
执行完之后,会从execution stack
中离开
继续逐行执行function a()
。当function a()
执行完之后,一样会从execution stack
中抽离,再回到Global Execution Context
逐行执行。如下图:
异步函数
异步函数: 某些函数不会立即执行,需要等到某个时机到达后才会执行,这样的函数称之为异步函数。比如事件处理函数。异步函数的执行时机,会被宿主环境控制。
下面代码的执行结果是什么?
console.log("a")
setTimeout(() => {
console.log("b")
}, 0);
for (let i = 0; i < 1000; i++) {
console.log("c")
}
浏览器内核
简单来说浏览器内核是通过取得页面内容、整理信息(应用 CSS )、计算和组合最终输出可视化的图像结果,通常也被称为渲染引擎。
浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
- GUI 渲染线程
- JavaScript 引擎线程
- 定时触发器线程
- 事件触发线程
- 异步 http 请求线程(网络线程)
GUI 渲染线程
- 主要负责页面的渲染,解析 HTML、CSS,构建 DOM 树,布局和绘制等。
- 当界面需要重绘或者由于某种操作引发回流时,将执行该线程。
- 该线程与 JS 引擎线程互斥,当执行 JS 引擎线程时,GUI 渲染会被挂起,当任务队列空闲时,主线程才会去执行 GUI 渲染。
JavaScript 引擎线程
- 该线程当然是主要负责处理 JavaScript 脚本,执行代码。
- 也是主要负责执行准备好待执行的事件,即定时器计数结束,或者异步请求成功并正确返回时,将依次进入任务队列,等待 JS 引擎线程的执行。
- 当然,该线程与 GUI 渲染线程互斥,当 JS 引擎线程执行 JavaScript 脚本时间过长,将导致页面渲染的阻塞。
定时触发器线程
- 负责执行异步定时器一类的函数的线程,如:setTimeout、setInterval。
- 主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待 JS 引擎线程执行。
事件触发线程
- 主要负责将准备好的事件交给 JS 引擎线程执行。
比如 setTimeout 定时器计数结束, ajax 等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待 JS 引擎线程的执行。
异步 http 请求线程
- 负责执行异步请求一类的函数的线程,如:Promise、fetch、ajax 等。
- 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待 JS 引擎线程执行。
事件循环(轮询)
当上面的线程发生了某些事请,如果该线程发现,这件事情有处理程序,它会将该处理程序加入一个叫做事件队列的内存。当JS引擎发现,执行栈中已经没有了任何内容后,会将事件队列中的第一个函数加入到执行栈中执行。
JS引擎对事件队列的取出执行方式,以及与宿主环境的配合,称之为事件循环。
事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:
- 宏任务(队列):macroTask,计时器结束的回调、事件回调、http回调等等绝大部分异步函数进入宏队列
- 微任务(队列):MutationObserver,Promise产生的回调进入微队列
MutationObserver用于监听某个DOM对象的变化
当执行栈清空时,JS引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<ul id="container"></ul>
<button id="btn">点击</button>
<script>
let count = 1;
const ul = document.getElementById("container");
document.getElementById("btn").onclick = function A() {
setTimeout(function C() {
console.log("添加了一个li")
}, 0);
var li = document.createElement("li")
li.innerText = count++;
ul.appendChild(li);
}
//监听ul
const observer = new MutationObserver(function B() {
//当监听的dom元素发生变化时运行的回调函数
console.log("ul元素发生了变化")
})
//监听ul
observer.observe(ul, {
attributes: true, //监听属性的变化
childList: true, //监听子元素的变化
subtree: true //监听子树的变化
})
//取消监听
// observer.disconnect();
</script>
</body>
</html>
Comments