我的博客html
看了不少js执行机制的文章彷佛都是似懂非懂,到技术面问的时候,理不清思绪。总结了众多文章的例子和精华,但愿能帮到大家node
一般所说的 JavaScript Engine
(JS引擎)负责执行一个个 chunk
(能够理解为事件块
)的程序,每一个 chunk
一般是以 function
为单位,一个 chunk
执行完成后,才会执行下一个 chunk
。下一个 chunk
是什么呢?取决于当前 Event Loop Queue
(事件循环队列)中的队首。ajax
一般听到的JavaScript Engine
和JavaScript runtime
是什么?编程
window
、 DOM
。还有Node.js环境:require
、export
Event Loop Queue
(事件循环队列)中存放的都是消息,每一个消息关联着一个函数,JavaScript Engine
(如下简称JS引擎)就按照队列中的消息顺序执行它们,也就是执行 chunk
。segmentfault
例如数组
setTimeout( function() {
console.log('timeout')
}, 1000)复制代码
当JS引擎执行的时候,能够分为3步chunkpromise
setTimeout
启动定时器(1000毫秒)执行callback
放入 Event Loop Queue
每一步都是一个chunk
,能够发现,第2步,获得机会很重要,因此说即便延迟1000ms也不必定准的缘由。由于若是有其余任务在前面,它至少要等其余消息对应的程序都完成后才能将callback
推入队列,后面咱们会举个🌰浏览器
像这个一个一个执行chunk
的过程就叫作Event Loop(事件循环)
。bash
按照阮老师的说法:网络
整体角度:主线程执行的时候产生栈(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
中有微任务队列
和宏任务队列
根据目前,咱们先大概画个草图
具体部分后面会讲,那先说说同步和异步
事件分为同步和异步
同步任务
同步任务直接进入主线程进行执行
console.log('1');
var sub = 0;
for(var i = 0;i < 1000000000; i++) {
sub++
}
console.log(sub);
console.log('2');
.....复制代码
会点编程的都知道,在打印出sub
的值以前,系统是不会打印出2
的。按照先进先出的顺序执行chunk。
若是是Execution Context Stack(执行上下文堆栈)
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');复制代码
分析一下
Event Table
,注册完事件setTimeout
后进入Event Queue
,等待主线程执行完毕无论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怎么算,做为平常使用最多的一种异步,咱们必须搞清楚它的运行机制。
console.log('start');
$.ajax({
url:'xxx.com?user=123',
success:function(res){
console.log('success')
}
})
setTimeout(() => {
console.log('timeout')
},100);
console.log('end');复制代码
答案是不愿定的,多是
start
end
timeout
success复制代码
也有多是
start
end
success
timeout复制代码
前两步没有疑问,都是做为同步函数执行,问题缘由出在ajax身上
前面咱们说过,异步任务必须有callback
,ajax的callback
是success()
,也就是只有当请求成功后,触发了对应的callback success()
才会被放入任务队列(Event Queue)等待主线程执行。而在请求结果返回的期间,后者的setTimeout
颇有可能已经达到了指定的条件(执行100毫秒延时完毕
)将它的回调函数放入了任务队列等主线程执行。这时候可能ajax结果仍未返回...
再加点料
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是ES6新增的概念。
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");
复制代码
打印结果
运用刚刚说说的,分析一遍
resolve()
,触发A1 then
回调函数放入微任务队列中等待主线程执行B1 then
回调函数放入微任务队列"end"
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:
node方法,process.nextTick
能够把当前任务添加到执行栈的尾部,也就是在下一次Event Loop(主线程读取"任务队列")以前执行。也就是说,它指定的任务必定会发生在全部异步任务以前。和setTimeout(fn,0)
很像。
process.nextTick(callback)
复制代码
Node.js0.8之前是没有setImmediate的,在当前"任务队列"的尾部添加事件,官方称setImmediate
指定的回调函数,相似于setTimeout(callback,0)
,会将事件放到下一个事件循环中,因此也会比nextTick
慢执行,有一点——须要了解setImmediate
和nextTick
的区别。nextTick
虽然异步执行,可是不会给其余io事件执行的任何机会,而setImmediate
是执行于下一个event loop
。总之process.nextTick()
的优先级高于setImmediate
setImmediate(callback)复制代码
必定发生在setTimeout
以前,你能够把它当作是setImmediate
。MutationObserver
是一个构造器,接受一个callback
参数,用来处理节点变化的回调函数,返回两个参数
var observe = new MutationObserver(function(mutations,observer){
// code...
})复制代码
在这不说过多,能够去了解下具体用法
Object.observe方法用于为对象指定监视到属性修改时调用的回调函数
Object.observe(obj, function(changes){
changes.forEach(function(change) {
console.log(change,change.oldValue);
});
});复制代码
什么状况下才会触发?
来个大🌰
任务优先级
同步任务
>>> 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); //添加了1000ms
process.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!
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
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
整体打印顺序
1
7
6
8
9
11
10
12
13
2
4
3
5复制代码
emmm...可能须要多看几遍消化一下。
如今有了Web Worker
,它是一个独立的线程,可是仍未改变原有的单线程,Web Worker
只是个额外的线程,有本身的内存空间(栈、堆)以及 Event Loop Queue
。要与这样的不一样的线程通讯,只能经过 postMessage
。一次 postMessage
就是在另外一个线程的 Event Loop Queue
中加入一条消息。说到postMessage
可能有些人会联想到Service Work
,可是他们是两个大相径庭
Service Worker:
处理网络请求的后台服务。完美的离线状况下后台同步或推送通知的处理方案。不能直接与DOM交互。通讯(页面和Service Worker之间)得经过postMessage
方法 ,有另外一篇文章是关于本地储存,其中运用到页面离线访问Service Work of Google PWA,有兴趣的能够看下
Web Worker:
模仿多线程,容许复杂的脚本在后台运行,因此它们不会阻止其余脚本的运行。是保持您的UI响应的同时也执行处理器密集型功能的完美解决方案。不能直接与DOM交互。通讯必须经过postMessage
方法
若是意犹未尽能够尝试去深刻Promise另外一篇文章——一次性让你懂async/await,解决回调地狱