资讯专栏INFORMATION COLUMN

浅析 event-loop 事件轮询

2501207950 / 1223人阅读

摘要:如果执行的准备时间大于了,因为执行同步代码后,定时器的回调已经被放入队列,所以会先执行队列。


阅读原文


浏览器中的事件轮询

JavaScript 是一门单线程语言,之所以说是单线程,是因为在浏览器中,如果是多线程,并且两个线程同时操作了同一个 Dom 元素,那最后的结果会出现问题。所以,JavaScript 是单线程的,但是如果完全由上至下的一行一行执行代码,假如一个代码块执行了很长的时间,后面必须要等待当前执行完毕,这样的效率是非常低的,所以有了异步的概念,确切的说,JavaScript 的主线程是单线程的,但是也有其他的线程去帮我们实现异步操作,比如定时器线程、事件线程、Ajax 线程。

在浏览器中执行 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(已废弃)。

案例 1

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 是宏任务,晚于同步代码执行。

案例 2

setTimeout(() => console.log(1), 2000);
setTimeout(() => console.log(2), 1000);
console.log(3);

// 3
// 2
// 1

上面代码可以看出其实 setTimeout 并不是在同步代码执行的时候就放入了异步队列,而是等待时间到达时才会放入异步队列,所以才会有了上面的结果。

案例 3

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 的事件轮询机制,并深刻的体会到了他们之间的相同和不同。


文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98286.html

相关文章

  • 理解Event-Loop

    摘要:回调函数任务完成的时候,需要执行哪段代码来处理呢当然是回调函数了。事件处理器和回调函数类似。但是特定的事件处理器在浏览器进入异步事件驱动阶段时就会针对特定的事件注册。当事件对象返回到执行线程时,事件处理器也会同时进入执行栈中执行。 Event Loop(事件轮询)机制是一个经常把人搞晕的东东。我不敢说我完全明白,只是在此谈谈我的浅见。 事件的处理 浏览器是一个事件驱动(event-dr...

    blair 评论0 收藏0
  • 从网络IO到Thrift网络模型

    摘要:基本原理函数监视的文件描述符分类,分别是和。具体模型如下与模式相比,在完成数据读取之后,将业务处理过程交由一个线程池来完成,主线程直接返回进行下一次循环操作,效率大大提升。是提供的最高效的网络模型。 I/O多路复用 IO多路复用就是通过一种机制,一个进程可以监听多个文件描述符,一个某个描述符就绪(一般是读就绪或写就绪),就能够通知程序进行相应的读写操作。select、poll、epol...

    马永翠 评论0 收藏0
  • JavaScript从初级往高级走系列————异步

    摘要:之所以是单线程,取决于它的实际使用,例如不可能同添加一个和删除这个,所以它只能是单线程的。所以,这个新标准并没有改变单线程的本质。 原文博客地址:https://finget.github.io/2018/05/21/async/ 异步 什么是单线程,和异步有什么关系 什么是event-loop 是否用过jQuery的Deferred Promise的基本使用和原理 介绍一下asyn...

    andot 评论0 收藏0
  • Java NIO浅析

    摘要:阻塞请求结果返回之前,当前线程被挂起。也就是说在异步中,不会对用户线程产生任何阻塞。当前线程在拿到此次请求结果的过程中,可以做其它事情。事实上,可以只用一个线程处理所有的通道。 准备知识 同步、异步、阻塞、非阻塞 同步和异步说的是服务端消息的通知机制,阻塞和非阻塞说的是客户端线程的状态。已客户端一次网络请求为例做简单说明: 同步同步是指一次请求没有得到结果之前就不返回。 异步请求不会...

    yeooo 评论0 收藏0

发表评论

0条评论

2501207950

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<