探秘javascript执行机制

273 阅读8分钟

前言

之前的文章已经带着大家探究了不少关于vue,react,node等框架的知识,但是说归到底这些所谓的vue,react的框架也是基于javascript的,所以想要成为一个有理想的前端工程师必须也得对javascript理解得够透彻,现在就带大家一起来探究javascript的执行机制,解开它神秘的面纱!

执行 & 运行

首先我们需要声明下,JavaScript 的执行和运行是两个不同概念的,执行,一般依赖于环境,比如 node、浏览器、Ringo 等, JavaScript 在不同环境下的执行机制可能并不相同。而今天我们要讨论的就是 JavaScript 的一种执行,。而运行呢,是指JavaScript 的解析引擎。这是统一的。

JavaScript执行机制,重点有两点:

1.JavaScript是一门单线程语言。

2.Event Loop(事件循环)是JavaScript的执行机制。

既然说js是单线程,那就是在执行代码的时候是从上往下执行的,先来看一段代码:

 setTimeout(function(){
      console.log('定时器开始')
  });
  new Promise(function(resolve){
      console.log('Promise开始');
      resolve();
  }).then(function(){
      console.log('执行then函数')
  });
  console.log('代码执行结束');

输出结果:

从上面的代码执行可以很形象的概括出javascript的单线程,同步任务和异步任务以及事件循环的特点,要想知道javascript的执行机制必须理清楚这几个特点,项目咱们就来做一一介绍。

单线程

JavaScript作为一门浏览器脚本语言,当初研发之时就是为用户操作和浏览器DOM服务的,而多线程存在线程之间资源抢占,死锁,冲突等一系列问题。这就决定了JavaScript一定是单线程的,不然就存在着冲突问题。

因此所有浏览器的JS引擎(浏览器用于读取并执行JavaScript)在执行JavaScript只分配一个线程。虽然单线程不存在冲突的问题,但它同时也带来了一个新的问题--阻塞

很简单的例子,就想是汽车行驶在马路上,单线程只有一条路,如果前面的车抛锚了,那就意味着后面的车就得乖乖等着,等到前面的车换完轮胎,才能继续上路。

但我们都知道JavaScript是非阻塞的,为了解决上述的问题,JavaScript使用了异步和回调的方式来解决这种情况。

js单线程又是如何实现异步的呢?

js中的异步以及多线程都可以理解成为一种“假象”,就拿h5的WebWorker来说,子线程有诸多限制,不能控制DOM,不能修改全局对象等等,通常只用来做计算做数据处理。

这些限制并没有违背我们之前的观点,所以说是“假象”。JS异步的执行机制其实就是事件循环(eventloop),理解了eventloop机制,就理解了js异步的执行机制。

事件循环(eventloop)

我们通过网上比较流传广泛的一张图,来仔细看一下什么是EventLoop

右上角的虚线框中,代表了主线程运行时,产生的执行栈和存储object类型数据堆内存,我们的同步代码就存在执行栈中。而我们的异步代码则被分配给WebAPIs去执行,当其执行结束后,就会按照推入上文所说的任务队列中,并注册回调函数,供主线程调用。

当主线程处理完所有同步任务,这时执行栈就空了,主线程就会访问任务队列的头部,看看任务队列的头部有没有已经注册好的回调函数,如果有就把其放置执行栈,进行执行。执行完后执行栈又空了,就会继续前面的操作,如此循环往复,这就是JS的运行机制——EventLoop

任务队列

上面事件循环咱们提到了任务队列,我们这里理解为每一个语句就是一个任务

console.log(1);
console.log(2);

如上语句,其实就是就可以理解为两个 task。

而 queue 呢,就是FIFO的队列!

所以 Task Queue 就是承载任务的队列。而 JavaScript 的 Event Loop 就是会不断地过来找这个 queue,问有没有 task 可以运行运行。

任务又有同步任务和异步任务

同步任务(SyncTask)

当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。这个过程反复进行,直到执行栈中的代码全部执行完毕。

异步任务(AsyncTask)

js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码...如此反复

当然,除了同步任务,异步任务,还有宏任务和微任务的概念,在刚开始的学习中我将宏任务和微任务,归结于异步任务之下。但是我发现它并不是,因为肯定在任务队列中肯定是先执行宏任务,然后在宏任务结束的间隙内去执行微任务。

经过进一步的了解,我明白整块代码块进入执行栈的行为就是一个宏任务,因此宏任务微任务并不是异步任务的子集,而是除了同步异步之外,更宽泛的对执行栈任务的分类,而之所以会造成一开始的理解偏差,是因为恰好异步任务中含有宏任务和微任务。那么什么才是真正的宏任务和微任务呢?

宏任务和微任务

由宿主环境(对JS而已就是浏览器或者Node环境)提供的是宏任务,而由一些语言标准(ES6,node等)提供的是微任务。

1.宏任务 包含整个script代码块,setTimeout, setIntval等

2.微任务 Promise , process.nextTick等

在划分宏任务、微任务的时候并没有提到async/ await的本质就是Promise

我们来看一段代码理解宏任务和微任务的执行顺序:

console.log(1)

setTimeout(function(){
    console.log(2)
},0)

new Promise(function(resolve){
    resolve()
}).then(function(){
    console.log(3)
})

console.log(4)

按照逻辑,应该是1,4,2,3 但是运行后的输出是1,4,3,2 这时候我们应该明白,任务队列中也有优先级

1.代码块进入执行栈,首先明确这是一个宏任务

2.执行console.log(1),输出1

3.遇到setTimeout,是一个异步任务,且是一个宏任务,进入宏任务队列

4.遇到New Promise是同步任务,执行resolve,遇到Promise.then是异步任务,且是一个微任务,进入微任务队列。

5.执行console.log(4),输出4

6.第一个宏任务执行完毕,检查微任务队列中是否存在微任务,有的话全部执行。

7.执行Promise.then,输出3

8.微任务执行结束,检查宏任务队列,执行一个宏任务

9.执行setTimeout,输出2

10.检查微任务队列。。。检查宏任务队列。。。

请看下面这个流程图最能清晰表达他们的执行顺序:

谈了这么多,我们已经知道了javascrpt执行中涉及的几个概念,下面就来概括一下总体的执行机制:

第一轮事件循环:

1、主线程执行js整段代码(宏任务),将ajax、setTimeout、promise等回调函数注册到Event Queue,并区分宏任务和微任务。 2、主线程提取并执行Event Queue 中的ajax、promise等所有微任务,并注册微任务中的异步任务到Event Queue。

第二轮循环

1、主线程提取Event Queue 中的第一个宏任务(通常是setTimeout)。

2、主线程执行setTimeout宏任务,并注册setTimeout代码中的异步任务到Event Queue(如果有)。

3、执行Event Queue中的所有微任务,并注册微任务中的异步任务到Event Queue(如果有)。

到了第三轮就会去循环往复..

总结

不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。