1.2 事件循环
现在我们来澄清一件事情(可能令人震惊):尽管你显然能够编写异步 JavaScript 代码(就像前面我们看到的定时代码),但直到最近(ES6),JavaScript 才真正内建有直接的异步概念。
什么?!这种说法似乎很疯狂,对不对?但事实就是这样。JavaScript 引擎本身所做的只不过是在需要的时候,在给定的任意时刻执行程序中的单个代码块。
“需要”,谁的需要?这正是关键所在!
JavaScript 引擎并不是独立运行的,它运行在宿主环境 中,对多数开发者来说通常就是 Web 浏览器。经过最近几年(不仅于此)的发展,JavaScript 已经超出了浏览器的范围,进入了其他环境,比如通过像 Node.js 这样的工具进入服务器领域。实际上,JavaScript 现如今已经嵌入到了从机器人到电灯泡等各种各样的设备中。
但是,所有这些环境都有一个共同“点”(thread,也指线程。不论真假与否,这都不算一个很精妙的异步笑话),即它们都提供了一种机制来处理程序中多个块的执行,且执行每块时调用 JavaScript 引擎,这种机制被称为事件循环 。
换句话说,JavaScript 引擎本身并没有时间的概念,只是一个按需执行 JavaScript 任意代码片段的环境。“事件”(JavaScript 代码执行)调度总是由包含它的环境进行。
所以,举例来说,如果你的 JavaScript 程序发出一个 Ajax 请求,从服务器获取一些数据,那你就在一个函数(通常称为回调函数 )中设置好响应代码,然后 JavaScript 引擎会通知宿主环境:“嘿,现在我要暂停执行,你一旦完成网络请求,拿到了数据,就请调用这个函数。”
然后浏览器就会设置侦听来自网络的响应,拿到要给你的数据之后,就会把回调函数插入到事件循环,以此实现对这个回调的调度执行。
那么,什么是事件循环?
先通过一段伪代码了解一下这个概念 :
// eventLoop是一个用作队列的数组 // (先进,先出) var eventLoop = [ ]; var event; // “永远”执行 while (true) { // 一次tick if (eventLoop.length > 0) { // 拿到队列中的下一个事件 event = eventLoop.shift(); // 现在,执行下一个事件 try { event(); } catch (err) { reportError(err); } } }
这当然是一段极度简化的伪代码,只用来说明概念。不过它应该足以用来帮助大家有更好的理解。
你可以看到,有一个用 while 循环实现的持续运行的循环,循环的每一轮称为一个 tick。对每个 tick 而言,如果在队列中有等待事件,那么就会从队列中摘下一个事件并执行。这些事件就是你的回调函数。
一定要清楚,setTimeout(..) 并没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的 tick 会摘下并执行这个回调。
如果这时候事件循环中已经有 20 个项目了会怎样呢?你的回调就会等待。它得排在其他项目后面——通常没有抢占式的方式支持直接将其排到队首。这也解释了为什么 setTimeout(..) 定时器的精度可能不高。大体说来,只能确保你的回调函数不会在指定的时间间隔之前运行,但可能会在那个时刻运行,也可能在那之后运行,要根据事件队列的状态而定。
所以换句话说就是,程序通常分成了很多小块,在事件循环队列中一个接一个地执行。严格地说,和你的程序不直接相关的其他事件也可能会插入到队列中。
前面提到的“直到最近”是指 ES6 从本质上改变了在哪里管理事件循环。本来它几乎已经是一种正式的技术模型了,但现在 ES6 精确指定了事件循环的工作细节,这意味着在技术上将其纳入了 JavaScript 引擎的势力范围,而不是只由宿主环境来管理。这个改变的一个主要原因是 ES6 中 Promise 的引入,因为这项技术要求对事件循环队列的调度运行能够直接进行精细控制(参见 1.4.3 节中对 setTimeout(..0) 的讨论),具体内容会在第 3 章中介绍。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论