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

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

背景

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

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

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效,已经坚持阅读七个月。

已读完书籍《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》

当前阅读周书籍《JavaScript异步编程设计快速响应的网络应用》

异步函数的编写

何时称函数为异步的

异步函数这个术语有点名不副实:调用一个函数时,程序只在该函数返回之后才能继续。JavaScript 写手如果称一个函数为“异步的”,其意思是这个函数会导致将来再运行另一个函数,后者取自于事件队列(若后面这个函数是作为参数传递给前者的,则称其为回调函数,简称为回调)。于是,一个取用回调的异步函数永远都能通过以下测试。

var functionHasReturned = false;
asyncFunction(function() {
console.assert(functionHasReturned);
});
functionHasReturned = true;

异步函数还涉及另一个术语,即非阻塞。非阻塞这个词强调了异步函数的高速度:异步MySQL数据库驱动程序做一个查询可能要花上一小时,但负责发送查询请求的那个函数却能以微秒级速度返回。这对于那些需要快速处理海量请求的网站服务器来说,绝对是个福音。

通常,那些取用回调的函数都会将其作为自己的最后一个参数。(可惜的是,老资格的setTimeout和setInterval都是这一约定的特例。)不过,有些异步函数也会间接取用回调,它们会返回 Promise对象或使用PubSub模式。本书稍后就会介绍这些异步设计模式。

遗憾的是,要想确认某个函数异步与否,唯一的方法就是审查其源代码。有些同步函数却拥有看起来像是异步的API,这或者是因为它们将来可能会变成异步的,又或者是因为回调这种形式能方便地返回多个参数。一旦存疑,请别指望函数就是异步的。

间或异步的函数

有些函数某些时候是异步的,但其他时候却不然。举个例子,jQuery的同名函数(通常记作$)可用于延迟函数直至DOM已经结束加载。但是,若 DOM 早已结束了加载,则不存在任何延迟,$的回调将会立即触发。

不注意的话,这种行为的不可预知性会带来很多麻烦。我曾经看到也犯过这样一个错误,即假定$会在已加载本页面其他脚本之后再运行一个函数。

// application.js
$(function() {
utils.log('Ready');
});
// utils.js
window.utils = {
log:function() {
if (window.console) console.log.apply(console, arguments);
}
};
<src ="application.js" />
<script src ="util.js" /script>

这段代码运行得很好,但前提是浏览器并未从缓存中加载页面(这会导致 DOM 早在脚本运行之前就已加载就绪)。如果出现这种情况,传递给$的回调就会在设置 utils.log 之前运行,从而导致一个错误。

缓存型异步函数

间或异步的函数有一个常见变种是可缓存结果的异步请求类函数。举例来说,假设正在编写一个基于浏览器的计算器,它使用了网页Worker 对象以单独开一个线程来进行计算。主脚本看起来像这样:

var calculationCache = {},
calculationCallbacks = {},
mathWorker = new Worker('calculator.js');
mathWorker.addEventListener('message', function(e) {
var message = e.data;
calculationCache[message.formula] = message.result;
calculationCallbacks[message.formula](message.result);
});
function runCalculation(formula, callback) {
if (formula in calculationCache) {
return callback(calculationCache[formula]);
};
if (formula in calculationCallbacks) {
return setTimeout(function() {
runCalculation(formula, callback);
}, 0);
};
mathWorker.postMessage(formula);
calculationCallbacks[formula] = callback;
}

在这里,当结果已经缓存时,runCalculation函数是同步的,否则就是异步的。存在3种可能的情景。

  • 公式已经计算完成,于是结果位于 calculationCache 中。这种情况下,runCalculation是同步的。
  • 公式已经发送给 Worker 对象,但尚未收到结果。这种情况下,runCalculation设定了一个延时以便再次调用自身;重复这一过程直到结果位于calculationCache中为止。
  • 公式尚未发送给Worker对象。这种情况下,将会从Worker对象的'message'事件监听器激活回调。

请注意,在第2种和第3种情景中,我们按照两种不同的方式来等待任务的完成。这个例子写成这样,就是为了演示依据哪几种常见方式来等待某些东西发生改变(如缓存型计算公式的值)。是不是应该倾向于其中某种方式呢?我们接着往下看。

异步递归与回调存储

在runCalculation函数中,为了等待Worker对象完成自己的工作,或者通过延时而重复相同的函数调用(即异步递归),或者简单地存储回调结果。

哪种方式更好呢?乍一看,只使用异步递归是最简单的,因为这里不再需要calculationCallbacks对象。出于这个目的,JavaScript新手常常会使用setTimeout,因为它很像线程型语言的风格。此程序的Java版本可能会有这样一个循环:

while (!calculationCache.get(formula)) {
Thread.sleep(0);
};

但是,延时并不是免费的午餐。大量延时的话,会造成巨大的计算荷载。异步递归有一点很可怕,即在等待任务完成期间,可触发之延时的次数是不受限的!此外,异步递归还毫无必要地复杂化了应用程序的事件结构。基于这些原因,应将异步递归视作一种“反模式”的方式。

在这个计算器例子中,为了避免异步递归,可以为每个公式存储一个回调数组。

var calculationCache = {},
calculationCallbacks = {},
mathWorker = new Worker('calculator.js');
mathWorker.addEventListener('message', function(e) {
var message = e.data;
calculationCache[message.formula] = message.result;
calculationCallbacks[message.formula]
.forEach(function(callback) {
callback(message.result);
});
});
function runCalculation(formula, callback) {
if (formula in calculationCache) {
return callback(calculationCache[formula]);
};
if (formula in calculationCallbacks) {
return calculationCallbacks[formula].push(callback);
};
mathWorker.postMessage(formula);
calculationCallbacks[formula] = [callback];
}

没有了延时,我们的代码要直观得多,也高效得多。

总的来说,请避免异步递归。仅当所采用的库提供了异步功能但没有提供任何形式的回调机制时,异步递归才有必要。如果真的遇到这种情况,要做的第一件事应该是为该库写一个补丁。或者,干脆找一个更好的库。

总结

JavaScript 中的每个异步函数都构建在其他某个或某些异步函数之上。凡是异步函数,从上到下(一直到原生代码)都是异步的!

反之亦然:任何函数只要使用了异步的函数,就必须以异步的方式给出其操作结果。JavaScript并没有提供一种机制以阻止函数在其异步操作结束之前返回。事实上,除非函数返回,否则不会触发任何异步事件。


作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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