JavaScript程序采用了异步事件驱动编程(Event-driven programming)模型,维基百科对它的解释是:javascript
事件驱动程序设计(英语:Event-driven programming)是一种电脑程序设计模型。这种模型的程序运行流程是由用户的动做(如鼠标的按键,键盘的按键动做)或者是由其余程序的消息来决定的。相对于批处理程序设计(batch programming)而言,程序运行的流程是由程序员来决定。批量的程序设计在初级程序设计教学课程上是一种方式。然而,事件驱动程序设计这种设计模型是在交互程序(Interactive program)的状况下孕育而生的html
简而言之,在web前端编程里面JavaScript经过浏览器提供的事件模型API和用户交互,接受用户的输入。前端
事件驱动程序模型基本的实现原理基本上都是使用 事件循环(Event Loop)。html5
而JS的运行环境主要有两个:浏览器、Node。java
在两个环境下的Event Loop实现是不同的,在浏览器中基于 规范 来实现,不一样浏览器可能有小小区别。在Node中基于 libuv 这个库来实现node
JS是单线程执行的,而基于事件循环模型,造成了基本没有阻塞(除了alert或同步XHR等操做)的状态。程序员
先看HTML标准的一系列解释:web
为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。 有两类事件循环:一种针对浏览上下文(browsing context),还有一种针对worker(web worker)。编程
为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)vim
上图中,主线程运行的时候,产生堆栈,栈中的代码调用各类外部API,异步操做执行完成后,就在消息队列中排队。只要栈中的代码执行完毕,主线程就会去读取“任务队列”,依次执行那些事件所对应的回调函数。
下面看一个有意思的例子,猜一下它的运行结果:
setTimeout(
function(){
console.log('1')
},0);
new Promise(
function(resolve){
console.log('2');
resolve()
}).then(
function(){
console.log('3');
});
console.log('4');
复制代码
打印结果:
2
4
3
1
复制代码
这是为何?是否是跟上面说的相违背了?其实这里面就有了两个概念宏任务(task/macrotask),微任务(microtask),下面咱们来详细介绍一下这两个东东。
根据 规范,每一个线程都有一个事件循环(Event Loop),在浏览器中除了主要的页面执行线程 外,Web worker是在一个新的线程中运行的,因此能够将其独立看待。
每一个事件循环有至少一个任务队列(Task Queue,也能够称做Macrotask宏任务),各个任务队列中放置着不一样来源(或者不一样分类)的任务,可让浏览器根据本身的实现来进行优先级排序
以及一个微任务队列(Microtask Queue),主要用于处理一些状态的改变,UI渲染工做以前的一些必要操做(能够防止屡次无心义的UI渲染)
主线程的代码执行时,会将执行程序置入执行栈(Stack)中,执行完毕后出栈,另外有个堆空间(Heap),主要用于存储对象及一些非结构化的数据。
常见的macrotask有:
run <script>(同步的代码执行)
setTimeout
setInterval
setImmediate (Node环境中)
requestAnimationFrame
I/O
UI rendering
复制代码
常见的microtask有:
process.nextTick (Node环境中)
Promise callback
Object.observe (基本上已经废弃)
MutationObserver
复制代码
一、执行宏任务(先进先出),一次循环只执行一个宏任务)
二、执行栈 —— 同步方法顺序执行,异步方法交给异步处理模块
三、执行栈为空时取出微任务执行(先进先出),直到微任务队列为空
四、更新UI渲染。完成一轮循环,反复执行1-4。(不必定每次循环都会渲染)
复制代码
在一轮event loop中屡次修改同一dom,只有最后一次会进行绘制。
渲染更新(Update the rendering)会在event loop中的tasks和microtasks完成后进行,但并非每轮event loop都会更新渲染,浏览器有本身的机制来肯定是否要更新渲染。若是在一帧(16.7ms)里屡次修改了dom,浏览器可能只会渲染绘制一次。
若是但愿在每轮event loop都即时呈现变更,可使用requestAnimationFrame.
复制代码
那么咱们回到上面的那个例子就不难解释了:
==注意==: Promise 自身的代码是同步执行的,只有 .then后的回调函数才是微任务。
主线程的执行过程:
在Node环境中,macrotask部分主要多了setImmediate,microtask部分主要多了process.nextTick,而这个nextTick是独立出来自成队列的,优先级高于其余microtask
不过事件循环的的实现就不太同样了,能够参考 Node事件文档 libuv事件文档
每一轮事件循环都会通过六个阶段,在每一个阶段后,都会执行microtask
比较特殊的是在poll阶段,执行程序同步执行poll队列里的回调,直到队列为空或执行的回调达到系统上限
接下来再检查有无预设的setImmediate,若是有就转入check阶段,没有就先查询最近的timer的距离,以其做为poll阶段的阻塞时间,若是timer队列是空的,它就一直阻塞下去
而nextTick并不在这些阶段中执行,它在每一个阶段以后都会执行。
一个简单的例子:
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
console.log(5);
复制代码
根据以上知识,应该很快就能知道输出结果是 5 3 4 1 2
修改一下:
process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => {
process.nextTick(() => console.log(0));
console.log(4);
});
复制代码
输出为 1 3 2 4 0,由于nextTick队列优先级高于同一轮事件循环中其余microtask队列
再次修改:
process.nextTick(() => console.log(1));
console.log(0);
setTimeout(()=> {
console.log('timer1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
process.nextTick(() => console.log(2));
setTimeout(()=> {
console.log('timer2');
process.nextTick(() => console.log(3));
Promise.resolve().then(() => {
console.log('promise2');
});
}, 0);
复制代码
输出结果为:
0
1
2
timer1
timer2
3
promise1
promise2
复制代码
与在浏览器中不一样,这里promise1并非在timer1以后输出,由于在setTimeout执行的时候是出于timer阶段,会先一并处理timer回调.
知道JS的事件循环是怎么样的了,就须要知道怎么才能把它用好:
在microtask中不要放置复杂的处理程序,防止阻塞UI的渲染
可使用process.nextTick处理一些比较紧急的事情
能够在setTimeout回调中处理上轮事件循环中UI渲染的结果
注意不要滥用setInterval和setTimeout,它们并非能够保证可以按时处理的,setInterval甚至还会出现丢帧的状况,可考虑使用 requestAnimationFrame
一些可能会影响到UI的异步操做,可放在promise回调中处理,防止多一轮事件循环致使重复执行UI的渲染
在Node中使用immediate来可能会获得更多的保证
若有错误欢迎指正,相互进步。
参考连接:
JavaScript 运行机制详解:再谈Event Loop