从一个题目浅浅理解JS运行机制 | 青训营笔记
这是我参加「第四届青训营 」笔记创作活动的第2天

小白一枚,欢迎指正

话不多说,直接上题

console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
    console.log('promise1');
}).then(function() {
    console.log('promise2');
});
console.log('script end');

答案如下

script start
script end
promise1
promise2
setTimeout

要想了解答案,需求具备以下小知识,没明白的不妨往下挖挖 ^.^

一、V8 引擎内的 栈

也被称为“调用栈、履行栈”。栈是一种LIFO(last input first output)后进先出的数据结构。

当函数被推入履行栈,JS引擎就开端解析函数体,在堆里创建变量、把新的函数调用推入栈顶、或许把函数自身分发给Web API调用的专属容器

当函数有了回来值,或许被分发到Web API容器,它就会被弹出栈,一起下一个函数调用会被推入栈顶。

假如JS引擎履行完一个函数,而且该函数没有清晰的指明回来值,JS引擎会默认的回来undefined然后再将之弹出栈。人们通常说的JS同步运转指的便是JS引擎解析函数然后弹出栈(再运转下一个函数)的运转流程。简言之,在单线程下同一时刻只做一件事。

二、Web API

从栈发送到Web API容器内的Web API调用(比方事情监听函数、HTTP/AJAX恳求、或许是守时器函数)会一向在Web API容器内,直到触发操作停止。这些触发操作可能是一个点击事情被触发、或许是HTTP恳求完结从数据源获取数据、或许是守时器到达触发的时刻点,一旦到达触发条件,一个回调函数就会被推入第四个也是最终一个容器:回调行列

回调行列将按增加的次序存储一切回调函数。当调用栈函数使命为空时,它会将行列最初的回调函数发送到调用栈。当调用栈函数使命再次清空时,它将发送下一个行列首位的回调函数。

回调行列是一种FIFO(first input first output)先进先出的数据结构。

三、烘托进程

WebAPI处理由栈中弹出后发送来的使命 所运用的线程和JS引擎所运用的的线程是不同的,浏览器有多个进程,其间烘托进程是我们首要关心的,里边又包含了多个线程。

页面的烘托,JS的履行,事情的循环,都在这个烘托进程内进行。

浏览器的烘托进程是多线程的, 首要包含的线程有:

  1. GUI烘托线程
    • 担任烘托浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和制作等。
    • 当界面需求重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会履行
    • 留意,GUI烘托线程与JS引擎线程是互斥的,当JS引擎履行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个行列中等到JS引擎闲暇时当即被履行。
  2. JS引擎线程
    • 也称为JS内核,担任处理Javascript脚本程序。(例如V8引擎)
    • JS引擎线程担任解析Javascript脚本,运转代码。
    • JS引擎一向等候着使命行列中使命的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时分都只有一个JS线程在运转JS程序
    • 同样留意,GUI烘托线程与JS引擎线程是互斥的,所以假如JS履行的时刻过长,这样就会造成页面的烘托不连贯,导致页面烘托加载堵塞。
  3. 事情触发线程
    • 归属于浏览器而不是JS引擎,用来控制事情循环(能够了解,JS引擎自己都忙不过来,需求浏览器另开线程协助)
    • 当JS引擎履行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步恳求等),会将对应使命增加到事情线程中
    • 当对应的事情符合触发条件被触发时,该线程会把事情增加到待处理行列的队尾,等候JS引擎的处理
    • 留意,由于JS的单线程关系,所以这些待处理行列中的事情都得排队等候JS引擎处理(当JS引擎闲暇时才会去履行)
  4. 守时触发器线程
    • setIntervalsetTimeout所在线程
    • 浏览器守时计数器并不是由JavaScript引擎计数的,(由于JavaScript引擎是单线程的, 假如处于堵塞线程状况就会影响记计时的精确)
    • 因而通过独自线程来计时并触发守时(计时结束后,增加到事情行列中,等候JS引擎闲暇后履行)
    • 留意,W3C在HTML标准中规则,规则要求setTimeout中低于4ms的时刻间隔算为4ms。
  5. 异步http恳求线程
    • 在XMLHttpRequest在衔接后是通过浏览器新开一个线程恳求
    • 将检测到状况改变时,假如设置有回调函数,异步线程就发生状况改变事情,将这个回调再放入事情行列中。再由JavaScript引擎履行。

至此,我们能够得到整个使命履行流程,这个流程也叫事情循环

从一个题目浅浅理解JS运行机制 | 青训营笔记

四、最终一步,宏使命和微使命

JS中分为两种使命类型:macrotaskmicrotask,在ECMAScript中,microtask称为jobs,macrotask可称为task

  • macrotask(又称之为宏使命),能够了解是每次履行栈履行的代码便是一个宏使命(包含每次从事情行列中获取一个事情回调并放到履行栈中履行)

    • 每一个task会自始至终将这个使命履行结束,不会履行其它
    • 浏览器为了能够使得JS内部task与DOM使命能够有序的履行,会在一个task履行结束后,在下一个 task 履行开端前,对页面进行重新烘托
    • task->烘托->task->...
  • microtask(又称为微使命),能够了解是在当时 task 履行结束后当即履行的使命

    • 也便是说,在当时task使命后,下一个task之前,在烘托之前
    • 所以它的响应速度比较setTimeout(setTimeout是task)会更快,由于无需等烘托
    • 也便是说,在某一个macrotask履行完后,就会将在它履行期间发生的一切microtask都履行结束(在烘托前)

什么样的场景会形成macrotask和microtask呢?

  • macrotask:主代码块,setTimeout,setInterval等(能够看到,事情行列中的每一个事情都是一个macrotask)
  • microtask:Promise,process.nextTick等

macrotask和microtask是由什么控制保护的呢?

  • macrotask中的事情都是放在一个事情行列中的,而这个行列由事情触发线程保护
  • microtask中的一切微使命都是增加到微使命行列(Job Queues)中,等候当时macrotask履行结束后履行,而这个行列由JS引擎线程保护

五、代入解说答案

  • 履行一个宏使命(栈中没有就从事情行列中获取)

    这个时分打印出履行栈中的同步代码,也便是script start 和script end

  • 履行过程中假如遇到微使命,就将它增加到微使命的使命行列中

    这个时分,的确在过程中发现了微使命,也便是promise,将这个使命放到微使命行列,此时履行栈中只剩下一个守时器使命,这个使命不会立刻履行,他会被放入宏使命行列,履行栈闲暇

  • 宏使命履行结束后,当即履行当时微使命行列中的一切微使命(顺次履行)

    此时短暂闲暇的履行栈又插入promise,输出promise1和promise2,然后持续闲暇

  • 当时宏使命履行结束,开端检查烘托,然后GUI线程接收烘托

  • 烘托结束后,JS线程持续接收,开端下一个宏使命(从事情行列中获取)

    由于履行栈闲暇,从宏使命行列中取出已经满足条件的守时器使命,这个时分输出了最终一行setTimeout

over

更多细节 推荐原文1,原文2