深入基础—JS运行机制
写在前面
做前端的时候,需要尽快把页面渲染出来,但页面渲染的历程也包含了很多的机制,DOM树和CSSOM树的构建、布局树、绘制、重绘重排……为了给予用户流畅的体验和交互,需要做很多的优化,想要对程序进行优化,就不得不了解程序运行的机制,了解了这些,在编码的时候思路也许会更清晰,看了很多js的运行机制文章,想着自己梳理一遍,加深一下印象。
JS是单线程
为什么JS是单线程
我们知道,浏览器是多进程的,也就是说,在浏览器运行中支持可以同时运行多个进程来执行不同的任务而不被打扰,就像我们打开多个标签页,有一个崩溃了而不会影响其它的标签页。浏览器需要同时完成很多的工作。 而JS是单线程的,为什么呢?这与它的用途有关,我们都在写脚本的时候,更多的是为了与用户进行互动和操作DOM对吧,而线程作为最小的资源调度单位,我们不希望线程执行时资源缺乏,也就是说如果是多线程的,会导致多个线程同时操作DOM而引发竞态条件(Race Condition),导致数据不一致或者其他问题。
JS引擎会阻塞渲染引擎
EventLoop 事件循环机制
为了保证页面成功地渲染而不卡顿,JS采用了事件循环机制。个人这样理解会比较顺畅一些,如有误请指正。
为了更好的理解这张图,需要理解下面几个概念:
事件表(Event Table)
事件表用于注册和存储事件及其对应的回调函数。当某个事件被触发时,它会在事件表中查找相应的回调函数并将其加入事件队列,比如sreTimeout()。
同步任务(synchronous task)
同步任务最好理解,也就是会立即执行的任务,会按顺序立即进入执行栈,不会产生阻塞。
异步任务(asynchronous task) :
- 异步任务不会直接进入执行栈,而是会在事件表(Event Table)进行注册后被放入任务队列(Event Queue)中,等待主线程空闲后再执行。
执行栈和任务队列
JS是单线程,所以同一时间只能执行一个任务,而执行栈就是储存代码执行期间的函数引用的一种数据结构。同样的,任务队列也是一种数据结构,任务队列储存待执行的异步任务,当执行栈为空时,将其推入执行栈执行。同时也分为宏任务队列和微任务队列。
宏任务队列(Macro Task Queue) :
- 存储宏任务,如
setTimeout
、setInterval
、I/O操作、UI渲染等。 - 执行过程中一次只会执行一个宏任务,如果其中有微任务或宏任务,会进入到相应的队列。
微任务队列(Micro Task Queue) :
- 存储微任务,如
Promise.then
、MutationObserver
、process.nextTick
等。 - 执行过程中会将当前队列的微任务一次性执行。同理,出现其他任务也会进入相应的任务队列。
在微任务或宏任务执行过程中遇见同步任务时,会进入到执行栈执行
好了,根据上面的理解,Event Loop时间循环如何进行的? 请思考一下。
初始状态
- 调用栈(执行栈的具体体现)为空,栈底是全局作用域
- 微任务为空
- 宏任务不为空 : 最开始的时候整个script脚本作为宏任务进入
Macro
队列
开始执行
- 在script脚本中开始向下执行代码,如果遇见同步代码则进入执行栈执行(如函数调用、变量赋值)
- 异步代码进入事件表注册。
- 如果存在回调 则会在计时器完成计时后分发到宏任务或者是微任务
- 否则 直接分发到宏任务或者微任务
- 当同步代码执行完毕 执行栈为空。js引擎存在
monitoring process
进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去任务队列那里检查是否有等待被执行的任务。
当前事件执行完,执行栈进入idle(空闲)状态 等待事件
所以事件循环机制也就是这样不断的重询
这里给一小段代码测试一下
console.log('start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise then callback');
});
console.log('end');
问个问题 希望未来的自己再看时也思考思考整个流程
所以setTimeout(fn(),0)
会是立即执行吗?
那么setInterval(fn(),ms)
又真的会按时执行吗?
代码运行结果:
start
end
promise then callback
setTimeout
写在最后呢
插个眼 这篇文章 挺有意思的 推荐看一下 终于算是吧JS运行机制的基础写完了,如果有不对的或者有疑问的欢迎各位一起指正讨论。
转载自:https://juejin.cn/post/7392782029254574120