原因
虽然我完全没有系统地学习过事件循环机制,但是经过一段时间的体验,听说过一些比如宏任务和微任务,单线程,Ajax和它是异步操作,会被执行最后和其他零散的信息。结合实际的代码,大部分情况下也可以保证代码按照我想要的顺序执行,但是当我实际被问到这个问题的时候,我发现我并不能真正理解其中的原理,有很多相关资料,但还是得用自己的理解来表达。
为什么要有事件循环?
首先是一个简单的问题,换句话说,事件循环是做什么的,我为什么要学习这个?正如第一段所说,众所周知它是单线程语言,但这并不意味着不需要异步操作。反过来想。如果你写的所有Ajax操作都是同步的会发生什么:每次我们下一次向服务器发送请求时,整个页面都会停滞不前,直到请求返回,不管响应时间是1毫秒,1秒,或 1 分钟。对于用户体验来说,这无疑是一场灾难,所以提供了各种异步编程方法:事件循环、、、等。这里我们还是先关注事件循环,随着问题的深入,我们就会知道事件循环有什么问题为我们解决。
事件循环是如何工作的?
要了解这个问题,建议先看这个视频:Event Loop到底是什么? ,然后是视频中提到的网站:放大镜,结合视频,我们可以直观的看到事件是如何循环的,网站根据输入的代码来动画化这个过程。
按照视频的思路,我们将执行分为几个部分:调用栈、事件循环、回调队列、其他api。
调用栈
因为它是单线程的,所以我们只能逐句执行我们的代码。编译器每次读取一个函数,就将其压入栈中,栈顶函数返回结果时,弹出栈。在这个过程中,只有同步函数函数会进入调用栈跟随正常的执行流程,异步函数会进入回调队列,形成事件循环的第一步。
网络 API
视频中最让人意外的是,很多我们熟悉的功能都没有提供,而是来自于Web API,比如Ajax、DOM等。方法的实现并没有出现在源码中V8的,因为是浏览器提供的,更准确的说应该是运行环境提供的,因为运行环境不统一,不同的浏览器内核也没有提到。现在,我们可以将其分为浏览器和节点。似乎和我们讨论的事件循环无关,但还是有区别的。我们稍后会解释这个问题。
任务队列
异步方法经过Web API处理后会进入任务队列。例如,浏览器提供了一个计时器。处理此方法时,计时器在后台启动。当达到设定的时间时,该方法将被添加到任务队列中。当这批同步任务处理完毕后,方法会从队列中取出,放入调用栈中执行。因此,我们实际设置的时间是指最早可以执行该方法的时间,而不是延迟执行多长时间。让我们看一个例子,你可以先在脑海中运行模拟:
console.log('1')
setTimeout(function setFirstTimeout() {
console.log('2')
new Promise(function (resolve) {
console.log('3')
resolve()
}).then(function () {
console.log('4')
})
},0)
new Promise(function (resolve) {
console.log('5')
resolve()
}).then(function () {
console.log('6')
})
console.log('7')
实际执行我们可以得到 1、5、 7、6、2、3、4 结果,把这段代码放到提到的网站上上面可以很清楚的看到流程,我们定义这个方法经过Web API处理后进入Queue,等待主线程的代码执行完毕,然后通过事件循环的机制进入调用栈。
这一切都说得通:为什么总是最后执行,但真的是这样吗?让我们看下一个问题。
是否必须在所有代码的末尾执行 – 宏任务和微任务
即使这个问题没有仔细研究过,但我从经验中知道肯定不是这样的,虽然会相对延迟。 ,但并不总是在所有代码的末尾执行,这里有一个更大的问题——宏任务和微任务。我们在上面的代码中添加了一个 DOM 操作。
console.log('1')
$.on('button','click',function onClick(){
console.log('Clicked');
})
setTimeout(function setFirstTimeout() {
console.log('2')
new Promise(function (resolve) {
console.log('3')
resolve()
}).then(function () {
console.log('4')
})
},0)
new Promise(function (resolve) {
console.log('5')
resolve()
}).then(function () {
console.log('6')
})
console.log('7')
直接看结果,当事件的回调方法进入事件队列时,我点击了绑定了事件的按钮,所以点击的回调方法也进入了事件队列中,同步任务处理的时候,按照队列先进先出的原则,会先处理事件队列的回调方法,再处理点击事件的回调方法。
这不是一个巧妙的例子,但 DOM 操作确实与宏任务属于同一类别。与宏观任务相比,微任务是常见的类别。如下:
宏任务
微任务
其实从上面的例子中,应该有人已经发现执行顺序不正常了。 then 中的回调函数既不执行也不进入回调队列。显然,程序中没有错误。正是因为宏任务与微任务不同。
简单来说,宏任务和微任务都有自己的任务队列。在微任务队列中,当前宏任务执行完毕后,依次执行微任务。让我们丰富前面的例子:
console.log("1");
setTimeout(function s1() {
console.log("2");
process.nextTick(function p2() {
console.log("3");
});
new Promise(function (resolve) {
console.log("4");
resolve();
}).then(function t2() {
console.log("5");
});
});
process.nextTick(function p1() {
console.log("6");
});
new Promise(function (resolve) {
console.log("7");
resolve();
}).then(function t1() {
console.log("8");
});
console.log("9");
setTimeout(function s2() {
console.log("10");
process.nextTick(function () {
console.log("11");
});
new Promise(function (resolve) {
console.log("12");
resolve();
}).then(function () {
console.log("13");
});
});
在v16版本节点环境的执行结果为:1、7、9、6、8、2、4、@ >3、5、@ >15、1@>12、11、13,其他环境会有差异,后面再讲,先看手头的问题,以微任务为前提进行分析。
执行.log(5、4@>遇到宏任务,加入Queue,遇到微任务。加入Task Queue,执行.log(5、5@>微任务加入Task Queue for .log(5、6@>遇到宏任务时,添加到Queue中
全局宏任务执行后,我们可以得到这样两个队列,而1、@的输出7、9,接下来按照规则执行这个宏任务中的微任务p1和t1 , 得到 6 和 8。
队列
s1
p1
s2
t1
继续下一个宏任务s1:
.log(5、9@>遇到微任务时,将其加入Task Queue并执行.log in new(4)将微任务加入Task Queue
p2
t2
因此,以下输出为:2、4、@>3、5,以此类推,以下规则类似,不再一一赘述。
Node和浏览器有什么区别?
之前的问题应该解决了,但也带来了新的问题。前面提到过使用v16版本的node环境。执行,那么如果不是v16版本的node,甚至不使用node运行,会是什么结果呢?这次看到这篇文章评论区的一些讨论,彻底了解了执行机制,v10之前的事件循环中node的处理与浏览器不同,所以又得到了一个结果。切换到v10版本后,还是得到1、7、9、6、8、2、4、@>3、@ >5、15、1@>12、11、13,我个人认为以最新版本为准有兴趣的可以看文章评论区。
然后还有另一种情况。一开始我在Vue中验证了这段代码,得到的结果是1、7、9、8、2、4、@>5、 6、15、1@>12、13、3、11,前提是.是一个宏任务,这个结果是正确的,但我不太清楚为什么。另外,我以为还有一种方法,我查了一下,发现是不同的话题。限于篇幅,打算再开篇学习。具体内容也可以看这篇博客。 Vue的细节是还是?
还有什么?
写这篇博客是为了了解事件循环的机制,没想到内容这么多。刚上班的时候遇到一个问题,就是定时器走的越来越快。那个时候,我以为我已经想通了。今天从这篇文章的角度回过头来看,我只看到了冰山一角,而这篇文章也只写了事件循环的冰山一角。幸好现在知道了,除了Vue的问题,还有一个和事件循环相关的渲染问题,这部分内容后面会整理成文章,这里有一篇博文和一段视频推荐:
深入解析你所不知道的和浏览器渲染、帧动画、空闲回调(电影演示)
在循环中
暂无评论内容