07-JavaScript运行机制那些事儿

257 阅读9分钟

了解到vue更新dom的机制是异步的,就牵扯出了事件循环、任务队列、微任务、宏任务等,这几天开始查阅各种资料去学习这一块,把这些概念及JS的执行机制了解透彻了,遇到bug的时候,可以快速精准的定位问题,话不多说,一起去看看~

“JavaScript 是单线程、异步、非阻塞、解释型脚本语言。”

单线程与多线程

  • 单线程

JavaScript语言的一大特点就是:单线程,同一时间只能做一件事。往细了讲,就是JS的这个线程用来负责解释和执行JavaScript代码,也就是我们说的主线程。主线程上的代码是按顺序执行的。

  • 多线程

why?为什么JavaScript不设计成多线程的呢?大家一起齐心协力干活,这样不就缩短了执行的时间吗?

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。【摘自:阮一峰】

同步与异步

  • 同步

JavaScript是单线程的,主线程上的代码是自上而下按顺序执行的:

console.log('a')
console.log('b')
console.log('b')

大家都知道上面的代码会依次打印出:a、b、c

console.log('a')
$.ajax({
  url,
  async:false,// 同步请求
  success:function(){
    console.log('b') 
  }
})
console.log('b')

那如果其中某项任务是耗时比较长的ajax请求呢,这种时候如果依然使用同步请求,那么后面的任务就会被阻塞,浏览器就会处于暂时‘卡死’状态,对用户是非常不友好的。

  • 异步

然而JavaScript中代码执行的时候,大家发现定时器等并不会阻塞代码的执行,是因为主线程将遇到的计时器、DOM事件、ajax异步请求等直接交给了webapi,也就是浏览器提供的别的线程去处理了,主线程继续执行后面的任务,这样就实现了异步并且是非阻塞的。

异步一般包括:

  1. 定时器
  2. 事件绑定
  3. Ajax
  4. 回调函数
  5. ...

事件循环与任务队列

在讲述事件循环与任务队列前,需要讲述一下浏览器与JavaScript之间的关系:

JavaScript是单线程的,浏览器是多进程(大家自主去了解线程与进程的关系)。

  • 浏览器包括哪些进程

    • Browser进程
    • 第三方插件进程
    • GPU进程
    • 浏览器渲染进程
  • 浏览器渲染进程

    对于前端来说,最重要的是理解了浏览器的渲染机制,整体也就梳理清楚了。浏览器的渲染进程是多线程的,包括哪些线程呢:

    • GUI渲染线程

      • 负责渲染浏览器界面,解析HTML、CSS,构建DOM树和RenderObject树,布局与绘制等。
      • 当界面需要重绘(repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
      • GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时,GUI线程会被挂起,GUI更新会被保存在一个队列中,等到JS引擎空闲时立即被执行。
    • JS引擎线程

      • 成为JS内核,负责处理JavaScript脚本程序(我们所熟悉的引擎是chrom与nodejs中使用的v8引擎)
      • 这个引擎由两个部分组成,内存堆与调用栈
      • 内存堆:进行内存分配。如变量赋值
      • 调用栈:调用栈中按顺序执行主线程的代码,当调用栈为空时,JS引擎就会读取任务队列,看看任务队列中有哪些事件,对应的异步任务结束等待状态,进入执行栈,开始执行。
      • 只要主线程空了,就会读取‘任务队列’,这就是JavaScript的运行机制。
    • 事件触发线程

      • 归属于浏览器,而不是JS引擎,用来控制事件轮询
      • 当JS引擎执行代码块如鼠标点击等,会将对应的任务添加到事件触发线程中
      • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理的任务队列中,等待JS引擎的处理
    • 定时器触发线程

      • setTimeout与setInterval所在线程
      • 浏览器定时计数器并不是由JavaScript引擎计数的,是由定时触发线程来计数的,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行
      • HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。
      • 注意:setTimeout倒计时结束后,只是将事件插入到任务队列,并不会立马执行,必须等到主线程代码执行完,才会执行去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
    • 异步http请求线程

      • 在XMLHTTPRequest连接后,通过浏览器新开了一个线程请求
      • 将检测到状态变更时,如果设置有回调函数,异步线程会产生状态变更事件,将这个回调放入任务队列中,等待JS引擎执行
  • 任务队列

任务队列是需要排队的,前一个任务执行完了,才能执行下一个任务,这也就是JavaScript的单线程,前一个任务未执行结束,下一个任务就不会执行,需要一直等着。

所有的任务可以分为:同步任务与异步任务。同步任务就是在主线程上按顺序等待被执行;异步任务不会直接进入主线程,而是由栈中的代码调用各种外部API,由外部的API(由浏览器提供)去执行,将相应的事件(click、load、done)加入任务队列中。栈中的代码执行完毕后,主线程会去读取任务队列,依次执行那些事件所对应的回调函数。

  • 事件循环(Event Loop)

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

异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如 onclick, setTimeout, ajax 处理的方式都不同,这些异步操作是由浏览器内核的 webcore 来执行的,webcore 包含上图中的3种 webAPI,分别是 DOM Binding、network、timer模块。

​ 1. onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。

​ 2. setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。

​ 3. ajax 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。

任务队列是在事件循环之上的,事件循环每次 tick 后会查看 ES6 的任务队列中是否有任务要执行,也就是 ES6 的任务队列比事件循环中的任务(事件)队列优先级更高。如 Promise 就使用了 ES6 的任务队列特性。

Event Loop只是负责告诉你该执行那些任务,或者说哪些回调被触发了,真正的逻辑还是在进程中执行的。

微任务与宏任务

  • 微任务

microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。

注意:new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的。

  • 宏任务

(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:

注意:requestAnimationFrame姑且也算是宏任务吧,requestAnimationFrameMDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行 宏任务必然是在微任务之后执行的

  • async与await属于什么

async/await相当于是promise的语法糖,promise属于微任务,那么await等同于promise.then.

  • 示例
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
这个弄明白了,那么也基本分得清什么是微任务,什么是宏任务了。
可以在微任务与宏任务测试网站去操作

总结

JavaScript是单线程的,主线程会按顺序执行同步任务,遇到异步任务会调用外部的API,由外部的API去执行,当异步任务处于可执行的状态时,外部的API会将这些任务加入任务队列中,等待JS引擎执行。当栈中的代码执行完毕后,主线程会主动去读取任务队列,依次执行对应的回调函数,这时候会先查看是否有微任务可以执行,有微任务先执行微任务。

本人才疏浅薄,欢迎大家指正!

参考

阮一峰:JavaScript 运行机制详解:再谈Event Loop

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

了解javascript的运行机制(单线程、任务队列、EventLoop、微任务、宏任务)

微任务、宏任务与Event-Loop