js执行机制解析
前言
在继续探究之前先理解一下:
- 单线程
- 异步
- 事件循环(event loop)
- 栈、堆、队列
- V8 引擎
- 线程、进程
一、js为什么是单线程的?为什么需要异步?如何实现异步?
1、js为什么是单线程
这主要和js的用途有关,js作为浏览器的脚本语言,主要是实现用户和浏览器之间的交互,以及操作dom;这就决定了js只能是单线程的,试想一下,js被设计为多线程,一个线程需要修改这个dom,另一个线程又要删除这个dom,同时两个矛盾的命令,此时浏览器该怎么执行呢?所以为了避免这种复杂性,js从一诞生就是单线程
2、为什么需要异步
单线程就意味着所有的任务都要排队按序执行,前一个任务结束才会执行下一个任务,但是如果前一个任务执行耗时很长,那后面的任务就不得不处于等待状态;如果JS中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞,而呈现给用户的效果,阻塞就意味着"卡死",这样就导致了很差的用户体验
3、如何实现异步
通过事件循环 (event loop)
进程、线程
- 进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。
- 线程是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的。
二、浏览器环境下js引擎的事件循环机制(区别于node环境)
先看一个例子:
console.log(1)
setTimeout(function(){
console.log(2)
},0)
setTimeout(function(){
console.log(3)
},1000)
console.log(4)
执行结果:
// 1 4 2 3
// *****观察其执行顺序*******
可以发现打印2和3的任务是在打印4的任务后面执行的
Javascript 有一个主线程和调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。
JS 调用栈
JS 调用栈是一种后进先出的数据结构。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空
- a) 整体的script开始执行的时候,会把所有代码分为两部分:“同步任务”、“异步任务”
- b) 同步任务会直接进入主线程依次执行;
- c) 异步任务则会在异步有了结果后将注册的回调函数添加到 任务队列(消息队列) 中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。
- c) 异步任务则会在异步有了结果后将注册的回调函数添加到 任务队列(消息队列) 中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。
- d) 调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环。
任务队列
任务队列就是等候执行的一系列任务,只有主线程的调用栈(执行栈)的任务执行完清空后,事件循环机制才去任务队列拿任务执行
- 任务队列又分为macro-task(宏任务)和micro-task(微任务);
- 宏任务(按优先级顺序排列) 大概包括:script(整体代码),setTimeout,setInterval,setImmediate,I/O,UI rendering;
- 微任务(按优先级顺序排列) 大概包括:process.nextTick,Promise,Object.observe(已废弃),MutationObserver(html5新特性)
- setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
- 一个浏览器环境,只能有一个事件循环,而一个事件循环可以多个任务队列
- 一个事件循环可以有1个或多个宏任务队列,而仅有一个 微任务队列。
- 来自不同任务源的任务会进入到不同的任务队列
涉及了宏任务和微任务的概念之后再理一遍循环机制:
第一次事件循环中,JavaScript 引擎会把整个 script 代码当成一个宏任务执行,执行完成之后,再检测本次循环中是否存在微任务,存在的话就依次从微任务的任务队列中读取执行完所有的微任务,再读取宏任务的任务队列中的任务执行,再执行所有的微任务,如此循环。JS 的执行顺序就是每次事件循环中的宏任务-微任务。
盗一张图
三、实例
简单的理论描述了一下,还是要通过代码巩固一下
// 这里需要先注意下宏任务和微任务执行的优先级顺序
setTimeout(function(){
console.log(2);
},0);
new Promise(function(resolve){
console.log(3);
resolve();
console.log(4);
}).then(function(){
console.log(5);
});
console.log(6);
setTimeout(function(){
console.log(7);
},0);
console.log(8);
// 3 4 6 8 5 2 7
分析:
- 所有的script的代码被当做一个宏任务执行
- 碰到的同步任务依次进入主线程执行,碰到的异步任务按宏任务和微任务分别进入宏任务队列和微任务队列,此时按顺序打印出3,4,6,8
- 执行完成之后,再检查本次循环中是否存在微任务,存在的话去微任务队列读取执行所有的微任务,(Promise().then()是微任务),此时接着打印出5
- 微任务执行完,第一次循环结束;
- 从宏任务队列中取出第一个宏任务到主线程执行,打印2,再取出第二个宏任务执行,打印7
- 最终结果: 3 4 6 8 5 2 7
console.log('1');
setTimeout(function () {
console.log('2');
process.nextTick(function () {
console.log('3');
})
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {
console.log('5')
})
})
process.nextTick(function () {
console.log('6');
})
new Promise(function (resolve) {
console.log('7');
resolve();
}).then(function () {
console.log('8')
})
// 开启一个微任务
queueMicrotask(() => {
console.log("queueMicrotask1")
});
setTimeout(function () {
console.log('9');
process.nextTick(function () {
console.log('10');
})
new Promise(function (resolve) {
console.log('11');
resolve();
}).then(function () {
console.log('12')
})
})
// 结果
// 1 7 6 8 queueMicrotask1 2 4 3 5 9 11 10 12
- 所有代码当做一个宏任务执行, 输出同步结果 1 7 ,然后将异步任务按宏任务-微任务区分推入任务队列
- 执行任务队列所有的微任务 6 8 queueMicrotask1
- 此时微任务队列执行完,取一个宏任务执行(先进先出),输出同步结果 2 4,然后将这个宏任务中的微任务继续推人任务队列
- 接着继续判断任务队列是否含有微任务 继续执行 输出 3 5
- 微任务执行完,继续取一个宏任务执行 输出同步结果 9 11
- 接着继续判断任务队列是否含有微任务 继续执行 输出 10 12
async function async1 () {
console.log('1')
await async2();
console.log('2')
}
async function async2 () {
console.log('3')
}
console.log('4')
setTimeout(function () {
console.log('5')
}, 0)
async1();
new Promise (function (resolve) {
console.log('6')
resolve();
}).then (function () {
console.log('7')
})
console.log('8')
// 结果
// 4 1 3 6 8 2 7 5
注意
Async 函数中:
- await前的可以看做Promise外部的diamante
- await相当于在执行Promise中的为同步代码块
- await后的相当于在执行then的回调 微任务
四、node和浏览器下event loop的区别
解读文章:浏览器与Node的事件循环(Event Loop)有何区别?