浅析 event-loop 事件轮询

在这里插入图片描述


阅读原文


浏览器中的事件轮询

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 再也不占用全局做用域的执行环境,最后释放全局做用域,这就是在栈中执行同步代码时的先进后出原则,更像是一个杯子,先放进去的在最下面,须要最后取出。异步

而异步队列更像时一个管道,有两个口,从入口进,从出口出,因此是先进先出,在宏任务队列中表明的有 setTimeoutsetIntervalsetImmediateMessageChannel,微任务的表明为 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

同为宏任务,setImmediatesetTimeout 延迟时间为 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 中的事件轮询机制与浏览器类似又不一样,类似的是,一样先在栈中执行同步代码,一样是先进后出,不一样的是 Node 有本身的多个处理不一样问题的阶段和对应的队列,也有本身内部实现的微任务 process.nextTick,Node 的整个事件轮询机制是 Libuv 库实现的。

Node 中事件轮询的流程以下图:

在这里插入图片描述

从图中能够看出,在 Node 中有多个队列,分别执行不一样的操做,而每次在队列切换的时候都去执行一次微任务队列,反复的轮询。

案例 1

setTimeout(function() {
    console.log("setTimeout");
}, 0);

setImmediate(function() {
    console.log("setInmediate");
});

默认状况下 setTimeoutsetImmediate 是不知道哪个先执行的,顺序不固定,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

上面代码的执行过程是,先执行栈,栈执行时打印 1Promise.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

setImmediatesetTimeout 执行顺序不固定,假设 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

上面案例的 setTimeoutsetImmediate 的执行顺序是固定的,前面都是不固定的,这是为何?

由于前面的不固定是在栈中执行同步代码时就遇到了 setTimeoutsetImmediate,由于没法判断 Node 的准备时间,不肯定准备结束定时器是否到时并加入 timer 队列。

而上面代码明显能够看出 Node 准备结束后会直接执行 poll 队列进行文件的读取,在回调中将 setTimeoutsetImmediate 分别加入 timer 队列和 check 队列,Node 队列的轮询是有顺序的,在 poll 队列后应该先切换到 check 队列,而后再从新轮询到 timer 队列,因此获得上面的结果。

案例 7

Promise.resolve().then(() => console.log("Promise"));
process.nextTick(() => console.log("nextTick"));

// nextTick
// Promise

在 Node 中有两个微任务,Promisethen 方法和 process.nextTick,从上面案例的结果咱们能够看出,在微任务队列中 process.nextTick 是优先执行的。

上面内容就是浏览器与 Node 在事件轮询的规则,相信在读完之后应该已经完全弄清了浏览器的事件轮询机制和 Node 的事件轮询机制,并深入的体会到了他们之间的相同和不一样。

相关文章
相关标签/搜索