引言

  相信所有学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 。在初期许多人会把异步理解成类似多线程的编程模式,其实他们中有着很大的差别,要完全理解异步,就需要了解 JS 的运行核心——事件循环(event loop)。在之前我对事件循环的认识也是一知半解的,直到我看了 Philip Roberts 的演讲 What the heck is the event loop anyway?,我才对事件循环有了一个全面的认识,所以我想写一篇介绍 JS 事件循环的文章,以供大家学习和参考。

一、为什么会有异步?

  为什么 JS 当中会有异步?我们想象一下,如果我们同步的执行一下代码会发生什么:

 $.get(url, function(data) {
//do something
});

  在我们使用 ajax 进行通信的时候,我们都默认了它是异步的,但是如果我们设置其为同步执行,会发生什么?如果你自己写一个小的测试程序,将后台代码延迟5s你会发现浏览器会出现阻塞,直到 ajax 响应了之后才会正常运行。这便是异步模式要解决的首要问题,如何使浏览器非阻塞的运行任务。想象一下如果我们同步的执行 ajax 请求的话,我们的等待的时间是一个未知数,在网络通信中可能很快也可能很慢,也可能永远也不会响应,这也就会导致浏览器会阻塞在一个未知的任务上面,这也是我们不希望看到的。所以我们希望有一种方式能够异步的处理程序,我们并不需要关心一个 ajax 请求会在何时完成,甚至它可以永远不会响应,我们只需要知道在请求响应后该如何处理,并且在等待响应的这段时间内我们还可以做一些其他的工作。因此,便有了 JavaScript Event Loop。

二、什么是事件队列?

  首先,我们先来看一段简单的代码:

 console.log("script start");

 setTimeout(function () {
console.log("setTimeout");
}, 1000); console.log("script end");

  你可以在这里查看结果:

  我们可以看到,首先,程序输出 'script start' 和 'script end',在大约1s之后输出了 'setTimeout' 。该程序的 'script end' 并没有等待1s之后输出,而是立即输出。这是因为 setTimeout 是一个异步的函数。意思也就是说当我们设置一个延迟函数的时候,当前脚本并不会阻塞,它只是会在浏览器的事件表中进行记录,程序会继续向下执行。当延迟的时间结束之后,事件表会将回调函数添加至事件队列(task queue)中,事件队列拿到了任务过后便将任务压入执行栈(stack)当中,执行栈执行任务,输出 'setTimeout'。

  事件队列是一个存储着待执行任务的队列,其中的任务严格按照时间先后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕之后,再执行下一个任务。执行栈则是一个类似于函数调用栈的运行容器,当执行栈为空时,JS 引擎便检查事件队列,如果不为空的话,事件队列便将第一个任务压入执行栈中运行。

  现在,我们对上面的代码进行一点修改:

 console.log("script start");

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

  将延迟时间设置为0,看看程序会以何种顺序输出?无论我们设置多少的延迟时间,'setTimeout' 总是会在 'script end' 之后输出。有些浏览器可能会有一个最小延迟时间,有的是 15ms,有的是 10ms,这个在很多书当中都有提到,这可能会给同学们造成一种错觉:由于程序运行速度很快,并且有最小延迟时间,所以 'setTimeout' 会在 'script end' 之后输出。现在让我们在稍微变一下,来消除你的错觉:

 console.log("script start");

 setTimeout(function () {
console.log("setTimeout");
}, 0); //具体数字不定,这取决于你的硬件配置和浏览器
for(var i = 0; i < 999999999; i ++){
//do something
} console.log("script end");

  你可以在这里查看结果:

  可以看出,无论后面我们做了多少延迟性的工作,'setTimeout' 总是会在 'script end' 之后输出。所以究竟发生了什么?这是因为 setTimeout 的回调函数只是会被添加至事件队列,而不是立即执行。由于当前的任务没有执行结束,所以 setTimeout 任务不会执行,直到输出了 'script end' 之后,当前任务执行完毕,执行栈为空,这时事件队列才会把 setTimeout 回调函数压入执行栈执行。

  执行栈则像是函数的调用栈,是一个树状的栈:

三、事件队列有何作用?

  通过以上的 demo 相信同学们都会对事件队列和执行栈有了一个基本的认识,那么事件队列有何作用?最简单易懂的一点就是之前我们所提到的异步问题。由于 JS 是单线程的,同步执行任务会造成浏览器的阻塞,所以我们将 JS 分成一个又一个的任务,通过不停的循环来执行事件队列中的任务。这就使得当我们挂起某一个任务的时候可以去做一些其他的事情,而不需要等待这个任务执行完毕。所以事件循环的运行机制大致分为以下步骤:

  1.   检查事件队列是否为空,如果为空,则继续检查;如不为空,则执行 2;
  2.   取出事件队列的首部,压入执行栈;
  3. 执行任务;
  4. 检查执行栈,如果执行栈为空,则跳回第 1 步;如不为空,则继续检查;

  然而目前为止我们讨论的仅仅是 JS 引擎如何执行 JS 代码,现在我们结合 Web APIs 来讨论事件循环在当中扮演的角色。

  在开始我们讨论过 ajax 技术的异步性和同步性,通过事件循环机制,我们则不需要等待 ajax 响应之后再进行工作。我们则是设置一个回调函数,将 ajax 请求挂起,然后继续执行后面的代码,至于请求何时响应,对我们的程序不会有影响,甚至它可能永远也不响应,也不会使浏览器阻塞。而当响应成功了以后,浏览器的事件表则会将回调函数添加至事件队列中等待执行。事件监听器的回调函数也是一个任务,当我们注册了一个事件监听器时,浏览器事件表会进行登记,当我们触发事件时,事件表便将回调函数添加至事件队列当中。

  我们知道 DOM 操作会触发浏览器对文档进行渲染,如修改排版规则,修改背景颜色等等,那么这类操作是如何在浏览器当中奏效的?至此我们已经知道了事件循环是如何执行的,事件循环器会不停的检查事件队列,如果不为空,则取出队首压入执行栈执行。当一个任务执行完毕之后,事件循环器又会继续不停的检查事件队列,不过在这间,浏览器会对页面进行渲染。这就保证了用户在浏览页面的时候不会出现页面阻塞的情况,这也使 JS 动画成为可能, jQuery 动画在底层均是使用 setTimeout 和 setInterval 来进行实现。想象一下如果我们同步的执行动画,那么我们不会看见任何渐变的效果,浏览器会在任务执行结束之后渲染窗口。反之我们使用异步的方法,浏览器会在每一个任务执行结束之后渲染窗口,这样我们就能看见动画的渐变效果了。

  考虑如下两种遍历方式:

 var arr = new Array(999);
arr.fill(1);
function asyncForEach(array, handler){
var t = setInterval(function () {
if(array.length === 0){
clearInterval(t);
}else {
handler(arr.shift());
}
}, 0);
} //异步遍历
asyncForEach(arr, function (value) {
console.log(value);
}); //同步遍历
arr.forEach(function (value, index, arr) {
console.log(value);
});

  经过测试,我们可以看出,采用同步遍历的方法,当数组长度上升到3位数的时候,便会出现阻塞,但是异步遍历却不会出现阻塞现象(除非数组长度非常大,那是因为计算机的内存空间不足)。这是因为同步遍历方法是一个单独的任务,这个任务会将所有的数组元素遍历一遍,然后才会开始下一个任务。而异步遍历的方法将每一次遍历拆分成一个单独的任务,一个任务只遍历一个数组元素,所以在每个任务之间,我们的浏览器可以进行渲染,所以我们不会看见阻塞的情况。下面这个 demo 演示了在异步遍历前后发生的事情:

总结

  现在,相信你已经认识了 JavaScript 的真实面目了吧。 JavaScript 是一门单线程的语言,但是其事件循环的特性使得我们可以异步的执行程序。这些异步的程序也就是一个又一个独立的任务,这些任务包括了 setTimeout、setInterval、ajax、eventListener 等等。关于事件循环,我们需要记住以下几点:

  •   事件队列严格按照时间先后顺序将任务压入执行栈执行;
  • 当执行栈为空时,浏览器会一直不停的检查事件队列,如果不为空,则取出第一个任务;
  • 在每一个任务结束之后,浏览器会对页面进行渲染;

  本文 demo 放在 jsfiddle 上,如需转载,注明下出处就好了。若您发现本文有所纰漏,欢迎在评论区指出。

深入理解 JavaScript 事件循环(一)— event loop的更多相关文章

  1. 一篇文章图文并茂地带你轻松学完 JavaScript 事件循环机制(event loop)

    JavaScript 事件循环机制 (event loop) 本篇文章已经默认你有了基础的 ES6 和 javascript语法 知识. 本篇文章比较细致,如果已经对同步异步,单线程等概念比较熟悉的读 ...

  2. 理解js事件循环(event loop)

    队列:先进先出 栈:后进先出 javascript的Event Loop 和 Node.js的Event Loop 区别: js(运行在浏览器),有主线程.异步任务队列的概念: node.js使用li ...

  3. 深入理解JavaScript的事件循环(Event Loop)

    一.什么是事件循环 JS的代码执行是基于一种事件循环的机制,之所以称作事件循环,MDN给出的解释为 因为它经常被用于类似如下的方式来实现 while (queue.waitForMessage()) ...

  4. Node.js 事件循环(Event Loop)介绍

    Node.js 事件循环(Event Loop)介绍 JavaScript是一种单线程运行但又绝不会阻塞的语言,其实现非阻塞的关键是“事件循环”和“回调机制”.Node.js在JavaScript的基 ...

  5. js事件循环机制(Event Loop)

    javascript从诞生之日起就是一门  单线程的  非阻塞的  脚本语言,单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务,非阻塞靠的就是 event lo ...

  6. js高级-浏览器事件循环机制Event Loop

    JavaScript 是队列的形式一个个执行的 同一时间只能执行一段代码,单线程的  (队列的数据结构) 浏览器是多线程的 JavaScript执行线程负责执行js代码 UI线程负责UI展示的 Jav ...

  7. 深入理解 JavaScript 事件循环(二)— task and microtask

    引言 microtask 这一名词是 JS 中比较新的概念,几乎所有人都是在学习 ES6 的 Promise 时才接触这一新概念,我也不例外.当我刚开始学习 Promise 的时候,对其中回调函数的执 ...

  8. [译] 深入理解 JavaScript 事件循环(二)— task and microtask

    引言 microtask 这一名词是 JS 中比较新的概念,几乎所有人都是在学习 ES6 的 Promise 时才接触这一新概念,我也不例外.当我刚开始学习 Promise 的时候,对其中回调函数的执 ...

  9. 深入理解JavaScript事件循环机制

    前言 众所周知,JavaScript 是一门单线程语言,虽然在 html5 中提出了 Web-Worker ,但这并未改变 JavaScript 是单线程这一核心.可看HTML规范中的这段话: To ...

随机推荐

  1. 利用GDB进行多线程调试

    一.多线程调试 多线程调试重要就是下面几个命令: info thread 查看当前进程的线程. thread <ID> 切换调试的线程为指定ID的线程. break file.c:100 ...

  2. 使用Blender的UV映射制作一个地球

    UV映射是一个用来2D图片纹理转换3D网格的标准技术.U和V表示平面坐标的两个轴,对应了3D空间中X.Y和Z.Blender手册是这样解释UV映射的:想象一个3D模型对象,例如一个球体,平铺到桌面上. ...

  3. Android打开其它应用程序

    PackageManager pm = getPackageManager(); Intent i = pm.getLaunchIntentForPackage(packageName); start ...

  4. Redis学习-发布/订阅

    Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息.Redis 客户端可以订阅任意数量的频道. 常用命令 命令 描述 复杂度 返回 PSUBS ...

  5. java的List分页 取出数据后使用List分页

    以前一直是在DAO层直接从数据库里分页,但是今天因为有些数据,需要混合展示,就是根据条件取出了多个对象的集合,然后把这些多个List放到一个List里,然后在从这个List里进行分页. MemberA ...

  6. H5水果机,一个网络版的lao hu ji

    该游戏为h5小游戏,纯属娱乐,技术探讨,相关技术在文章结尾,欢迎探讨交流 花了几天时间开发了这款水果lao hu ji,更新了几个版本,还有不足的地方,由于时间有限暂时没有继续更新新版本 未完成的功能 ...

  7. POJ2524并查集水题

    Description There are so many different religions in the world today that it is difficult to keep tr ...

  8. LeetCode Weekly Contest

    链接:https://leetcode.com/contest/leetcode-weekly-contest-33/ A.Longest Harmonious Subsequence 思路:hash ...

  9. myEclipse hibernate连接数据库配置方法

    Access数据库:

  10. sublime比较好用的插件

    emmet, markdown preview, package Control, SFTP, Anaconda