前端基本功(五):了解javascript的运行机制(单线程、任务队列、EventLoop、微任务、宏任务)

1. 单线程特色

  1. 单线程能够避免多线程操做带来的复杂的同步问题。
  2. HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此,这个新标准并无改变JavaScript单线程的本质。
  3. 咱们必须清楚一点,触发和执行并非同一律念,计时器的回调函数必定会在指定delay的时间后被触发,但并不必定当即执行,可能须要等待。全部JavaScript代码是在一个线程里执行的,像鼠标点击和计时器之类的事件只有在JS单线程空闲时才执行。

2. 任务队列

  1. 全部同步任务都在主线程上执行,造成一个执行栈。
  2. 主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。
  4. 只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。

3. Event Loop

JS 会建立一个相似于 while (true) 的循环,每执行一次循环体的过程称之为 Tick。每次 Tick 的过程就是查看是否有待处理事件,若是有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中,也就是每次 Tick 会查看任务队列中是否有须要执行的任务。javascript

event loop

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

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

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

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

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

4. javascript是单线程的,浏览器是多线程的。

浏览器的内核是多线程的,它们在内核控制下相互配合以保持同步,一个浏览器至少实现三个常驻线程:JavaScript引擎线程,GUI渲染线程,浏览器事件触发线程。数据库

  1. JavaScript引擎是基于事件驱动单线程执行的,JavaScript引擎一直等待着任务队列中任务的到来,而后加以处理,浏览器不管何时都只有一个JavaScript线程在运行JavaScript程序。
  2. GUI渲染线程负责渲染浏览器界面,当界面须要重绘(Repaint)或因为某种操做引起回流(Reflow)时,该线程就会执行。但须要注意,GUI渲染线程与JavaScript引擎是互斥的,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JavaScript引擎空闲时当即被执行。
  3. 事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JavaScript引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeout、也可来自浏览器内核的其余线程如鼠标点击、Ajax异步请求等,但因为JavaScript的单线程关系全部这些事件都得排队等待JavaScript引擎处理(当线程中没有执行任何同步代码的前提下才会执行异步代码)。

5. 多线程的优势和缺点分别是什么?

优势:一、将耗时较长的操做(网络请求、图片下载、音频下载、数据库访问等)放在子线程中执行,能够防止主线程的卡死;二、能够发挥多核处理的优点,提高cpu的使用率。 缺点:一、每开辟一个子线程就消耗必定的资源; 二、会形成代码的可读性变差;三、若是出现多个线程同时访问一个资源,会出现资源争夺的状况。promise

6. 浏览器的event loop至少包含两个队列,macrotask队列和microtask队列

  1. microtask 即微任务,是由js引擎分发的任务,老是添加到当前任务队列末尾执行。另外在处理microtask期间,若是有新添加的microtasks,也会被添加到队列的末尾并执行: Promise、MutaionObserver、process.nextTick(Node.js 环境)
  2. macrotask队列 等同于咱们常说的任务队列,macrotask是由宿主环境分发的异步任务,事件轮询的时候老是一个一个任务队列去查看执行的,"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取:script(总体代码)、setTimeout、setInterval、I/O、UI交互事件、setImmediate(Node.js 环境)
  3. 只要有微任务咱们确定是执行微任务的,当前进行的会执行,当前执行完的若是执行完后event loop仍是检测到微任务,仍是执行微任务,检测出没有微任务,咱们就执行宏任务队列中的任务。
  4. 经典面试题:
async function async1(){
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2(){
    console.log('async2')
}
console.log('script start')
setTimeout(function(){
    console.log('setTimeout') 
},0)  
async1();
new Promise(function(resolve){
    console.log('promise1')
    resolve();
}).then(function(){
    console.log('promise2')
})
console.log('script end')
/* 解题思路: 首先按照代码的执行顺序从上往下,js始终都是单线程的,先执行的确定是同步任务,再根据进入任务队列的顺序先进先出,先微后宏。 微任务是一次性将队列中存在的微任务执行完毕,宏任务是一个一个先进先出。 Promise是一个构造函数,调用的时候会生成Promise实例。当Promise的状态改变时会调用then函数中定义的回调函数。 咱们都知道这个回调函数不会马上执行,他是一个微任务会被添加到当前任务队列中的末尾,在下一轮任务开始执行以前执行。 async/await成对出现,async标记的函数会返回一个Promise对象,可使用then方法添加回调函数。await后面的语句会同步执行。但 await 下面的语句会被当成微任务添加到当前任务队列的末尾异步执行。 */

/* 答案: > node8版本: script start -> async1 start -> async2 -> promise1 -> script end -> promise2 -> async1 end -> setTimeout <= node8版本: script start -> async1 start -> async2 -> promise1 -> script end -> async1 end -> promise2 -> setTimeout 这主要是node.js8版本与其余版本的差别,他们对await的执行方法不一样 */
复制代码
相关文章
相关标签/搜索