JS执行机制

2020-04-03 16:27发布

一、前言:

在本文中我们将介绍到如下内容:

1js单线程

2js的同步异步和事件循环

3、宏任务和微任务

4、案例分析

二、JS单线程

JavaScriptJS1995年由Netscape公司的Brendan Eich,在网景导航者浏览器上首次设计实现而成js的发明用途是面向浏览器的;js的功能除了包含ES的语法外,更重要的是操作DOMBOM。所以在设计之初,它就被设计为一种单线程的语言;因为多线程设计会带来很多同步上的问题;比如:我们现在假设了两个线程,aba线程要给DOM元素div赋值,b线程要获取元素div的值;如果按照顺序去做a->b可能符合我们的预期,但是b->a我们就无法得到最新的值。这里的多线程执行次序和同步上的问题就很难操作;所以基于浏览器的特性,把js设计为一种单线程的语言。

单线程不存在上面的多线程同步和执行次序问题,它执行起来像一个队列,排队执行,执行完一个任务,执行下一个任务。这对于浏览器的DOMBOM来说完全是可控的;但是如果有些任务执行的时间比较久,这对于用户体验是致命的,比如我们执行一个js计算操作,需要10秒钟,那么整个程序就需要等待这10秒才能继续执行,浏览器上就出现白屏加载10秒钟的现象;这种情况下我们的网页就完全没有用户体验了。

所以,对于js单线程面临的任务执行排队问题,js还设计有同步任务和异步任务。

三、JS的同步异步和事件循环

我们在了解js同步和异步之前,首先要了解什么是同步,什么是异步。

同步,事情要一件一件做,饭要一口一口吃;就是这个道理;js在做一个事情的时候是专注的,无法做其他任何事情,必须要做完这个事情才能继续执行其他。

异步,我们早上起床后,可以去烧水,烧水过程中我们可以洗漱,洗漱完毕等水烧做饭。烧水和洗漱两个事情的执行是不冲突,不需要相互等待的,这就是异步。比如js中的定时器,ajax请求都是异步操作。

图一

图一是js执行任务的过程,js是单线程的执行机制,所以维护的执行栈为左侧同步执行。至于右侧的异步操作,js维护了一个事件队列,队列里面的异步任务(比如定时器或ajax)在执行完毕后,通过注册回调函数的形式让主线程读取回调函数并执行。

我们看到主线程在和Event Queue(事件队列)之间是双向的,事件队列中有异步任务执行完毕就会通知主线程,主线程一旦有空闲就会去读取事件队列;所以他们是双向的。需要注意的是,只有主线程空闲下来后才会去读取事件队列。

这个过程是往复循环的。主线程从任务队列中读取事件,这个过程是循环不断的,所以整个过程的这种运行机制又称为Event Loop(事件循环)。

执行过程:
1)所有的同步任务都在主线程上执行,形成一个执行栈(比如有10console.log,它们需要按照顺序执行)
2)主线程之外,还存在一个事件队列异步任务的执行都会进入这个队列(可以认为主线程暂时不会管这些异步任务)。只要异步任务有了运行结果(比如,定时器到时,ajax请求反馈了结果),就在任务队列之中放置一个事件。
3)一旦执行栈中的所有同步任务执行完毕(主线程有了空闲)主线程就会读取事件队列,将可执行的任务放在主线程执行。事件队列是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。
4)主线程不断重复上面的第三步。
只要主线程空闲了(主线程执行栈没有任务了),就会去读取事件队列

 

所以,我们在使用js的过程中,发现总是同步代码先执行,异步代码的执行要靠后一些;比如下面代码:

setTimeout(() => {

console.log("时间到");

}, 0);

console.log("开始");

执行结果:开始 时间到

setTimeout是一个异步任务被放到异步的事件队列中,当主线程工作结束后才会去读取执行。

再比如:

setTimeout(() => {

console.log("时间到");

}, 0);

for (let i = 0; i < 1000000000; i++) {}

console.log("开始");

执行结果:开始 时间到

for循环也是主线程需要执行的代码,目前来看主线程有两个工作要做:1、for循环;2、console打印;只有这两个事情都完毕后才会去事件队列去读取setTimeout注册的回调函数执行;

此时也看到另外一个问题,定时器不是那么精准的。

四、宏任务和微任务

除了广义的同步任务和异步任务,我们对任务有更精细的定义:

· macro-task(宏任务)

· micro-task(微任务)

宏任务列表:

微任务列表:

上面类别分别列出了宏任务和微任务的情况,也列出了浏览器和node的对应情况(不熟悉node的同学可以略过,只看浏览器即可);

任务执行次序是这样的:

执行一个宏任务(先执行同步代码)-->执行所有微任务-->UI render-->执行下一个宏任务-->执行所有微任务-->UI render-->......

备注:

根据HTML 标准,一轮事件循环执行结束之后,下轮事件循环执行之前开始进行UI render。即:macro-task任务执行完毕,接着执行完所有的micro-task任务后,此时本轮循环结束,开始执行UI renderUI render完毕之后接着下一轮循环。但是UI render不一定会执行,因为需要考虑ui渲染消耗的性能有没有ui变动

五、案例分析

console.log("1");

 

setTimeout(function() {

console.log("2");

new Promise(function(resolve) {

console.log("4");

resolve();

}).then(function() {

console.log("5");

});

});

new Promise(function(resolve) {

console.log("7");

resolve();

}).then(function() {

console.log("8");

});

 

setTimeout(function() {

console.log("9");

new Promise(function(resolve) {

console.log("11");

resolve();

}).then(function() {

console.log("12");

});

});

1第一轮事件循环流程:

· 整体script作为第一个宏任务进入主线程,遇到console.log,输出1

· 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1

· 遇到Promisenew Promise直接执行,输出7then被分发到微任务Event Queue中。我们记为then1

· 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2

 

宏任务Event Queue

微任务Event Queue

setTimeout1

then1

setTimeout2


上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了17同时我们发现了then1个微任务执行then1,输出8

第一轮事件循环结束,这一轮输出178

2第二轮事件循环从setTimeout1宏任务开始:

· 首先输出2

· new Promise立即执行输出4

· then也分发到微任务Event Queue中,记为then2

宏任务Event Queue

微任务Event Queue

setTimeout2

then2

第二轮事件循环宏任务结束,我们then2个微任务可以执行输出5

第二轮事件循环结束,第二轮输出2435

3第三轮事件循环setTimeout2执行

· 直接输出9

· 直接执行new Promise,输出11

· then分发到微任务Event Queue中,记为then3

宏任务Event Queue

微任务Event Queue


then3

第三轮事件循环宏任务执行结束,执行微任务then3输出12

第三轮事件循环结束,第三轮输出91112

整段代码,共进行了三次事件循环,完整的输出为17824591112