JavaScript语言的特色是单线程,单线程只是指主线程,但不论是浏览器执行环境仍是node执行环境,除了主线程还有其余的线程,如:网络线程,定时器触发线程,事件触发线程等等,这些线程是如何与主线程协同工做的呢?node
这里不得不提一个任务队列的概念,js代码中全部代码分两种:同步任务、异步任务。ajax
全部同步任务都在主线程上执行,造成一个执行栈;数据库
主线程以外,还存在一个任务队列,只要异步任务有了运行结果,就在任务队列中放置一个事件;api
一旦执行栈中全部同步任务执行完毕,系统就会读取任务队列,那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。数组
主线程不断重复上一步。promise
浏览器和node中宏任务和微任务是不一样的,后面详细说明。下面先来了解宏任务和微任务的概念,宏任务和微任务都是任务队列里面的,能够想象成任务队列中其实有两列,宏任务是一列,微任务是一列。浏览器
首先咱们把任务队列里面的任务称为task,浏览器为了可以使得JS内部task与DOM任务可以有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行从新渲染 (task->渲染->task->...),宏任务就是上述的 任务队列里的任务,严格按照时间顺序压栈和执行。如 setTimeOut、setInverter等,下图为浏览器与node中的宏任务。bash
微任务一般来讲就是须要在当前 task 执行结束后当即执行的任务,好比对一系列动做作出反馈,或或者是须要异步的执行任务而又不须要分配一个新的 task,这样即可以减少一点性能的开销。只要执行栈中没有其余的js代码正在执行且每一个宏任务执行完,微任务队列会当即执行。若是在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,以后也会被执行。下图为浏览器与node中的微任务。网络
主线程从任务队列中读取事件,这个过程是循环不断的,这个运行机制被称为Event Loop(事件环)多线程
主线程运行的时候,产生堆和栈,heap就是堆,堆里面是存的是各类对象和函数,stack是栈,var a=1就存储在栈内;dom事件,ajax请求,定时器等异步操做的回调会被放到任务队列callback queue中,这个队列时先进先出的顺序,主线程执行完毕以后会依次执行callback queue中的任务,对应的异步任务就会结束等待状态,进入主线程被执行。
当stack执行栈空的时候,当即执行microtask checkpoint ,microtask checkpoint 会检查整个微任务队列。因此就会执行微任务队列中全部的任务,才会去执行第一个宏任务,执行完第一个宏任务后,又会去清空微任务队列。
具体支持分类以下: macro-task: setTimeout, setInterval, setImmediate, I/O, UI rendering,mesageChannel micro-task: Promises(这里指浏览器实现的原生 Promise),Object.observe, MutationObserver
咱们用下面一段代码来检验一下是否理解浏览器事件环:
setTimeout(function(){
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('then1');
})
},0)
Promise.resolve().then(()=>{
console.log('then2');
Promise.resolve().then(()=>{
console.log('then3');
})
setTimeout(function(){
console.log('setTimeout2')
},0)
})
复制代码
执行结果是then2 then3 setTimeout1 then1 setTimeout2
首先代码里面的setTimeout和Promise都是异步任务,js从上到下执行代码,分别将这两个异步任务放到了宏任务队列和微任务队列,执行栈此时为空先清空微任务队列,因此先输出了then2,而后在微任务队列中有添加一个then3的promise任务,在宏任务中添加了一个setTimeout2的定时器任务,因此接着执行下一个微任务,因此输出了then3,开始执行第一个宏任务,输出setTimeout1,而且在微任务队列又添加then1的promise任务,因此转去执行微任务,输出then1,再去执行一个宏任务,就是以前放进去的setTimeout2.
Node.js也是单线程的Event Loop,可是它的运行机制不一样于浏览器环境。
Node在进程启动时,便会建立一个相似于while(true)的循环,每执行一次循环体的过程被称为tick,中文翻译应该意为“滴答”,就像时钟同样,每滴答一下,就表示过去了1s。这个tick也有点这个意思,每循环一次,都表示本次tick结束,下次tick开始。每一个tick开始之初,都会检查是否有事件须要处理,若是有,就取出事件及关联的callbak函数,若是存在有关联的callback函数,就把事件的结果做为参数调用这个callback函数执行。若是不在有事件处理,就退出进程。
那么在每一个tick的过程当中,如何判断是否有事件须要处理,先要引入一个概念,叫作“观察者”(watcher)。每个事件循环都有一个或者多个观察者,判断是否有事件要处理的过程就是向这些观察者询问是否有须要处理的事件
Node的观察者有这样几种:
定时器观察者:setTimeout,setInterval
idle观察者:顾名思义,就是早已等在那里的观察者,之后会说到的process.nextTick就属于这类
I/O观察者:顾名思义,就是I/O相关观察者,也就是I/O的回调事件,如网络,文件,数据库I/O等
check观察者:顾名思义,就是须要检查的观察者,后面会说到的setImmediate就属于这类
事件循环是一个典型的生产者/消费者模型。异步I/O,网络请求,setTimeout等都是典型的事件生产者,源源不断的为Node提供不一样类型的事件,这些事件被传到对应的观察者那里,事件循环在每次tick时则从观察者那里取出事件并处理。
咱们如今知道,JavaScript的异步I/O调用过程当中,回调函数并不禁咱们开发者调用,事实上,在JavaScript发起调用到内核执行完I/O操做的过程当中,存在一种中间产物,它叫作请求对象。这个请求对象会从新封装回调函数及参数,并作一些其余的处理。这个请求对象,会在异步事件完成时被调用,取出回调函数和参数,并传入执行结果进行回调。
组装好请求对象,送入I/O线程池等待执行,实际上只是完成了异步I/O的第一步;第二步则是异步I/O被线程池处理结束后的回调,也就是执行回调。
不一样类型的观察者,处理的优先级不一样,idle观察者最早,I/O观察者其次,check观察者最后。
setTimeout()和setInterval()分别用于单次和屡次运行任务,其建立的定时器会被插入到定时器观察者内部的一个红黑树中。每次Tick运行时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,若超过则造成一个事件,其回调函数立刻运行。
执行机制:
一、初始化Event loop;
二、执行主代码,遇到异步处理,就分配给对应的队列,直到主代码执行完毕;
三、主代码中遇到全部的微任务,先去执行全部的nextTick(),而后执行其余的微任务,就是nextTick()在微任务里面等级最高;
四、开始Event loop,就是上面的各个观察者按顺序检查;
五、每次执行完毕一个观察者队列,转下一个观察者以前,会清空微任务队列;
六、timer阶段的定时器是不许的,在超过规定时间后,一旦获得执行机会就当即执行。
promise的then是微任务,process.nextTick()也是微任务,执行顺序是nextTick大于then
Promise.resolve().then(()=>{
console.log('then');
})
process.nextTick(()=>{
console.log('nextTick');
})
复制代码
上面代码先输出nextTick,后输出then 咱们能够利用process.nextTick是异步任务,而且执行快的特色实现一些巧妙的解决办法。
class A{
constructor(){
this.arr=[];
process.nextTick(()=>{
console.log(this.arr);
})
}
add(val){
this.arr.push(val);
}
}
let a=new A();
a.add('123');
a.add('456');
复制代码
假如咱们这里没有加process.nextTick的时候,这里打印出来的空数组,由于new实例的时候,就执行了constructor了,可是加了这个process.nextTick后,里面的代码会等同步代码先执行完毕后再执行,这是就已经拿到了数据。打印出['123','456']。
setTimeout(()=>{
console.log('timeout1');
process.nextTick(()=>{
console.log('nextTick');
})
},1000)
setTimeout(()=>{
console.log('timeout2')
},1000)
复制代码
输出:timeout1 timeout2 nextTick 先清空时间队列,去执行下一个队列以前,先去清空微任务队列,也就是idle队列,因此顺序是这样的
setTimeout(()=>{
console.log('timeout1');
process.nextTick(()=>{
console.log('nextTick1');
})
},1000)
process.nextTick(()=>{
setTimeout(()=>{
console.log('timeout2')
},1000)
console.log('nextTick2');
})
复制代码
上面代码的执行顺序是不固定的,有时候
nextTick2 timeout1 nextTick1 timeout2
nextTick2 timeout1 timeout2 nextTick1
timer阶段的定时器是不许的,他是在超过规定时间后,一旦获得执行机会就当即执行。
上面代码,先走idle队列,先输出nextTick2是固定的,这时候定时器队列中放了两个定时器了。确定是限制性timeout1,由于他是先放进去的,可是第一个定时器执行完毕后,第二个定时器不必定到结束时间,因此就会去执行idle队列,输出nextTick1,以后再执行timeout2。
第一个定时器是1000毫秒,可是第二个定时器的结束时间多是1000.8ms,由于process。nextTick也须要执行时间。第一个定时器执行完以后,可能还没到1000.8ms,因此他就去清空了idle任务队列,若是第一个定时器执行完毕后,已经到了1000.8ms,那么确定先执行第二个定时器。
因此定时器的时间在底层实现的时候是不同的。
又一个例子
setImmediate(()=>{
console.log('setImmediate');
})
setTimeout(()=>{
console.log('setTimeout');
},0); //规范是4ms,这里规定的时间0,在底层实现的时候不是0ms
复制代码
输出:谁均可能先输出
咱们知道setImmediate是check检查队列中的,node执行栈执行时间若是是5ms,那么走到时间队列的时候,定时器时间就已经到了,因此先执行setTimeout,再执行setImmediate,可是也有可能node执行栈中代码执行了2ms,没到4ms,就会先走setImmediate,再走时间队列。
let fs=require('fs');
fs.readFile('./1.txt',function(){
setImmediate(()=>{
console.log('setImmediate');
})
setTimeout(()=>{
console.log('setTimeout');
},0);
})
复制代码
文件读取会走poll轮询阶段,获得回调信息后,下一阶段是check阶段,因此setImmediate永远先走。执行结果顺序永远同样
最后一个小测试
let fs=require('fs');
setImmediate(()=>{
Promise.resolve().then(()=>{
console.log('then1');
})
},0)
Promise.resolve().then(()=>{
console.log('then2');
})
fs.readFile('./1.txt',function(){
process.nextTick(()=>{
console.log('nextTick');
})
setImmediate(()=>{
console.log('setImmediate');
})
})
复制代码
答案在下面哦~
then2 then1 nextTick setImmediate
第一次确定是执行微任务输出then2,而后走poll阶段文件读取,文件读取不是马上执行回调函数的,由于异步任务须要时间等待读取结果,执行栈也不是在等着他执行完毕的,直接执行check阶段,执行setImmediate的回调函数,里面遇到了微任务,如今微任务队列被添加进去一个,在执行fs的回调以前,清空微任务队列,因此输出then1,接着执行fs的回调,添加进去nextTick微任务,check阶段的setImmediate,走完poll阶段,确定要去清空微任务队列,输出nextTick,再走check阶段,输出setImmediate。