从开始作前端到目前为止,陆续看了不少帖子讲JS运行机制,看过不久就忘了,仍是本身理一遍好些css
经过码字使本身对JS运行机制相关内容更加深入(本身用心写过的贴子,内容也会牢记于心)html
顺道给你们看看(我太难了,深夜码字,反复修改,说这么多就是想请你点个赞在看)前端
参考了不少资料(帖子),取其精华,去其糟糠,都在文末,可自行了解html5
是时候搞一波我大js了node
从零到一百再到一,从多方面了解JS的运行机制,体会更深入,请认真读下去git
本文大体分为如下这样的步骤来帮助咱们由广入深更加清晰的了解JS运行机制github
JS运行机制在日常前端面试时无论是笔试题仍是面试题命中率都极高web
说到JS运行机制,你知道多少面试
看到这你们可能回说:JS运行机制嘛,很简单,事件循环、宏微任务那点东西ajax
是的,做为一名前端咱们都了解,可是若是这真的面试问到了这个地方,你真的能够答好吗(灵魂一问🤔️)
无论你对JS了解多少,到这里你们不防先中止一下阅读,假设你目前在面试,面试官让你阐述下JS运行机制,思考下你的答案,用20秒的时间(面试时20s已经很长了),而后带着答案再接着往下看,有人曾经说过:没有思考的阅读纯粹是消磨时间罢了
,这话很好(由于是我说的,皮一下😄)
也有不少刚开始接触JS的同窗会被任务队列 执行栈 微任务 宏任务
这些高大上点的名次搞的很懵
接下来,咱们来细致的梳理一遍你就能够清晰的了解它们了
咱们都知道,CPU
是计算机的核心,承担全部的计算任务
官网说法,进程
是CPU
资源分配的最小单位
字面意思就是进行中的程序,我将它理解为一个能够独立运行且拥有本身的资源空间的任务程序
进程
包括运行中的程序和程序所使用到的内存和系统资源
CPU
能够有不少进程,咱们的电脑每打开一个软件就会产生一个或多个进程
,为何电脑运行的软件多就会卡,是由于CPU
给每一个进程
分配资源空间,可是一个CPU
一共就那么多资源,分出去越多,越卡,每一个进程
之间是相互独立的,CPU
在运行一个进程
时,其余的进程处于非运行状态,CPU
使用 时间片轮转调度算法 来实现同时运行多个进程
线程
是CPU
调度的最小单位
线程
是创建在进程
的基础上的一次程序运行单位,通俗点解释线程
就是程序中的一个执行流,一个进程
能够有多个线程
一个进程
中只有一个执行流称做单线程
,即程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行
一个进程
中有多个执行流称做多线程
,即在一个程序中能够同时运行多个不一样的线程
来执行不一样的任务, 也就是说容许单个程序建立多个并行执行的线程
来完成各自的任务
进程是操做系统分配资源的最小单位,线程是程序执行的最小单位
一个进程由一个或多个线程组成,线程能够理解为是一个进程中代码的不一样执行路线
进程之间相互独立,但同一进程下的各个线程间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)
调度和切换:线程上下文切换比进程上下文切换要快得多
多进程:多进程指的是在同一个时间里,同一个计算机系统中若是容许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的,好比你们能够在网易云听歌的同时打开编辑器敲代码,编辑器和网易云的进程之间不会相互干扰
多线程:多线程是指程序中包含多个执行流,即在一个程序中能够同时运行多个不一样的线程来执行不一样的任务,也就是说容许单个程序建立多个并行执行的线程来完成各自的任务
JS的单线程,与它的用途有关。做为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操做DOM。这决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?
还有人说js还有Worker线程,对的,为了利用多核CPU的计算能力,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程是完 全受主线程控制的,并且不得操做DOM
因此,这个标准并无改变JavaScript是单线程的本质
了解了进程和线程以后,接下来看看浏览器解析,浏览器之间也是有些许差距的,不过大体是差很少的,下文咱们皆用市场占有比例最大的Chrome为例
做为前端,免不了和浏览器打交道,浏览器是多进程的,拿Chrome来讲,咱们每打开一个Tab页就会产生一个进程,咱们使用Chrome打开不少标签页不关,电脑会愈来愈卡,不说其余,首先就很耗CPU
Browser进程
第三方插件进程
GPU进程
渲染进程(重)
咱们假设浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差
同理若是插件崩溃了也会影响整个浏览器
固然多进程还有其它的诸多优点,不过多阐述
浏览器进程有不少,每一个进程又有不少线程,都会占用内存
这也意味着内存等资源消耗会很大,有点拿空间换时间的意思
到此可不仅是为了让咱们理解为什么Chrome运行时间长了电脑会卡,哈哈,第一个重点来了
页面的渲染,JS的执行,事件的循环,都在渲染进程内执行,因此咱们要重点了解渲染进程
渲染进程是多线程的,咱们来看渲染进程的一些经常使用较为主要的线程
<script>
标签,就会中止GUI的渲染,而后js引擎线程开始工做,执行里面的js代码,等js执行完毕,js引擎线程中止工做,GUI继续渲染下面的内容。因此若是js执行时间太长就会形成页面卡顿的状况setInterval
与setTimeout
所在线程setTimeout
中低于4ms的时间间隔算为4ms了解了上面这些基础后,接下来咱们开始进入今天的正题
首先要知道,JS分为同步任务和异步任务
同步任务都在主线程(这里的主线程就是JS引擎线程)上执行,会造成一个执行栈
主线程以外,事件触发线程管理着一个任务队列
,只要异步任务有了运行结果,就在任务队列
之中放一个事件回调
一旦执行栈
中的全部同步任务执行完毕(也就是JS引擎线程空闲了),系统就会读取任务队列
,将可运行的异步任务(任务队列中的事件回调,只要任务队列中有事件回调,就说明能够执行)添加到执行栈中,开始执行
咱们来看一段简单的代码
let setTimeoutCallBack = function() { console.log('我是定时器回调'); }; let httpCallback = function() { console.log('我是http请求回调'); } // 同步任务 console.log('我是同步任务1'); // 异步定时任务 setTimeout(setTimeoutCallBack,1000); // 异步http请求任务 ajax.get('/info',httpCallback); // 同步任务 console.log('我是同步任务2'); 复制代码
上述代码执行过程
JS是按照顺序从上往下依次执行的,能够先理解为这段代码时的执行环境就是主线程,也就是也就是当前执行栈
首先,执行console.log('我是同步任务1')
接着,执行到setTimeout
时,会移交给定时器线程
,通知定时器线程
1s 后将 setTimeoutCallBack
这个回调交给事件触发线程
处理,在 1s 后事件触发线程
会收到 setTimeoutCallBack
这个回调并把它加入到事件触发线程
所管理的事件队列中等待执行
接着,执行http请求,会移交给异步http请求线程
发送网络请求,请求成功后将 httpCallback
这个回调交由事件触发线程处理,事件触发线程
收到 httpCallback
这个回调后把它加入到事件触发线程
所管理的事件队列中等待执行
再接着执行console.log('我是同步任务2')
至此主线程执行栈中执行完毕,JS引擎线程
已经空闲,开始向事件触发线程
发起询问,询问事件触发线程
的事件队列中是否有须要执行的回调函数,若是有将事件队列中的回调事件加入执行栈中,开始执行回调,若是事件队列中没有回调,JS引擎线程
会一直发起询问,直到有为止
到了这里咱们发现,浏览器上的全部线程的工做都很单一且独立,很是符合单一原则
定时触发线程只管理定时器且只关注定时不关心结果,定时结束就把回调扔给事件触发线程
异步http请求线程只管理http请求一样不关心结果,请求结束把回调扔给事件触发线程
事件触发线程只关心异步回调入事件队列
而咱们JS引擎线程只会执行执行栈中的事件,执行栈中的代码执行完毕,就会读取事件队列中的事件并添加到执行栈中继续执行,这样反反复复就是咱们所谓的事件循环(Event Loop)
图解
首先,执行栈开始顺序执行
判断是否为同步,异步则进入异步线程,最终事件回调给事件触发线程的任务队列等待执行,同步继续执行
执行栈空,询问任务队列中是否有事件回调
任务队列中有事件回调则把回调加入执行栈末尾继续从第一步开始执行
任务队列中没有事件回调则不停发起询问
在ECMAScript中,macrotask
也被称为task
咱们能够将每次执行栈执行的代码当作是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每个宏任务会从头至尾执行完毕,不会执行其余
因为JS引擎线程
和GUI渲染线程
是互斥的关系,浏览器为了可以使宏任务
和DOM任务
有序的进行,会在一个宏任务
执行结果后,在下一个宏任务
执行前,GUI渲染线程
开始工做,对页面进行渲染
宏任务 -> GUI渲染 -> 宏任务 -> ...
复制代码
常见的宏任务
ES6新引入了Promise标准,同时浏览器实现上多了一个microtask
微任务概念,在ECMAScript中,microtask
也被称为jobs
咱们已经知道宏任务
结束后,会执行渲染,而后执行下一个宏任务
, 而微任务能够理解成在当前宏任务
执行后当即执行的任务
当一个宏任务
执行完,会在渲染前,将执行期间所产生的全部微任务
都执行完
宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> ...
复制代码
常见微任务
看了上述宏任务微任务的解释你可能还不太清楚,不要紧,往下看,先记住那些常见的宏微任务便可
咱们经过几个例子来看,这几个例子思路来自掘金云中君
的文章参考连接【14】,经过渲染背景颜色来区分宏任务和微任务,很直观,我以为颇有意思,因此这里也用这种例子
找一个空白的页面,在console中输入如下代码
document.body.style = 'background:black'; document.body.style = 'background:red'; document.body.style = 'background:blue'; document.body.style = 'background:pink'; 复制代码
咱们看到上面动图背景直接渲染了粉红色,根据上文里讲浏览器会先执行完一个宏任务,再执行当前执行栈的全部微任务,而后移交GUI渲染,上面四行代码均属于同一次宏任务,所有执行完才会执行渲染,渲染时GUI线程
会将全部UI改动优化合并,因此视觉上,只会看到页面变成粉红色
再接着看
document.body.style = 'background:blue'; setTimeout(()=>{ document.body.style = 'background:black' },200) 复制代码
上述代码中,页面会先卡一下蓝色,再变成黑色背景,页面上写的是200毫秒,你们能够把它当成0毫秒,由于0毫秒的话因为浏览器渲染太快,录屏很差捕捉,我又没啥录屏慢放的工具,你们能够自行测试的,结果也是同样,最安全的方法是写一个index.html
文件,在这个文件中插入上面的js脚本,而后浏览器打开,谷歌下使用控制台中performance
功能查看一帧一帧的加载最为恰当,不过这样录屏很差录因此。。。
回归正题,之因此会卡一下蓝色,是由于以上代码属于两次宏任务
,第一次宏任务
执行的代码是将背景变成蓝色,而后触发渲染,将页面变成蓝色,再触发第二次宏任务将背景变成黑色
再来看
document.body.style = 'background:blue' console.log(1); Promise.resolve().then(()=>{ console.log(2); document.body.style = 'background:pink' }); console.log(3); 复制代码
控制台输出 1 3 2 , 是由于 promise 对象的 then 方法的回调函数是异步执行,因此 2 最后输出
页面的背景色直接变成粉色,没有通过蓝色的阶段,是由于,咱们在宏任务中将背景设置为蓝色,但在进行渲染前执行了微任务, 在微任务中将背景变成了粉色,而后才执行的渲染
setTimeout
是一个宏任务,它的事件回调在宏任务队列,Promise.then()
是一个微任务,它的事件回调在微任务队列,两者并非一个任务队列此时,你可能还很迷惑,不要紧,请接着往下看
首先执行一个宏任务,执行结束后判断是否存在微任务
有微任务先执行全部的微任务,再渲染,没有微任务则直接渲染
而后再接着执行下一个宏任务
首先,总体的script(做为第一个宏任务)开始执行的时候,会把全部代码分为同步任务
、异步任务
两部分
同步任务会直接进入主线程依次执行
异步任务会再分为宏任务和微任务
宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中
微任务也会进入到另外一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中
当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,若是有任务,就所有执行,若是没有就执行下一个宏任务
上述过程会不断重复,这就是Event Loop,比较完整的事件循环
new Promise(() => {}).then()
,咱们来看这样一个Promise代码
前面的 new Promise()
这一部分是一个构造函数,这是一个同步任务
后面的 .then()
才是一个异步微任务,这一点是很是重要的
new Promise((resolve) => { console.log(1) resolve() }).then(()=>{ console.log(2) }) console.log(3) 复制代码
上面代码输出1 3 2
async/await本质上仍是基于Promise的一些封装,而Promise是属于微任务的一种
因此在使用await关键字与Promise.then效果相似
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
的异步
首先给你们来一个比较直观的动图
之因此放这个动图,就是为了向你们推荐这篇好文,动图录屏自参考连接【1】
极力推荐你们看看这篇帖子,很是nice,分步动画生动且直观,有时间的话能够本身去体验下
不过在看这个帖子以前你要先了解下运行机制会更好读懂些
接下来这个来自网上随意找的一个比较简单的面试题,求输出结果
function test() { console.log(1) setTimeout(function () { // timer1 console.log(2) }, 1000) } test(); setTimeout(function () { // timer2 console.log(3) }) new Promise(function (resolve) { console.log(4) setTimeout(function () { // timer3 console.log(5) }, 100) resolve() }).then(function () { setTimeout(function () { // timer4 console.log(6) }, 0) console.log(7) }) console.log(8) 复制代码
结合咱们上述的JS运行机制再来看这道题就简单明了的多了
JS是顺序从上而下执行
执行到test(),test方法为同步,直接执行,console.log(1)
打印1
test方法中setTimeout为异步宏任务,回调咱们把它记作timer1放入宏任务队列
接着执行,test方法下面有一个setTimeout为异步宏任务,回调咱们把它记作timer2放入宏任务队列
接着执行promise,new Promise是同步任务,直接执行,打印4
new Promise里面的setTimeout是异步宏任务,回调咱们记作timer3放到宏任务队列
Promise.then是微任务,放到微任务队列
console.log(8)是同步任务,直接执行,打印8
主线程任务执行完毕,检查微任务队列中有Promise.then
开始执行微任务,发现有setTimeout是异步宏任务,记作timer4放到宏任务队列
微任务队列中的console.log(7)是同步任务,直接执行,打印7
微任务执行完毕,第一次循环结束
检查宏任务队列,里面有timer一、timer二、timer三、timer4,四个定时器宏任务,按照定时器延迟时间获得能够执行的顺序,即Event Queue:timer二、timer四、timer三、timer1,依次拿出放入执行栈末尾执行 (插播一条:浏览器 event loop 的 Macrotask queue,就是宏任务队列在每次循环中只会读取一个任务)
执行timer2,console.log(3)为同步任务,直接执行,打印3
检查没有微任务,第二次Event Loop结束
执行timer4,console.log(6)为同步任务,直接执行,打印6
检查没有微任务,第三次Event Loop结束
执行timer3,console.log(5)同步任务,直接执行,打印5
检查没有微任务,第四次Event Loop结束
执行timer1,console.log(2)同步任务,直接执行,打印2
检查没有微任务,也没有宏任务,第五次Event Loop结束
结果:1,4,8,7,3,6,5,2
上面的一切都是针对于浏览器的EventLoop
虽然NodeJS中的JavaScript运行环境也是V8,也是单线程,可是,仍是有一些与浏览器中的表现是不同的
其实nodejs与浏览器的区别,就是nodejs的宏任务分好几种类型,而这好几种又有不一样的任务队列,而不一样的任务队列又有顺序区别,而微任务是穿插在每一种宏任务之间的
在node环境下,process.nextTick的优先级高于Promise,能够简单理解为在宏任务结束后会先执行微任务队列中的nextTickQueue部分,而后才会执行微任务中的Promise部分
上图来自NodeJS官网
如上图所示,nodejs的宏任务分好几种类型,咱们只简单介绍大致内容了解,不详细解释,否则又是啰哩啰嗦一大篇
NodeJS的Event Loop相对比较麻烦
Node会先执行全部类型为 timers 的 MacroTask,而后执行全部的 MicroTask(NextTick例外)
进入 poll 阶段,执行几乎全部 MacroTask,而后执行全部的 MicroTask
再执行全部类型为 check 的 MacroTask,而后执行全部的 MicroTask
再执行全部类型为 close callbacks 的 MacroTask,而后执行全部的 MicroTask
至此,完成一个 Tick,回到 timers 阶段
……
如此反复,无穷无尽……
复制代码
反观浏览器中Event Loop就比较容易理解
先执行一个 MacroTask,而后执行全部的 MicroTask
再执行一个 MacroTask,而后执行全部的 MicroTask
……
如此反复,无穷无尽……
复制代码
好了,关于Node中各个类型阶段的解析,这里就不过多说明了,本身查阅资料吧,这里就是简单提一下,NodeJS的Event Loop解释起来比浏览器这繁杂,这里就只作个对比
上面的流程图都是本身画的,因此有点low,见谅
水平有限,欢迎指错
码字不易,看完对你有帮助请点赞,有疑问请评论提出
看完这篇帖子推荐看下 硬核JS」深刻了解异步解决方案 一文,会对JS异步编程理解更加深入
最近拾起了一个被冻结的公众号,又从新搞了下
欢迎你们关注【不正经的前端】,加我,加群,或者拿一些资料均可以的,时不时发一些优质原创
做者:isboyjc
邮箱:214930661@qq.com
GitHub: Github.com/isboyjc
参考