Js事件循环机制 Event loop

更新日期: 2019-09-13阅读: 1.7k标签: 事件

前言

1、队列的特征先进先出
2、js是单线程的,任务都是排队执行,不会同步执行对个任务
3、js分为同步(赋值,循环,分支语句)和异步(ajax,dom事件,定时器)
4、事件循环机制
js执行引擎的主线程,从任务队列获取任务,如果是异步任务,那么运行到异步任务时候,异步任务就退出主线程,主线程进行下一个任务的获取处理,如果是异步任务完成,就插入到任务队列的末尾,等待主线程处理。


单线程

因为javascript是单线程的,所谓的单线程是指JS引擎中负责解释和执行javascript代码的线程只有一个,可以称为主线程。

除了主线程之外,还存在其他的线程。例如:处理 AJAX 请求的线程、处理 DOM 事件的线程、定时器线程、读写文件的线程(例如在Node.js中)等等。


异步过程

一个异步过程通常是这样的:主线程发起一个异步的请求,相应的工作线程请求并告知主线程已经收到异步函数的返回;主线程可以继续执行后面的代码。同时工作线程执行异步任务。工作线程工作完成后,通知主线程;主线程收到通知后,执行一定的工作。

异步函数通常具有以下的形式:

A(args..., callbackFn)

它可以叫做异步过程的发起函数,或者叫做异步任务注册函数。args是这个函数需要的参数。callbackFn也是这个函数的参数,但是它比较特殊所以单独列出来。

所以,从主线程的角度看,一个异步过程包括下面两个要素:

  • 发起函数(或叫注册函数)A
  • 回调函数callbackFn

它们都是在主线程上调用的,其中注册函数用来发起异步过程,回调函数用来处理结果。

举个具体的例子:

setTimeout(fn, 1000);

其中的setTimeout就是异步过程的发起函数,fn是回调函数。

前面说的形式A(args..., callbackFn)只是一种抽象的表示,并不代表回调函数一定要作为发起函数的参数,例如:
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx; // 添加回调函数
xhr.open('GET', url);
xhr.send(); // 发起函数

发起函数和回调函数就是分离的。


消息队列和事件循环

异步过程中,工作线程在异步操作完成后需要通知主线程,那么通知机制是怎么样的呢?就是利用消息队列和事件循环。

工作线程将消息放到消息队列,主线程通过事件循环过程去取消息。
  • 消息队列: 消息队列是一个先进先出的队列,里面放着各种各样的消息;
  • 事件循环: 事件循环是指主线程重复从消息队列中取消息,执行的过程。

实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。

事件循环用代码表示大概是这样的:

while(true) {
    var message = queue.get();
    execute(message);
}

那么,消息队列中放的消息具体是什么东西?消息的具体结构当然跟具体的实现有关,但是为了简单起见,我们可以认为:

消息就是注册异步任务时添加的回调函数。

再次以异步AJAX为例,假设存在如下的代码:

$.ajax('http://segmentfault.com', function(resp) {
    console.log('我是响应:', resp);
});

// 其他代码
...
...
...

主线程在发起AJAX请求后,会继续执行其他代码。AJAX线程负责请求segmentfault.com,拿到响应后,它会把响应封装成一个JavaScript对象,然后构造一条消息:

// 消息队列中的消息就长这个样子
var message = function () {
    callbackFn(response);
}
// 其中的callbackFn就是前面代码中得到成功响应时的回调函数。

主线程在执行完当前循环中的所有代码后,就会到消息队列取出这条消息(也就是message函数),并执行它。到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,AJAX线程在收到HTTP响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。

用图表示这个过程就是:

image

从上文中我们也可以得到这样一个明显的结论,就是:异步过程的回调函数,一定不在当前这一轮事件循环中执行。


异步和事件

上文中说的“事件循环”,为什么里面有个事件呢?那是因为:

消息队列中的每条消息实际上都对应着一个事件。

上文中一直没有提到一类很重要的异步过程:DOM事件。

举例来说:

var button = document.getElement('#btn');
button.addEventListener('click', function(e) {
    console.log();
});

从事件的角度来看,上述代码表示:在按钮上添加了一个鼠标单击事件的事件监听器;当用户点击按钮时,鼠标单击事件触发,事件监听器函数被调用。

从异步过程的角度看,addEventListener函数就是异步过程的发起函数,事件监听器函数就是异步过程的回调函数。事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行。

事件的概念实际上并不是必须的,事件机制实际上就是异步过程的通知机制。我觉得它的存在是为了编程接口对开发者更友好。

另一方面,所有的异步过程也都可以用事件来描述。例如:setTimeout可以看成对应一个时间到了!的事件。前文的 setTimeout(fn, 1000);可以看成:

timer.addEventListener('timeout', 1000, fn);


理解 JS 代码的执行

栗子1
console.log('start');

// Timer1
setTimeout(function() {
    console.log('hello');
},200);

// Timer2
setTimeout(function() {
    console.log('world');
},100);

console.log('end');

我们分步骤来进行这个过程解答

js执行引擎开始执行上述代码时,会先讲一个main()方法加入执行栈。首先第一个console.log(‘start’)入栈,console.log方法是一个webkit内核支持的普通方法,而不是Webapis涉及的方法,所以这里log(‘start’)方法立即出栈被引擎执行。

引擎继续往下,将setTimeout(callback,200)添加到执行栈。setTimeout()方法属于事件循环模型中WebAPIs中的方法,引擎在将setTimeout()方法出栈执行时,将延时执行的函数交给了相应模块,即图右方的timer模块来处理。

然后主线程继续向下执行,紧接着将第二个定时器也交给Timer模块,然后执行到第二个console.log(),控制台打印'end',

执行完毕后清空执行栈。但是并没有结束,在主线程执行的同时,Timer模块会检查其中的异步代码,一旦满足触发条件,就会将它添加到任务队列中。Timer2延迟100ms,所以会早于Timer1被添加到队列排头。而主线程此时处于空闲状态,所以会检查任务队列是否有待执行的任务。此时会将Timer2回调中的console.log()执行,控制台打印’world’,然后执行栈空闲后继续检查任务队列,将Timer1的代码压入执行栈中执行,控制台打印’hello’,清空执行栈,此时任务队列为空,执行结束,程序处理完毕,main()方法也出栈。

在这里再次强调一下,不是setTimeout加入了事件队列,而是setTimeout里面的回调函数加入了事件队列。定时设置的时间也未必到了相应的时间就会执行相应的消息,要等前面的事件执行完后才执行

栗子2
console.log(1);
// Time1
setTimeout(function() {
    console.log(2);
},300);
// Time2
setTimeout(function() {
    console.log(3)
},400);

for (var i = 0;i<10000;i++) {
    console.log(4);
}
// Time3
setTimeout(function() {
    console.log(5);
},100);

首先是打印出 1,然后是10000个4,那么Time1、Time2、Time3是顺序是如何的呢?

在这个代码中,for循环比较耗时,在Time1和Timer加入到执行队列中后,主线程依然还在执行for循环中的代码,处于阻塞状态。队列中的Time1和Time2并不会得以执行。当for循环结束,这时才将Time3交由Timer模块去管理,清空执行栈。虽然在这里Time3的延迟时间最短,但是加入任务队列后还是会排在Time1和Time2的后面,所以此时按顺序执行任务队列中的代码,依次打印2、3、5。

栗子3
console.log(1);

//Time2
setTimeout(function() {
    console.log(3)
},400);

//Time1
setTimeout(function() {
    console.log(2);
},300);

for (var i = 0;i<10000;i++) {
    console.log(4);
}
//Time3
setTimeout(function() {
    console.log(5);
},100);

我们将Time1和Time2的顺序对换一下,按照前面的说法,Time2先加入任务队列,然后是Time1,再然后是Time3。可是执行的结果还是1、4、2、3、5,这是为什么呢?虽然Time1的执行时间短,可是它比Time2晚加入任务队列啊。

为了验证这个问题,我们可以提出这样的一个假设:

如果setTimeout加入队列的阻塞时间大于两个setTimeout执行的间隔时间,那么先加入任务队列的先执行,尽管它里面设置的时间比另一个setTimeout的要大。
验证1
// Time2
setTimeout(function() {
    console.log(2);
},400);

var start=new Date();
for (var i = 0;i<5000;i++) {
    console.log('这里只是模拟一个耗时操作');
};
var end=new Date();
console.log('阻塞耗时:'+Number(end-start)+'毫秒');

// Time1
setTimeout(function() {
    console.log(3)
},300);

Time1比Time2设定的执行时间早100ms,但是Time2先加入任务队列,在Time2和Time1时间有一个阻塞的for循环。

验证2
setTimeout(function() {
    console.log(2);
},400);

var start=new Date();
for (var i = 0;i<500;i++) {
    console.log('这里只是模拟一个耗时操作');
};
var end=new Date();
console.log('阻塞耗时:'+Number(end-start)+'毫秒');

// Time1
setTimeout(function() {
    console.log(3)
},300);

此时,Time1先执行,因为阻塞的耗时小于Time1和Time2的执行间隔时间100毫秒;

验证3

我们再来验证一下,把Time2的执行时间设为350毫秒;

// Time2
setTimeout(function() {
    console.log(2);
},350);

var start=new Date();
for (var i = 0;i<500;i++) {
    console.log('这里只是模拟一个耗时操作');
};
var end=new Date();
console.log('阻塞耗时:'+Number(end-start)+'毫秒');

//Time1
setTimeout(function() {
    console.log(3)
},300);

直接结果为:

Time2先执行,因为阻塞的时间大于两个setTimeout之间的间隔时间。

通过上面的假设,我们可以得出这样一个结论:如果setTimeout加入队列的阻塞时间大于两个setTimeout执行的间隔时间,那么先加入任务队列的先执行,尽管它里面设置的时间可能比另一个setTimeout的要大。


链接: https://www.fly63.com/article/detial/5337

js中addEventListener事件监听器参数详解

我们都知道addEventListener() 的参数约定是:useCapture是可选参数,默认值为false,目前DOM 规范做了修订:addEventListener() 的第三个参数可以是个对象值了。passive就是告诉浏览器我可不可以用stopPropagation...

js监听浏览器返回,pushState,popstate 事件,window.history对象

在WebApp或浏览器中,会有点击返回、后退、上一页等按钮实现自己的关闭页面、调整到指定页面、确认离开页面或执行一些其它操作的需求。可以使用 popstate 事件进行监听返回、后退、上一页操作。

CSS中的pointer-events属性实现点穿效果

具有层级关系的结构中,使用了pointer-events:none 属性将会使当前元素中的事件不会被捕获,从而实现了点穿的效果。而当代码示例中假如top元素具有子元素且显示指定pointer-events属性不为none的时候,top元素注册的事件将会被捕获/冒泡触发

js鼠标事件参数,获取鼠标在网页中的坐标

事件对象 event,JavaScript 将事件event作为参数传递,IE中把 event 事件对象作为全局对象 window 的一个属性,获取鼠标在网页中的坐标 = 鼠标在视窗中的坐标 + 浏览器滚动条坐标

js事件冒泡和默认事件处理(原生js、vue)

何为默认事件?比如 a 会跳转页面,submit 会提交表单等。普通js方法:e.preventDefault()函数。Vue.js方法: .prevent 是vue 的内置修饰符,调用了 event.preventDefault()阻止默认事件

js keyup、keypress和keydown事件 详解

js keyup、keypress和keydown事件都是有关于键盘的事件,当一个按键被pressed 或released在每一个现代浏览器中,都可能有三种客户端事件。

深入nodejs-核心模块Events详解(事件驱动)

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用了一个事件驱动、非阻塞式 I/O的模型,使其轻量又高效。比如,文件操作中的fs事件流,网络编程所用到的tcp,http模块等,当你回想自己写的程序后,会发现很多操作都基于事件驱动,Events类。

纯CSS实现点击事件展现隐藏div菜单列表/元素切换

在写移动端导航的时候经常用到点击按钮出现/隐藏导航条的情况,最常见的方法当然还是前端框架直接调用,省心省力,不易出错;当然还有使用纯JS实现的小代码段。我这里整理了纯CSS实现方式,给需要的人和给自己做个笔记:实现原理利用CSS伪类:target

关于鼠标移动太快导致moseleave事件不触发的问题

我做的是一个table的编辑功能,当移入某行的时候展示编辑状态,在移出某行的时候显示的是原始状态,此时遇到一种情况,就是.当mousenter事件触发之后,由于鼠标移动得太快,同一个tr上绑定的mouseleave事件压根儿就没有执行。

Js事件传播流程

js事件传播流程主要分三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。在我们平常用的addEventListener方法中,一般只会用到两个参数,一个是需要绑定的事件,另一个是触发事件后要执行的函数

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!