9月阅读周·JavaScript异步编程设计快速响应的网络应用:深入理解JavaScript事件,异步函数的类型篇

举报
叶一一 发表于 2024/09/24 09:21:03 2024/09/24
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读七个月。已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScri...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出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畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。