JavaScript 是一门单线程语言,之因此说是单线程,是由于在浏览器中,若是是多线程,而且两个线程同时操做了同一个 Dom 元素,那最后的结果会出现问题。因此,JavaScript 是单线程的,可是若是彻底由上至下的一行一行执行代码,假如一个代码块执行了很长的时间,后面必需要等待当前执行完毕,这样的效率是很是低的,因此有了异步的概念,确切的说,JavaScript 的主线程是单线程的,可是也有其余的线程去帮咱们实现异步操做,好比定时器线程、事件线程、Ajax 线程。git
在浏览器中执行 JavaScript 有两个区域,一个是咱们平时所说的同步代码执行,是在栈中执行,原则是先进后出,而在执行异步代码的时候分为两个队列,macro-task
(宏任务)和 micro-task
(微任务),遵循先进先出的原则。浏览器
// 做用域链 function one() { console.log(1); function two() { console.log(2); function three() { console.log(3); } three(); } two(); } one(); // 1 // 2 // 3
上面的代码都是同步的代码,在执行的时候先将全局做用域放入栈中,执行全局做用域中的代码,解析了函数 one
,当执行函数调用 one()
的时候将 one
的做用域放入栈中,执行 one
中的代码,打印了 1
,解析了 two
,执行 two()
,将 two
放入栈中,执行 two
,打印了 2
,解析了 three
,执行了 three()
,将 three
放入栈中,执行 three
,打印了 3
。多线程
在函数执行完释放的过程当中,由于全局做用域中有 one
正在执行,one
中有 two
正在执行,two
中有 three
正在执行,因此释放内存时必须由内层向外层释放,three
执行后释放,此时 three
再也不占用 two
的执行环境,将 two
释放,two
再也不占用 one
的执行环境,将 one
释放,one
再也不占用全局做用域的执行环境,最后释放全局做用域,这就是在栈中执行同步代码时的先进后出原则,更像是一个杯子,先放进去的在最下面,须要最后取出。异步
而异步队列更像时一个管道,有两个口,从入口进,从出口出,因此是先进先出,在宏任务队列中表明的有 setTimeout
、setInterval
、setImmediate
、MessageChannel
,微任务的表明为 Promise 的 then
方法、MutationObserve
(已废弃)。函数
案例 1post
let messageChannel = new MessageChannel(); let prot2 = messageChannel.port2; messageChannel.port1.postMessage("I love you"); console.log(1); prot2.onmessage = function(e) { console.log(e.data); }; console.log(2); // 1 // 2 // I love you
从上面案例中能够看出,MessageChannel
是宏任务,晚于同步代码执行。ui
案例 2spa
setTimeout(() => console.log(1), 2000); setTimeout(() => console.log(2), 1000); console.log(3); // 3 // 2 // 1
上面代码能够看出其实 setTimeout
并非在同步代码执行的时候就放入了异步队列,而是等待时间到达时才会放入异步队列,因此才会有了上面的结果。线程
案例 3code
setImmediate(function() { console.log("setImmediate"); }); setTimeout(function() { console.log("setTimeout"); }, 0); console.log(1); // 1 // setTimeout // setImmediate
同为宏任务,setImmediate
在 setTimeout
延迟时间为 0
时是晚于 setTimeout
被放入异步队列的,这里须要注意的是 setImmediate
在浏览器端,到目前为止只有 IE 实现了。
上面的案例都是关于宏任务,下面咱们举一个有微任务的案例来看一看微任务和宏任务的执行机制,在浏览器端微任务的表明其实就是 Promise 的 then
方法。
案例 4
setTimeout(() => { console.log("setTimeout1"); Promise.resolve().then(data => { console.log("Promise1"); }); }, 0); Promise.resolve().then(data => { console.log("Promise2"); setTimeout(() => { console.log("setTimeout2"); }, 0); }); // Promise2 // setTimeout1 // Promise1 // setTimeout2
从上面的执行结果其实能够看出,同步代码在栈中执行完毕后会先去执行微任务队列,将微任务队列执行完毕后,会去执行宏任务队列,宏任务队列执行一个宏任务之后,会去看看有没有产生新的微任务,若是有则清空微任务队列后再执行下一个宏任务,依次轮询,直到清空整个异步队列。
在 Node 中的事件轮询机制与浏览器类似又不一样,类似的是,一样先在栈中执行同步代码,一样是先进后出,不一样的是 Node 有本身的多个处理不一样问题的阶段和对应的队列,也有本身内部实现的微任务 process.nextTick
,Node 的整个事件轮询机制是 Libuv 库实现的。
Node 中事件轮询的流程以下图:
从图中能够看出,在 Node 中有多个队列,分别执行不一样的操做,而每次在队列切换的时候都去执行一次微任务队列,反复的轮询。
案例 1
setTimeout(function() { console.log("setTimeout"); }, 0); setImmediate(function() { console.log("setInmediate"); });
默认状况下 setTimeout
和 setImmediate
是不知道哪个先执行的,顺序不固定,Node 执行的时候有准备的时间,setTimeout
延迟时间设置为 0
实际上是大概 4ms
,假设 Node 准备时间在 4ms
以内,开始执行轮询,定时器没到时间,因此轮询到下一队列,此时要等再次循环到 timer
队列后执行定时器,因此会先执行 check
队列的 setImmediate
。
若是 Node 执行的准备时间大于了 4ms
,由于执行同步代码后,定时器的回调已经被放入 timer
队列,因此会先执行 timer
队列。
案例 2
setTimeout(() => { console.log("setTimeout1"); Promise.resolve().then(() => { console.log("Promise1"); }); }, 0); setTimeout(() => { console.log("setTimeout2"); }, 0); console.log(1); // 1 // setTimeout1 // setTimeout2 // Promise1
Node 事件轮询中,轮询到每个队列时,都会将当前队列任务清空后,在切换下一队列以前清空一次微任务队列,这是与浏览器端不同的。
浏览器端会在宏任务队列当中执行一个任务后插入执行微任务队列,清空微任务队列后,再回到宏任务队列执行下一个宏任务。
上面案例在 Node 事件轮询中,会将 timer
队列清空后,在轮询下一个队列以前执行微任务队列。
案例 3
setTimeout(() => { console.log("setTimeout1"); }, 0); setTimeout(() => { console.log("setTimeout2"); }, 0); Promise.resolve().then(() => { console.log("Promise1"); }); console.log(1); // 1 // Promise1 // setTimeout1 // setTimeout2
上面代码的执行过程是,先执行栈,栈执行时打印 1
,Promise.resolve()
产生微任务,栈执行完毕,从栈切换到 timer
队列以前,执行微任务队列,再去执行 timer
队列。
案例 4
setImmediate(() => { console.log("setImmediate1"); setTimeout(() => { console.log("setTimeout1"); }, 0); }); setTimeout(() => { console.log("setTimeout2"); setImmediate(() => { console.log("setImmediate2"); }); }, 0); //结果1 // setImmediate1 // setTimeout2 // setTimeout1 // setImmediate2 // 结果2 // setTimeout2 // setImmediate1 // setImmediate2 // setTimeout1
setImmediate
和 setTimeout
执行顺序不固定,假设 check
队列先执行,会执行 setImmediate
打印 setImmediate1
,将遇到的定时器放入 timer
队列,轮询到 timer
队列,由于在栈中执行同步代码已经在 timer
队列放入了一个定时器,因此按前后顺序执行两个 setTimeout
,执行第一个定时器打印 setTimeout2
,将遇到的 setImmediate
放入 check
队列,执行第二个定时器打印 setTimeout1
,再次轮询到 check
队列执行新加入的 setImmediate
,打印 setImmediate2
,产生结果 1
。
假设 timer
队列先执行,会执行 setTimeout
打印 setTimeout2
,将遇到的 setImmediate
放入 check
队列,轮询到 check
队列,由于在栈中执行同步代码已经在 check
队列放入了一个 setImmediate
,因此按前后顺序执行两个 setImmediate
,执行第一个 setImmediate
打印 setImmediate1
,将遇到的 setTimeout
放入 timer
队列,执行第二个 setImmediate
打印 setImmediate2
,再次轮询到 timer
队列执行新加入的 setTimeout
,打印 setTimeout1
,产生结果 2
。
案例 5
setImmediate(() => { console.log("setImmediate1"); setTimeout(() => { console.log("setTimeout1"); }, 0); }); setTimeout(() => { process.nextTick(() => console.log("nextTick")); console.log("setTimeout2"); setImmediate(() => { console.log("setImmediate2"); }); }, 0); //结果1 // setImmediate1 // setTimeout2 // setTimeout1 // nextTick // setImmediate2 // 结果2 // setTimeout2 // nextTick // setImmediate1 // setImmediate2 // setTimeout1
这与上面一个案例相似,不一样的是在 setTimeout
执行的时候产生了一个微任务 nextTick
,咱们只要知道,在 Node 事件轮询中,在切换队列时要先去执行微任务队列,不管是 check
队列先执行,仍是 timer
队列先执行,都会很容易分析出上面的两个结果。
案例 6
const fs = require("fs"); fs.readFile("./.gitignore", "utf8", function() { setTimeout(() => { console.log("timeout"); }, 0); setImmediate(function() { console.log("setImmediate"); }); }); // setImmediate // timeout
上面案例的 setTimeout
和 setImmediate
的执行顺序是固定的,前面都是不固定的,这是为何?
由于前面的不固定是在栈中执行同步代码时就遇到了 setTimeout
和 setImmediate
,由于没法判断 Node 的准备时间,不肯定准备结束定时器是否到时并加入 timer
队列。
而上面代码明显能够看出 Node 准备结束后会直接执行 poll
队列进行文件的读取,在回调中将 setTimeout
和 setImmediate
分别加入 timer
队列和 check
队列,Node 队列的轮询是有顺序的,在 poll
队列后应该先切换到 check
队列,而后再从新轮询到 timer
队列,因此获得上面的结果。
案例 7
Promise.resolve().then(() => console.log("Promise")); process.nextTick(() => console.log("nextTick")); // nextTick // Promise
在 Node 中有两个微任务,Promise
的 then
方法和 process.nextTick
,从上面案例的结果咱们能够看出,在微任务队列中 process.nextTick
是优先执行的。
上面内容就是浏览器与 Node 在事件轮询的规则,相信在读完之后应该已经完全弄清了浏览器的事件轮询机制和 Node 的事件轮询机制,并深入的体会到了他们之间的相同和不一样。