浅析 event-loop 事件轮询

2018-10-11 admin

在这里插入图片描述

浏览器中的事件轮询

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://segmentfault.com/a/1190000016653777

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-42443.html

文章标题:浅析 event-loop 事件轮询

相关文章
Node.js之网络通讯模块实现浅析
前言 想必我们在用Node.js用的最多的应该是创建http服务,所以对于每个Web开发工程师而言,Node.js的网络相关模块学习是必不可少。 Node.js的网络模块架构 在Node.js的模块里面,与网络相关的模块有Net、DNS、H...
2017-04-05
Node.js事件驱动
Node.js事件驱动实现概览 虽然在ECMAScript的标准里并没有(也没有必要)明确规定“事件”,但是在浏览器中,事件作为一个极为重要的机制,给予JavaScript响应用户操作与DOM变化的能力;在Node.js中,异步事件驱动模型...
2017-03-24
javascript中attachEvent用法实例分析
本文实例讲述了javascript中attachEvent用法。分享给大家供大家参考。具体分析如下: 一般我们在JS中添加事件,是这样子的 obj.onclick=method 这种绑定事件的方式,兼容主流浏览器,但如果一个元素上添加多次同...
2017-03-23
JavaScript 事件对象介绍
JavaScript事件的一个重要的方面是它们拥有一些相对一致的特点,可以给开发提供强大的功能; 最方便和强大的就是事件对象,它们可以帮你处理鼠标事件和键盘敲击方面的情况; 此外还可以修改一般事件的捕获/冒泡流的函数; 一 事件对象 &#x...
2017-03-22
js实现touch移动触屏滑动事件
移动端触屏滑动的效果其实就是图片轮播,在PC的页面上很好实现,绑定click和mouseover等事件来完成。但是在移动设备上,要实现这种轮播的效果,就需要用到核心的touch事件。处理touch事件能跟踪到屏幕滑动的每根手指。 以下是四种...
2017-03-22
javascript相关事件的几个概念
客户端javascript程序采用了异步事件驱动编程模型。 相关事件的几个概念: **事件类型(event type):**用来说明发生什么类型事件的字符串; **事件目标(event target):**发生事件的对象; **事件处理程序...
2017-03-23
Zepto Touch events
“touch”模块添加以下事件,可以使用 on 和 off。 tap —元素tap的时候触发。 singleTap and doubleTap — 这一对事件可以用来检测元素上的单击和双击。(如果你不需要检测单击、双击,使用 tap 代替...
2017-04-26
鼠标经过子元素触发mouseout,mouseover事件的解决方案
我想实现的目标:当鼠标进入黑色框时,橙色框执行淡入动画;当黑色框范围移动的时候(即使经过粉色框,动画仍然不被触发);当鼠标移出的时候,橙色方块消失。 遇到的问题阐述:当鼠标移入黑色框的时候,橙色框执行淡入动画,但是当鼠标从黑色框经过粉色框的...
2017-03-27
javascript实现禁止鼠标滚轮事件
平时我们兼容什么东西总是在调整低版本IE的兼容性,但是这回不是因为低版本浏览器不给力。而是因为火狐给力过头了,完全不顾其它浏览器的感受标新立异了。除了火狐之外,所有的浏览器都可以使用MouseWheel事件来处理鼠标滚轮的响应。但是火狐却偏...
2017-03-27
JavaScript使用addEventListener添加事件监听用法实例
本文实例讲述了JavaScript使用addEventListener添加事件监听用法。分享给大家供大家参考。具体实现方法如下: <!DOCTYPE html> <html lang="en"> ...
2017-03-24
回到顶部