9月阅读周·JavaScript异步编程设计快速响应的网络应用:深入理解JavaScript事件,异步函数的类型篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读七个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》。
当前阅读周书籍:《JavaScript异步编程设计快速响应的网络应用》。
异步函数的类型
异步的I/O函数
创造Node.js,并不是为了人们能在服务器上运行JavaScript,仅仅是因为Ryan Dahl想要一个建立在某高级语言之上的事件驱动型服务器框架。JavaScript碰巧就是适合干这个的语言。为什么?因为JavaScript语言可以完美地实现非阻塞式I/O。
在其他语言中,一不小心就会“阻塞”应用(通常是运行循环)直到完成 I/O请求为止。而在 JavaScript中,这种阻塞方式几乎沦为无稽之谈。类似如下的循环将永远运行下去,不可能停下来。
var ajaxRequest = new XMLHttpRequest;
ajaxRequest.open('GET', url);
ajaxRequest.send(null);
while (ajaxRequest.readyState === XMLHttpRequest.UNSENT) {
// readyState 在循环返回之前不会有更改。
};
相反,我们需要附加一个事件处理器,随即返回事件队列。
var ajaxRequest = new XMLHttpRequest;
ajaxRequest.open('GET', url);
ajaxRequest.send(null);
ajaxRequest.onreadystatechange = function() {
// ……
};
就是这么回事。不论是在等待用户的按键行为,还是在等待远程服务器的批量数据,所需要做的就是定义一个回调,除非JavaScript环境提供的某个同步I/O函数已经替我们完成了阻塞。
在浏览器端,Ajax方法有一个可设置为false的async选项(但永远、永远别这么做),这会挂起整个浏览器窗格直到收到应答为止。在 Node.js 中,同步的 API 方法在名称上会有明确的标示,譬如fs.readFileSync。编写短小的脚本时,这些同步方法会很方便。但是,如果所编写的应用需要处理并行的多个请求或多项操作,则应该避免使用它们。可在今天,还有哪个应用不是这样的呢?
有些I/O函数既有同步效应,也有异步效应。举例来说,在现代浏览器中操纵DOM对象时,从脚本角度看,更改是即时生效的,但从视效角度看,在返回事件队列之前不会渲染这些DOM对象更改。这可以防止 DOM 对象被渲染成不一致的状态。
console.log是异步的吗?
WebKit的console.log由于表现出异步行为而让很多开发者惊诧不已。在Chrome或Safari中,以下这段代码会在控制台记录{foo:bar}。
EventModel/log.js
var obj = {};
console.log(obj);
obj.foo = 'bar';
怎么会这样?WebKit的console.log并没有立即拍摄对象快照,相反,它只存储了一个指向对象的引用,然后在代码返回事件队列时才去拍摄快照。
Node的console.log是另一回事,它是严格同步的,因此同样的代码输出的却为{}。
JavaScript采用了非阻塞式I/O,这对新手来说是最大的一个障碍,但这同样也是该语言的核心优势之一。有了非阻塞式 I/O,就能自然而然地写出高效的基于事件的代码。
异步的计时函数
我们已经看到,异步函数非常适合用于I/O操作,但有些时候,我们仅仅是因为需要异步而想要异步性。换句话说,我们想让一个函数在将来某个时刻再运行——这样的函数可能是为了作动画或模拟。基于时间的事件涉及两个著名的函数,即setTimeout与setInterval。
遗憾的是,这两个著名的计时器函数都有自己的一些缺陷。有个缺陷是无法弥补的:当同一个JavaScript进程正运行着代码时,任何JavaScript计时函数都无法使其他代码运行起来。但是,即便容忍了这一局限性,setTimeout及setInterval的不确定性也会令人犯怵。下面是一个示例。
EventModel/fireCount.js
var fireCount = 0;
var start = new Date;
var timer = setInterval(function() {
if (new Date-start > 1000) {
clearInterval(timer);
console.log(fireCount);
return;
}
fireCount++;
}, 0);
如果使用 setInterval 调度事件且延迟设定为0毫秒,则会尽可能频繁地运行此事件,对吗?那么,在运行于高速英特尔 i7 处理器之上的现代浏览器中,此事件的触发频率到底如何呢?
大约为200次/秒。这是Chrome、Safari和Firefox等浏览器的平均值。在Node环境下,此事件的触发频率大约能达到1000次/秒。(若使用setTimeout 来调度事件,重复这些实验也会得到类似的结果。)作为对比,如果将setInterval替换成简单的while循环,则在Chrome中此事件的触发频率将达到400万次/秒,而在Node中会达到500万次/秒!
这是怎么回事?最后我们发现,setTimeout 和 setInterval 就是想设计成慢吞吞的!事实上,HTML规范(这是所有主要浏览器都遵守的规范)推行的延时/时隔的最小值就是4毫秒!
那么,如果需要更细粒度的计时,该怎么办呢?有些运行时环境提供了备选方案。
- 在Node中,process.nextTick 允许将事件调度成尽可能快地触发。对于笔者的系统,process.nextTick事件的触发频率可以超过10万次/秒。
- 一些现代浏览器(含 IE9+)带有一个 requestAnimationFrame函数。此函数有两个目标:一方面,它允许以60+帧/秒的速度运行JavaScript 动画;另一方面,它又避免后台选项卡运行这些动画,从而节约CPU周期。在最新版的Chrome浏览器中,甚至能实现亚毫秒级的精度。
尽管这些计时函数是异步JavaScript混饭吃的家伙什儿,但永远不要忘记,setTimeout 和 setInterval 就是些不精确的计时工具。在Node中,如果只是想产生一个短时延迟,请使用process.nextTick。在浏览器端,请尝试使用垫片技术(shim):在支持requestAnimationFrame 的浏览器中,推荐使用requestAnimationFrame;在不支持 requestAnimationFrame 的浏览器中,则退而使用setTimeout。
总结
每一种 JavaScript 环境都有自己的异步函数集。有些函数,如setTimeout和setInterval,是各种JavaScript环境普遍都有的。另一些函数则专属于某些浏览器或某几种服务器端框架。JavaScript环境提供的异步函数通常可以分为两大类:I/O 函数和计时函数。如果想在应用中定义复杂的异步行为,就要使用这两类异步函数作为基本的构造块。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)