你是否遭受到这样的恐吓?
你是否有过每个表达式前面都console一遍值去找执行顺序?
看了很多js执行机制的文章似乎都是似懂非懂,到技术面问的时候,理不清思绪。总结了众多文章的例子和精华,希望能帮到你们
JavaScript 怎么执行的?
执行机制——事件循环(Event Loop)
通常所说的 JavaScript Engine
(JS引擎)负责执行一个个 chunk
(可以理解为事件块
)的程序,每个 chunk
通常是以 function
为单位,一个 chunk
执行完成后,才会执行下一个 chunk
。下一个 chunk
是什么呢?取决于当前 Event Loop Queue
(事件循环队列)中的队首。
通常听到的JavaScript Engine
和JavaScript runtime
是什么?
- Javascript Engine :Js引擎,负责解释并编译代码,让它变成能交给机器运行的代码(runnable commands)
- Javascript runtime :Js运行环境,主要提供一些对外调用的接口 。比如浏览器环境:
window
、DOM
。还有Node.js环境:require
、export
Event Loop Queue
(事件循环队列)中存放的都是消息,每个消息关联着一个函数,JavaScript Engine
(以下简称JS引擎)就按照队列中的消息顺序执行它们,也就是执行 chunk
。
例如
setTimeout( function() { console.log('timeout')}, 1000)复制代码
当JS引擎执行的时候,可以分为3步chunk
- 由
setTimeout
启动定时器(1000毫秒)执行 - 执行完毕后,得到机会将
callback
放入Event Loop Queue
- 此 callback 执行
每一步都是一个chunk
,可以发现,第2步,得到机会很重要,所以说即使延迟1000ms也不一定准的原因。因为如果有其他任务在前面,它至少要等其他消息对应的程序都完成后才能将callback
推入队列,后面我们会举个?
像这个一个一个执行chunk
的过程就叫做Event Loop(事件循环)
。
按照阮老师的说法:
总体角度:主线程执行的时候产生栈(stack)和堆(heap),栈中的代码负责调用各种API,在任务队列中加入事件(click,load,done),只要栈中的代码执行完毕后,就会去读取任务队列,依次执行那些事件所对应的回调函数。
执行的机制流程
同步直接进入主线程执行,如果是异步的,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
我们都知道,JS引擎 对 JavaScript
程序的执行是单线程的,为了防止同时去操作一个数据造成冲突或者是无法判断,但是 JavaScript Runtime
(整个运行环境)并不是单线程的;而且几乎所有的异步任务都是并发的,例如多个 Job Queue
、Ajax
、Timer
、I/O(Node)
等等。
而Node.js会略有不同,在node.js
启动时,创建了一个类似while(true)
的循环体,每次执行一次循环体称为一次tick
,每个tick
的过程就是查看是否有事件等待处理,如果有,则取出事件极其相关的回调函数并执行,然后执行下一次tick
。node的Event Loop
和浏览器有所不同。Event Loop
每次轮询:先执行完主代码,期中遇到异步代码会交给对应的队列,然后先执行完所有nextTick(),然后在执行其它所有微任务。
任务队列
任务队列task queue
中有微任务队列
和宏任务队列
- 微任务队列只有一个
- 宏任务可以有若干个
根据目前,我们先大概画个草图
具体部分后面会讲,那先说说同步和异步
执行机制——同步任务(synchronous)和异步任务(asynchronous)
事件分为同步和异步
同步任务
同步任务直接进入主线程进行执行
console.log('1');var sub = 0;for(var i = 0;i < 1000000000; i++) { sub++}console.log(sub);console.log('2');.....复制代码
会点编程的都知道,在打印出sub
的值之前,系统是不会打印出2
的。按照先进先出的顺序执行chunk。
如果是
function log(str) { console.log(str);}log('a');复制代码
从执行顺序上,首先log('a')
入栈,然后console.log('a')
再入栈,执行console.log('a')
出栈,log('a')
再出栈。
异步任务
异步任务必须指定回调函数,所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务进入Event Table
后,当指定的事情完成了,就将异步任务加入Event Queue
,等待主线程上的任务完成后,就执行Event Queue里的异步任务,也就是执行对应的回调函数。
指定的事情可以是setTimeout的time?
var value = 1;setTimeout(function(){ value = 2;}, 0)console.log(value); // 1复制代码
从这个例子很容易理解,即使设置时间再短,setTimeout
还是要等主线程执行完再执行,导致引用还是最初的value
值
?
console.log('task1');setTimeout(()=>{ console.log('task2') },0);var sub = 0;for(var i = 0;i < 1000000000;i++) { sub++}console.log(sub);console.log('task3');复制代码
分析一下
- task1进入主线程立即执行
- task2进入
Event Table
,注册完事件setTimeout
后进入Event Queue
,等待主线程执行完毕 - sub赋值后进入for循环自增,主线程一直被占用
- 计算完毕后打印出sub,主线程继续chunk
- task3进入主线程立即执行
- 主线程队列已清空,到Event Queue中执行任务,打印task2
不管for循环计算多久,只要主线程一直被占用,就不会执行Event Queue
队列里的任务。除非主线任务执行完毕。所有我们通常说的setTimeout
的time
是不标准的,准确的说,应该是大于等于这个time
var sub = 0;(function setTime(){ let start = (new Date()).valueOf();//开始时间 console.log('执行开始',start) setTimeout(()=>{ console.log('定时器结束',sub,(new Date()).valueOf()-start);//计算差异 },0);})();for(var i = 0;i < 1000000000;i++) { sub++}console.log('执行结束')复制代码
实际上,延迟会远远大于预期,达到了3004毫秒
最后的计算结果是根据浏览器的运行速度和电脑配置差异而定,这也是setTimeout
最容易被坑的一点。
AJAX怎么算
那ajax怎么算,作为日常使用最多的一种异步,我们必须搞清楚它的运行机制。
console.log('start');$.ajax({ url:'xxx.com?user=123', success:function(res){ console.log('success') }})setTimeout(() => { console.log('timeout')},100);console.log('end');复制代码
答案是不肯定的,可能是
startendtimeoutsuccess复制代码
也有可能是
startendsuccesstimeout复制代码
前两步没有疑问,都是作为同步函数执行,问题原因出在ajax身上
前面我们说过,异步任务必须有callback
,ajax的callback
是success()
,也就是只有当请求成功后,触发了对应的callback success()
才会被放入任务队列(Event Queue)等待主线程执行。而在请求结果返回的期间,后者的setTimeout
很有可能已经达到了指定的条件(执行100毫秒延时完毕
)将它的回调函数放入了任务队列等主线程执行。这时候可能ajax结果仍未返回...
Promise的执行机制
再加点料
console.log('执行开始');setTimeout(() => { console.log('timeout') }, 0);new Promise(function(resolve) { console.log('进入') resolve();}).then(res => console.log('Promise执行完毕') )console.log('执行结束');复制代码
先别继续往下看,假设你是浏览器,你会怎么运行,自我思考十秒钟
这里要注意,严格的来说,Promise 属于 Job Queue,只有then
才是异步。
Job Queue是什么
Job Queue是ES6新增的概念。
Job Queue和Event Loop Queue有什么区别?
- JavaScript runtime(JS运行环境)可以有多个Job Queue,但是只能有一个Event Loop Queue。
- JS引擎将当前chunk执行完会优先执行所有Job Queue,再去执行Event Loop Queue。
then
就是一种 Job Queue
。 分析流程:
- 遇到同步任务,进入主线程直接执行,打印出
"执行开始"
- 遇到
setTimeout
异步任务放入Event Table执行,满足条件后放入Event Queue的宏任务队列等待主线程执行 - 执行
Promise
,放入Job Queue
优先执行,执行同步任务打印出"进入"
- 返回
resolve()
触发then回调函数,放入Event Queue微任务队列等待主线程执行
- 执行同步任务打印出
"执行结束"
- 主线程清空,到
Event Queue
的微任务队列
取出任务开始执行。打印出"Promise执行完毕"
- 微任务队列清空,到宏任务队列取出任务执行,打印出
"timeout"
? plus
console.log("start");setTimeout(() => { console.log("setTimeout");}, 0);new Promise((resolve) => { resolve();}).then(() => { return console.log("A1");}).then(() => { return console.log("A2");});new Promise((resolve) => { resolve();}).then(() => { return console.log("B1");}).then(() => { return console.log("B2");}).then(() => { return console.log("B3");});console.log("end");复制代码
打印结果
运用刚刚说说的,分析一遍
- setTimeout异步任务,到Event Table执行完毕后将callback放入Event Queue宏任务队列等待主线程执行
- Promise 放入Job Queue优先进入主线程执行,返回
resolve()
,触发A1 then
回调函数放入微任务队列中等待主线程执行 - 到第二个Promise,同上,放入Job Queue执行,将
B1 then
回调函数放入微任务队列 - 执行同步函数,直接进入主线程执行,打印出
"end"
- 无同步任务,开始从task Queue 也就是 Event Queue里取出异步任务开始执行
- 首先取出队首的
A1 then()
回调函数开始执行,打印出"A1"
,返回promise
触发A2 then()
回调函数,添加到微任务队首。此时队首是B1 then()
- 从微任务队首取出
B1 then
回调函数,开始执行,返回promise触发B2 then()
回调函数,添加到微任务队首,此时队首是A2 then()
,再取出A2 then()
执行,这次没有回调 - 继续到微任务队首拿回调执行,重复轮询打印出
B2
和B3
。 - 微任务执行完毕,到宏任务队首取出
setTimeout
的回调函数放入主线程执行,打印出"setTimeout"
。
这样的话,Promise应该是搞懂了,但是微任务和宏任务?很多人对这个可能有点陌生,但是看完这个应该对这两者区别有所了解
异步任务分为宏任务和微任务
宏任务(macrotasks): setTimeout, setInterval, setImmediate(node.js), I/O, UI rendering
微任务(microtasks):process.nextTick(node.js), Promises, Object.observe, MutationObserver
先看一下具有特殊性的API:
process.nextTick
node方法,process.nextTick
可以把当前任务添加到执行栈的尾部,也就是在下一次Event Loop(主线程读取"任务队列")之前执行。也就是说,它指定的任务一定会发生在所有异步任务之前。和setTimeout(fn,0)
很像。
process.nextTick(callback)复制代码
setImmediate
Node.js0.8以前是没有setImmediate的,在当前"任务队列"的尾部添加事件,官方称setImmediate
指定的回调函数,类似于setTimeout(callback,0)
,会将事件放到下一个事件循环中,所以也会比nextTick
慢执行,有一点——需要了解setImmediate
和nextTick
的区别。nextTick
虽然异步执行,但是不会给其他io事件执行的任何机会,而setImmediate
是执行于下一个event loop
。总之process.nextTick()
的优先级高于setImmediate
setImmediate(callback)复制代码
MutationObserver
一定发生在setTimeout
之前,你可以把它看成是setImmediate
。MutationObserver
是一个构造器,接受一个callback
参数,用来处理节点变化的回调函数,返回两个参数
- mutations:节点变化记录列表(sequence<MutationRecord>)
- observer:构造MutationObserver对象。
var observe = new MutationObserver(function(mutations,observer){ // code...})复制代码
在这不说过多,可以去了解下
Object.observe
Object.observe方法用于为对象指定监视到属性修改时调用的回调函数
Object.observe(obj, function(changes){ changes.forEach(function(change) { console.log(change,change.oldValue); });});复制代码什么情况下才会触发?
- 原始JavaScript对象中的变化
- 当属性被添加、改变、或者删除时的变化
- 当数组中的元素被添加或者删除时的变化
- 对象的原型发生的变化
来个大?
总结:
任务优先级
同步任务
>>> process.nextTick
>>> 微任务(ajax/callback)
>>> setTimeout = 宏任务
??? setImmediate
setImmediate
是要等待下一次事件轮询,也就是本次结束后执行,所以需要画???
没有把Promise的Job Queue放进去是因为可以当成同步任务来进行处理。要明确的一点是,它是严格按照这个顺序去执行的,每次执行都会把以上的流程走一遍,都会再次轮询走一遍,然后把处理对应的规则。
拿个别人的?加点料,略微做一下修改,给大家分析一下
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') })}, 1000); //添加了1000msprocess.nextTick(function() { console.log('6');})new Promise(function(resolve) { console.log('7'); resolve();}).then(function() { console.log('8')})setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') })})setImmediate(function(){//添加setImmediate函数 console.log('13')})复制代码
第一遍Event Loop
- 走到
1
的时候,同步任务直接打印 - 遇到
setTimeout
,进入task 执行1000ms
延迟,此时未达到,不管它,继续往下走。 - 遇到
process.nextTick
,放入执行栈队尾(将于异步任务执行前执行)。 - 遇到
Promise
放入 Job Queue,JS引擎当前无chunk,直接进入主线程执行,打印出7
- 触发
resolve()
,将then 8
放入微任务队列等待主线程执行,继续往下走 - 遇到
setTimeout
,执行完毕,将setTimeout 9
的 callback 其放入宏任务队列 - 遇到
setImmediate
,将其callback放入Event Table,等待下一轮Event Loop执行
第一遍完毕 1
、7
当前队列
Number two Ready Go!
- 无同步任务,准备执行异步任务,JS引擎一看:"嘿!好家伙,还有个process",然后取出
process.nextTick
的回调函数执行,打印出6
- 再继续去微任务队首取出
then 8
,打印出8
。 - 微任务队列清空了,就到宏任务队列取出
setTimeout 9 callback
执行,打印出9
- 继续往下执行,又遇到
process.nextTick 10
,放入Event Queue等待执行 - 遇到
Promise
,将callback 放入 Job Queue,当前无chunk,执行打印出11
- 触发
resolve()
,添加回调函数then 12
,放入微任务队列
本次Event Loop还没有结束,同步任务执行完毕,目前任务队列
- 再取出
process.nextTick 10
,打印出10
- 去微任务队列,取出
then 12
执行,打印出12
- 本次Event Loop轮询结束 ,取出
setImmediate
打印出13
。
第二遍轮询完毕,打印出了 6
、8
、9
、11
、10
、12
、13
当前没有任务了,过了大概1000ms
,之前的setTimeout
延迟执行完毕了,放入宏任务
setTimeout
进入主线程开始执行。- 遇到同步任务,直接执行,打印出
2
- 遇到
process.nextTick
,callback放入Event Queue,等待同步任务执行完毕 - 遇到
Promise
,callback放入Job Queue,当前无chunk,进入主线程执行,打印出4
- 触发
resolve()
, 将then 5
放入微任务队列
同步执行完毕,先看下目前的队列
剩下的就很轻松了
- 取出
process.nextTick 3 callback
执行,打印出3
- 取出微任务
then 5
,打印出5
- over
总体打印顺序
17689111012132435复制代码
emmm...可能需要多看几遍消化一下。
Web Worker
现在有了Web Worker
,它是一个独立的线程,但是仍未改变原有的单线程,Web Worker
只是个额外的线程,有自己的内存空间(栈、堆)以及 Event Loop Queue
。要与这样的不同的线程通信,只能通过 postMessage
。一次 postMessage
就是在另一个线程的 Event Loop Queue
中加入一条消息。说到postMessage
可能有些人会联想到Service Work
,但是他们是两个截然不同
Web Worker和Service Worker的区别
Service Worker:
处理网络请求的后台服务。完美的离线情况下后台同步或推送通知的处理方案。不能直接与DOM交互。通信(页面和Service Worker之间)得通过postMessage
方法 ,有另一篇文章是关于本地储存,其中运用到页面离线访问Service Work of Google PWA, Web Worker:
模仿多线程,允许复杂的脚本在后台运行,所以它们不会阻止其他脚本的运行。是保持您的UI响应的同时也执行处理器密集型功能的完美解决方案。不能直接与DOM交互。通信必须通过postMessage
方法 如果意犹未尽可以尝试去深入Promise另一篇文章——