The process executed by the JS engine

Overview

The execution process of the js engine is mainly divided into three stages, namely syntax analysis, pre-compilation and execution. In the last article, we introduced the syntax analysis and pre-compilation phases, so let's make a brief summary first, as follows:

  • Syntax analysis : Check the syntax of the loaded code blocks respectively. If the syntax is correct, it will enter the pre-compilation stage; if it is incorrect, stop the execution of the code block, find the next code block and load it, and enter the syntax of the code block again after the loading is complete. Analysis phase

  • Pre-compilation : After passing the syntax analysis phase and entering the pre-compilation phase, a variable object is created (the arguments object is created (in the function running environment), the function declaration is parsed in advance, and the variable declaration is promoted), and the scope chain and this point are determined.

If you still have questions, look back at the execution process of the js engine in the previous article (1) .


This article mainly analyzes the third stage of js engine execution – the execution stage . Before the analysis, we first consider the following two questions:

js is single-threaded. In order to avoid code parsing blocking, asynchronous execution is used, so what is its asynchronous execution mechanism?

Through the event loop (Event Loop), understand the principle of the event loop and understand the asynchronous execution mechanism of js. This article mainly introduces.

js is single-threaded, so does it mean that there is only one thread participating in the execution of js?

No, there will be four threads involved in the process, but only the JS engine thread will always execute the JS script program, and the other three threads will only assist and not participate in code parsing and execution. The threads participating in the js execution process are:

  • JS engine thread : Also known as the JS kernel, the main thread responsible for parsing and executing Javascript scripts (such as the V8 engine)

  • Event trigger thread : belongs to the browser kernel process and is not controlled by the JS engine thread. Mainly used to control events (such as mouse, keyboard and other events), when the event is triggered, the event trigger thread will push the event handler into the event queue , waiting for the JS engine thread to execute

  • Timer trigger thread : It mainly controls the timer setInterval and the delayer setTimeout, which are used for the timing of the timer. When the timing is completed and the trigger condition of the timer is satisfied, the processing function of the timer is pushed into the event queue and waits for the execution of the JS engine thread. .
    Note: W3C stipulates in the HTML standard that the time interval when setTimeout is lower than 4ms is counted as 4ms.

  • HTTP asynchronous request thread : After connecting through XMLHttpRequest, through a new thread opened by the browser, when monitoring the state change of readyState, if the callback function of this state is set, the processing function of this state will be pushed into the event queue and wait for the JS engine thread implement.
    Note: The number of concurrent connections requested by a browser to a domain name is limited. The limit is 6 for Chrome and Firefox, and 10 for ie8.

Summary: Only the JS engine thread is always executing the JS script program, and the other three threads are only responsible for pushing the processing functions that meet the trigger conditions into the event queue, waiting for the JS engine thread to execute.

execution phase

Let's analyze a typical example (from Tasks, microtasks, queues and schedules , it is recommended to read with a good English foundation, very good article):

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');

这里我直接划分例子的代码结构,简单描述分析执行过程,暂不解释该过程中的概念和原理,概念和原理将会在下面具体讲解如下:

  1. 宏任务(macro-task),宏任务又按执行顺序分为同步任务和异步任务

    • 同步任务

      console.log('script start');

      console.log('script end');
    • 异步任务

      setTimeout(function() {
      console.log('setTimeout');
      }, 0);
  2. 微任务(micro-task)


    Promise.resolve().then(function() {
    console.log('promise1');
    }).then(function() {
    console.log('promise2');
    });

在JS引擎执行过程中,进入执行阶段后,代码的执行顺序如下:

宏任务(同步任务) --> 微任务 --> 宏任务(异步任务)

输出结果为:

script start
script end
promise1
promise2
setTimeout

进入ES6或Node环境中,JS的任务分为两种,分别是宏任务(macro-task)微任务(micro-task),在最新的ECMAScript中,微任务称为jobs,宏任务称为task,他们的执行顺序如上。可能很多人对上面的分析并不理解,那么我们接下来继续对上面例子进行详细分析。

宏任务

宏任务(macro-task)可分为同步任务异步任务

  • 同步任务指的是在JS引擎主线程上按顺序执行的任务,只有前一个任务执行完毕后,才能执行后一个任务,形成一个执行栈(函数调用栈)。

  • 异步任务指的是不直接进入JS引擎主线程,而是满足触发条件时,相关的线程将该异步任务推进任务队列(task queue),等待JS引擎主线程上的任务执行完毕,空闲时读取执行的任务,例如异步Ajax,DOM事件,setTimeout等。

理解宏任务中同步任务和异步任务的执行顺序,那么就相当于理解了JS异步执行机制–事件循环(Event Loop)

事件循环

事件循环可以理解成由三部分组成,分别是:

  • 主线程执行栈

  • 异步任务等待触发

  • 任务队列

任务队列(task queue)就是以队列的数据结构对事件任务进行管理,特点是先进先出,后进后出


这里直接引用一张著名的图片(参考自Philip Roberts的演讲《Help, I’m stuck in an event-loop》),帮助我们理解,如下:
Event Loop

在JS引擎主线程执行过程中:

  • 首先执行宏任务的同步任务,在主线程上形成一个执行栈,可理解为函数调用栈;

  • 当执行栈中的函数调用到一些异步执行的API(例如异步Ajax,DOM事件,setTimeout等API),则会开启对应的线程(Http异步请求线程,事件触发线程和定时器触发线程)进行监控和控制

  • 当异步任务的事件满足触发条件时,对应的线程则会把该事件的处理函数推进任务队列(task queue)中,等待主线程读取执行

  • 当JS引擎主线程上的任务执行完毕,则会读取任务队列中的事件,将任务队列中的事件任务推进主线程中,按任务队列顺序执行

  • 当JS引擎主线程上的任务执行完毕后,则会再次读取任务队列中的事件任务,如此循环,这就是事件循环(Event Loop)的过程

如果还是不能理解,那么我们再次拿上面的例子进行详细分析,该例子中宏任务的代码部分是:

console.log('script start');

setTimeout(function() {
console.log('setTimeout');
}, 0);

console.log('script end');

代码执行过程如下:

  1. JS引擎主线程按代码顺序执行,当执行到console.log('script start');,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script start,然后继续向下执行;

  2. JS引擎主线程执行到setTimeout(function() { console.log('setTimeout'); }, 0);,JS引擎主线程认为setTimeout是异步任务API,则向浏览器内核进程申请开启定时器线程进行计时和控制该setTimeout任务。由于W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms,那么当计时到4ms时,定时器线程就把该回调处理函数推进任务队列中等待主线程执行,然后JS引擎主线程继续向下执行

  3. JS引擎主线程执行到console.log('script end');,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script end

  4. JS引擎主线程上的任务执行完毕(输出script start和script end)后,主线程空闲,则开始读取任务队列中的事件任务,将该任务队里的事件任务推进主线程中,按任务队列顺序执行,最终输出setTimeout,所以输出的结果顺序为script start script end setTimeout

以上便是JS引擎执行宏任务的整个过程。

理解该过程后,我们做一些拓展性的思考:

我们都知道setTimeout和setInterval是异步任务的定时器,需要添加到任务队列等待主线程执行,那么使用setTimeout模拟实现setInterval,会有区别吗?

答案是有区别的,我们不妨思考一下:

  • setTimeout实现setInterval只能通过递归调用

  • setTimeout是在到了指定时间的时候就把事件推到任务队列中,只有当在任务队列中的setTimeout事件被主线程执行后,才会继续再次在到了指定时间的时候把事件推到任务队列,那么setTimeout的事件执行肯定比指定的时间要久,具体相差多少跟代码执行时间有关

  • setInterval则是每次都精确的隔一段时间就向任务队列推入一个事件,无论上一个setInterval事件是否已经执行,所以有可能存在setInterval的事件任务累积,导致setInterval的代码重复连续执行多次,影响页面性能。

综合以上的分析,使用setTimeout实现计时功能是比setInterval性能更好的。当然如果不需要兼容低版本的IE浏览器,使用requestAnimationFrame是更好的选择。

我们继续再做进一步的思考,如下:

高频率触发的事件(例如滚动事件)触发频率过高会影响页面性能,甚至造成页面卡顿,我们是否可以利用计时器的原理进行优化呢?

是可以的,我们可以利用setTimeout实现计时器的原理,对高频触发的事件进行优化,实现点在于将多个触发事件合并成一个,这就是防抖节流,本文先不做具体讲解,大家可以自行研究,有机会我再另开文章分析。

微任务

微任务是在es6和node环境中出现的一个任务类型,如果不考虑es6和node环境的话,我们只需要理解宏任务事件循环的执行过程就已经足够了,但是到了es6和node环境,我们就需要理解微任务的执行顺序了。
微任务(micro-task)的API主要有:Promise, process.nextTick

这里我们直接引用一张流程图帮助我们理解,如下:
task

在宏任务中执行的任务有两种,分别是同步任务异步任务,因为异步任务会在满足触发条件时才会推进任务队列(task queue),然后等待主线程上的任务执行完毕,再读取任务队列中的任务事件,最后推进主线程执行,所以这里将异步任务即任务队列看作是新的宏任务。执行的过程如上图所示:

  1. 执行宏任务中同步任务,执行结束;

  2. 检查是否存在可执行的微任务,有的话执行所有微任务,然后读取任务队列的任务事件,推进主线程形成新的宏任务;没有的话则读取任务队列的任务事件,推进主线程形成新的宏任务

  3. 执行新宏任务的事件任务,再检查是否存在可执行的微任务,如此不断的重复循环

这就是加入微任务后的详细事件循环,如果还没有理解,那么们对一开始的例子做一个全面的分析,如下:

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');

执行过程如下:

  1. 代码块通过语法分析和预编译后,进入执行阶段,当JS引擎主线程执行到console.log('script start');,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script start,然后继续向下执行;

  2. JS引擎主线程执行到setTimeout(function() { console.log('setTimeout'); }, 0);,JS引擎主线程认为setTimeout是异步任务API,则向浏览器内核进程申请开启定时器线程进行计时和控制该setTimeout任务。由于W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms,那么当计时到4ms时,定时器线程就把该回调处理函数推进任务队列中等待主线程执行,然后JS引擎主线程继续向下执行

  3. JS引擎主线程执行到Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); });,JS引擎主线程认为Promise是一个微任务,这把该任务划分为微任务,等待执行

  4. JS引擎主线程执行到console.log('script end');,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script end

  5. 主线程上的宏任务执行完毕,则开始检测是否存在可执行的微任务,检测到一个Promise微任务,那么立刻执行,输出promise1promise2

  6. 微任务执行完毕,主线程开始读取任务队列中的事件任务setTimeout,推入主线程形成新宏任务,然后在主线程中执行,输出setTimeout

最后的输出结果即为:

script start
script end
promise1
promise2
setTimeout

总结

以上便是JS引擎执行的全部过程,JS引擎的执行过程其实并不复杂,只要多思考多研究就可以理解,理解该过程后可以在一定程度上提高对JS的认识。

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325202705&siteId=291194637