这一次,彻底弄懂 JavaScript 执行机制

因为javascript是一门单线程语言,javascript是按照语句出现的顺序执行的


前言

正因为js是一行一行执行的,所以我们以为js都是这样的:

1
2
3
4
5
6
let a = '1';
console.log(a);

let b = '2';
console.log(b);

然而实际上js是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setTimeout(function(){
console.log('定时器开始啦')
});

new Promise(function(resolve){
console.log('马上执行for循环啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('执行then函数啦')
});

console.log('代码执行结束');

依照js是按照语句出现的顺序执行这个理念,自信的写下输出结果:

1
2
3
4
//"定时器开始啦"
//"马上执行for循环啦"
//"执行then函数啦"
//"代码执行结束"

去chrome上验证下,结果完全不对,瞬间懵了,说好的一行一行执行的呢?

我们真的要彻底弄明白javascript的执行机制了。

关于javascript

javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。所以一切javascript版的”多线程”都是用单线程模拟出来的,一切javascript多线程都是纸老虎!

javascript事件循环

既然js是单线程,那就像只有一个窗口的银行,客户需要排队一个一个办理业务,同理js任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此聪明的程序员将任务分为两类:

同步任务
异步任务
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

如图

导图要表达的内容用文字来表述的话:

  • 同步和异步任务分别进入不同的执行”场所”,同步的进入主线程,异步的进入Event Table并注册函数。
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

我们不禁要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

说了这么多文字,不如直接一段代码更直白:

1
2
3
4
5
6
7
8
9
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');

上面是一段简易的ajax请求代码:

  • ajax进入Event Table,注册回调函数success。
  • 执行console.log(‘代码执行结束’)。
  • ajax事件完成,回调函数success进入Event Queue。
  • 主线程从Event Queue读取回调函数success并执行。

事件循环Event Loop是js实现异步的一种方法,也是js的执行机制

首先要知道,JS分为同步任务和异步任务
同步任务都在主线程(这里的主线程就是JS引擎线程)上执行,会形成一个执行栈
主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放一个事件回调
一旦执行栈中的所有同步任务执行完毕(也就是JS引擎线程空闲了),系统就会读取任务队列,将可运行的异步任务(任务队列中的事件回调,只要任务队列中有事件回调,就说明可以执行)添加到执行栈中,开始执行

总结

javascript是一门单线程语言
Event Loop是javascript的执行机制

宏任务(macrotask) & 微任务(microtask)

宏任务(macrotask)

在ECMAScript中,macrotask也被称为task

我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他

由于JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染

1
宏任务 -> GUI渲染 -> 宏任务 -> ...

常见的宏任务

  • 主代码块
  • setTimeout
  • setInterval
  • setImmediate ()-Node
  • requestAnimationFrame ()-浏览器

微任务(microtask)
ES6新引入了Promise标准,同时浏览器实现上多了一个microtask微任务概念,在ECMAScript中,microtask也被称为jobs

我们已经知道宏任务结束后,会执行渲染,然后执行下一个宏任务, 而微任务可以理解成在当前宏任务执行后立即执行的任务

当一个宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完

1
宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> ...

常见微任务

  • process.nextTick ()-Node
  • Promise.then()
  • catch
  • finally
  • Object.observe
  • MutationObserver

微任务宏任务注意点

  • 浏览器会先执行一个宏任务,紧接着执行当前执行栈产生的微任务,再进行渲染,然后再执行下一个宏任务

  • 微任务和宏任务不在一个任务队列,不在一个任务队列

  1. 例如setTimeout是一个宏任务,它的事件回调在宏任务队列,Promise.then()是一个微任务,它的事件回调在微任务队列,二者并不是一个任务队列
  2. 以Chrome 为例,有关渲染的都是在渲染进程中执行,渲染进程中的任务(DOM树构建,js解析…等等)需要主线程执行的任务都会在主线程中执行,而浏览器维护了一套事件循环机制,主线程上的任务都会放到消息队列中执行,主线程会循环消息队列,并从头部取出任务进行执行,如果执行过程中产生其他任务需要主线程执行的,渲染进程中的其他线程会把该任务塞入到消息队列的尾部,消息队列中的任务都是宏任务
  3. 微任务是如何产生的呢?当执行到script脚本的时候,js引擎会为全局创建一个执行上下文,在该执行上下文中维护了一个微任务队列,当遇到微任务,就会把微任务回调放在微队列中,当所有的js代码执行完毕,在退出全局上下文之前引擎会去检查该队列,有回调就执行,没有就退出执行上下文,这也就是为什么微任务要早于宏任务,也是大家常说的,每个宏任务都有一个微任务队列(由于定时器是浏览器的API,所以定时器是宏任务,在js中遇到定时器会也是放入到浏览器的队列中)

图解宏任务和微任务

如图
首先执行一个宏任务,执行结束后判断是否存在微任务

有微任务先执行所有的微任务,再渲染,没有微任务则直接渲染

然后再接着执行下一个宏任务

图解完整的Event Loop

如图
首先,整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为同步任务、异步任务两部分

同步任务会直接进入主线程依次执行

异步任务会再分为宏任务和微任务

宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中

微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中

当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务

上述过程会不断重复,这就是Event Loop,比较完整的事件循环

关于Promise

new Promise(() => {}).then() ,我们来看这样一个Promise代码
前面的new Promise() 这一部分是一个构造函数,这是一个同步任务
后面的 .then() 才是一个异步微任务
,这一点是非常重要的

1
2
3
4
5
6
7
8
new Promise((resolve) => {
console.log(1)
resolve()
}).then(()=>{
console.log(2)
})
console.log(3)
上面代码输出1 3 2

关于 async/await 函数

async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种
所以在使用await关键字与Promise.then效果类似

1
2
3
4
5
6
7
8
9
setTimeout(() => console.log(4))

async function test() {
console.log(1)
await Promise.resolve()
console.log(3)
}
test()
console.log(2)

上述代码输出1 2 3 4
可以理解为,await 以前的代码,相当于与 new Promise 的同步代码,await 以后的代码相当于 Promise.then的异步

参考文章

「硬核JS」一次搞懂JS运行机制

JavaScript运行机制

这一次,彻底弄懂 JavaScript 执行机制