9月阅读周·JavaScript异步编程设计快速响应的网络应用:深入理解JavaScript事件,事件的调度篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读七个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》。
当前阅读周书籍:《JavaScript异步编程设计快速响应的网络应用》。
事件的调度
如果想让Javascript中的某段代码将来再运行,可以将它放在回调就
是一种普通函数,只不过它是传给像setTimeout这样的函数,或者绑定为像
document.onready 这样的属性。运行回调时,我们称已触发某事件(譬如延时结
束或页面加载完毕).
当然,可怕的总是那些细节,哪怕是像setTimeout这样看起来很简单的东
西。对setTimeout的描述通常像这样:
给定一个回调及n毫秒的延迟,setTimeout就会在n毫秒后运行该回调。
但是,正如我们将在这一节乃至这一章里看到的,以上描述存在严重缺陷。
大多数情况下,该描述只能算接近正确,而在其他情况下则完全是谬误。要想真
正理解 setTimeout,必须先大体理解Javascript事件模型。
现在还是将来运行
在探究setTimeout之前,先来看一个简单的例子。该情形常常会迷惑JavaScript新手,特别是那些刚刚从Java和Ruby等多线程语言迁移过来的新手。
EventModel/loopWithTimeout.js
for (var i = 1; i <= 3; i++) {
setTimeout(function(){ console.log(i); }, 0);
}
大多数刚接触JavaScript语言的人都会认为以上循环会输出1,2,3,或者重复输出这3个数字,因为这里的3次延时都抢着要第一个触发(每次暂停都调度为0毫秒后到时)。
要理解为什么输出是4,4,4,需要知道以下3件事。
- 这里只有一个名为i 的变量,其作用域由声明语句var i 定义(该声明语句在不经意间让i的作用域不是循环内部,而是扩散至蕴含循环的那个最内侧函数)。
- 循环结束后,i===4 一直递增,直到不再满足条件i<=3 为止。
- JavaScript事件处理器在线程空闲之前不会运行。
前两条还属于JavaScript 101的范畴,但第三个更像是一个“惊喜”。一开始使用JavaScript的时候,我也不太相信会这样。Java令我担心自己的代码随时会被中断。上百万种潜在的边界情况让我焦虑万分,我一直在想:“要是在这两行代码之间发生了什么稀奇古怪的事,会怎么样呢?”
然后,终于有一天,我再也没有这样的担心了……
线程的阻塞
下面这段代码打破了我对JavaScript事件的成见。
EventModel/loopBlockingTimeout.js
var start = new Date;
setTimeout(function(){
var end = new Date;
console.log('Time elapsed:', end - start, 'ms');
}, 500);
while (new Date - start < 1000) {};
按照多线程的思维定势,我会预计500毫秒后计时函数就会运行。不过这要求中断欲持续整整一秒钟的循环。如果运行代码,会得到类似这样的结果:
大家得到的数字可能会稍有不同,这是因为 setTimeout 和 setIn-terval一样,其计时精度要比我们的期望值差很多。不过,这个数字肯定至少是 1000,因为 setTimeout 回调在 while循环结束运行之前不可能被触发。
那么,如果 setTimeout 没有使用另一个线程,那它到底在做什么呢?
队列
调用 setTimeout 的时候,会有一个延时事件排入队列。然后setTimeout调用之后的那行代码运行,接着是再下一行代码,直到再也没有任何代码。这时 JavaScript 虚拟机才会问:“队列里都有谁啊?”
如果队列中至少有一个事件适合于“触发”(就像1000毫秒之前设定好的那个为期 500毫秒的延时事件),则虚拟机会挑选一个事件,并调用此事件的处理器(譬如传给 setTimeout 的那个函数)。事件处理器返回后,我们又回到队列处。
输入事件的工作方式完全一样:用户单击一个已附加有单击事件处理器的 DOM(Document Object Model,文档对象模型)元素时,会有一个单击事件排入队列。但是,该单击事件处理器要等到当前所有正在运行的代码均已结束后(可能还要等其他此前已排队的事件也依次结束)才会执行。因此,使用 JavaScript的那些网页一不小心就会变得毫无反应。
你可能听过事件循环这个术语,它是用于描述队列工作方式的。所谓事件循环,就像代码从一个循环中不断取出而运行一样:
runYourScript();
while (atLeastOneEventIsQueued) {
fireNextQueuedEvent();
};
总结
事件的易调度性是 JavaScript语言最大的特色之一。像 setTimeout这样的异步函数只是简单地做延迟执行,而不是孵化新的线程。JavaScript 代码永远不会被中断,这是因为代码在运行期间只需要排队事件即可,而这些事件在代码运行结束之前不会被触发。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)