你不懂js系列学习笔记-异步与性能- 02

第二章:回调

原文:You-Dont-Know-JS

主要理解 “回调地狱(callback hell)”痛苦的点到底是哪,以及尝试拯救回调。

1. 首先从实际生活中模拟

我相信大多数读者都曾经听某个人说过(甚至你自己就曾这么说),“我能一心多用”。试图表现得一心多用的效果包含幽默(孩子们的拍头揉肚子游戏),平常的行为(边走边嚼口香糖),和彻头彻尾的危险(开车时发微信)。

但我们是一心多用的人吗?我们真的能执行两个意识,有意地一起行动并在完全同一时刻思考/推理它们两个吗?我们最高级的大脑功能有并行的多线程功能吗?

答案可能令你吃惊:可能不是这样。

当我们 模拟 一心多用时,比如试着在打字的同时和朋友或家人通电话,实际上我们表现得更像一个快速环境切换器。换句话说,我们快速交替地在两个或更多任务间来回切换,在微小,快速的区块中 同时 处理每个任务。我们做的是如此之快,以至于从外界看开我们在 平行地 做这些事情。

难道这听起来不像异步事件并发吗(就像 JS 中发生的那样)?!如果不,回去再读一遍第一章!事实上,将庞大复杂的神经内科世界简化为我希望可以在这里讨论的东西的一个方法是,我们的大脑工作起来有点儿像事件轮询队列。我们的大脑可以被认为是运行在一个单线程事件轮询队列中,就像 JS 引擎那样。这听起来是个不错的匹配。

但是我们需要比我们刚才分析的更加细致入微。在我们如何计划各种任务,和我们的大脑实际如何运行这些任务之间,有一个巨大,明显的不同。

对比实际生活中自己计划做某些事情,我们小心,顺序地(A 然后 B 然后 C)计划,而且我们假设一个区间有某种临时的阻塞迫使 B 等待 A,使 C 等待 B。但实际上真正在执行的时候并不不会真正的按照心里的剧本来演。

比如:

“我得去商店,然后买些牛奶,然后去干洗店”。 但真实的情况往往是

“我得去趟商店,但是我确信在路上我会接到一个电话,于是‘嗨,妈妈’,然后她开始讲话,我会在 GPS 上搜索商店的位置,但那会花几分钟加载,所以我把收音机音量调小以便听到妈妈讲话,然后我发现我忘了穿夹克而且外面很冷,但没关系,继续开车并和妈妈说话,然后安全带警报提醒我要系好,于是‘是的,妈,我系着安全带呢,我总是系着安全带!’。啊,GPS 终于得到方向了,现在……”

这就是为什么正确编写和推理使用回调的异步 JS 代码是如此困难:因为它不是我们的大脑进行规划的工作方式。有许多不确定性,而且有时控制权并不在我们自己手里。

2. 回到代码中

考虑下面的代码:

listen("click", function handler(evt) {setTimeout(function request() {ajax("http://some.url.1", function response(text) {if (text == "hello") {handler();} else if (text == "world") {request();}});}, 500);
});

这样的代码常被称为“回调地狱(callback hell)”,有时也被称为“末日金字塔(pyramid of doom)”(由于嵌套的缩进使它看起来像一个放倒的三角形)。首先嵌套是问题吗?是它使追踪异步流程变得这么困难吗?当然,有一部分是。但是“回调地狱”实际上与嵌套/缩进几乎无关。它是一个深刻得多的问题。

让我不用嵌套重写一遍前面事件/超时/Ajax 嵌套的例子:

listen("click", handler);function handler() {setTimeout(request, 500);
}function request() {ajax("http://some.url.1", response);
}function response(text) {if (text == "hello") {handler();} else if (text == "world") {request();}
}

另一件需要注意的事是:为了将第 2,3,4 步链接在一起使他们相继发生,回调独自给我们的启示是将第 2 步硬编码在第 1 步中,将第 3 步硬编码在第 2 步中,将第 4 步硬编码在第 3 步中,如此继续。硬编码不一定是一件坏事,如果第 2 步应当总是在第 3 步之前真的是一个固定条件。

不过硬编码绝对会使代码变得更脆弱,因为它不考虑任何可能使在步骤前行的过程中出现偏差的异常情况。举个例子,如果第 2 步失败了,第 3 步永远不会到达,第 2 步也不会重试,或者移动到一个错误处理流程上,等等。

即便我们的大脑可能以顺序的方式规划一系列任务(这个,然后这个,然后这个),但我们大脑运行的事件的性质,使恢复/重试/分流这样的流程控制几乎毫不费力。如果你出去购物,而且你发现你把购物单忘在家里了,这并不会因为你没有提前计划这种情况而结束这一天。你的大脑会很容易地绕过这个小问题:你回家,取购物单,然后回头去商店。

但是手动硬编码的回调(甚至带有硬编码的错误处理)的脆弱本性通常不那么优雅。一旦你最终指明了(也就是提前规划好了)所有各种可能性/路径,代码就会变得如此复杂以至于几乎不能维护或更新。

这 才是“回调地狱”想表达的!嵌套/缩进基本上一个余兴表演,转移注意力的东西。

3. 信任问题

让我们来构建一个夸张的场景来生动地描绘一下信任危机。

想象你是一个开发者,正在建造一个贩卖昂贵电视的网站的结算系统。你已经将结算系统的各种页面顺利地制造完成。在最后一个页面,当用户点解“确定”购买电视时,你需要调用一个第三方函数(假如由一个跟踪分析公司提供),以便使这笔交易能够被追踪。

你注意到它们提供的是某种异步追踪工具,也许是为了最佳的性能,这意味着你需要传递一个回调函数。在你传入的这个程序的延续中,有你最后的代码——划客人的信用卡并显示一个感谢页面。

这段代码可能看起来像这样:

analytics.trackPurchase(purchaseData, function() {chargeCreditCard();displayThankyouPage();
});

足够简单,对吧?你写好代码,测试它,一切正常,然后你把它部署到生产环境。大家都很开心!

6 个月过去了,没有任何问题。你几乎已经忘了你曾写过的代码。一天早上,工作之前你先在咖啡店坐坐,悠闲地享用着你的拿铁,直到你接到老板慌张的电话要求你立即扔掉咖啡并冲进办公室。

当你到达时,你发现一位高端客户为了买同一台电视信用卡被划了 5 次,而且可以理解,他不高兴。客服已经道了歉并开始办理退款。但你的老板要求知道这是怎么发生的。“我们没有测试过这样的情况吗!?”

你甚至不记得你写过的代码了。但你还是往回挖掘试着找出是什么出错了。

在分析过一些日志之后,你得出的结论是,唯一的解释是分析工具不知怎么的,由于某些原因,将你的回调函数调用了 5 次而非一次。他们的文档中没有任何东西提到此事。

十分令人沮丧,你联系了客户支持,当然他们和你一样惊讶。他们同意将此事向上提交至开发者,并许诺给你回复。第二天,你收到一封很长的邮件解释他们发现了什么,然后你将它转发给了你的老板。

看起来,分析公司的开发者曾经制作了一些实验性的代码,在一定条件下,将会每秒重试一次收到的回调,在超时之前共计 5 秒。他们从没想要把这部分推到生产环境,但不知怎地他们这样做了,而且他们感到十分难堪而且抱歉。然后是许多他们如何定位错误的细节,和他们将要如何做以保证此事不再发生。等等,等等。

后来呢?

你找你的老板谈了此事,但是他对事情的状态不是感觉特别舒服。他坚持,而且你也勉强地同意,你不能再相信 他们 了(咬到你的东西),而你将需要指出如何保护放出的代码,使它们不再受这样的漏洞威胁。

修修补补之后,你实现了一些如下的特殊逻辑代码,团队中的每个人看起来都挺喜欢:

var tracked = false;analytics.trackPurchase(purchaseData, function() {if (!tracked) {tracked = true;chargeCreditCard();displayThankyouPage();}
});

注意: 对读过第一章的你来说这应当很熟悉,因为我们实质上创建了一个门闩来处理我们的回调被并发调用多次的情况。

但一个 QA 的工程师问,“如果他们没调你的回调怎么办?” 噢。谁也没想过。

你开始布下天罗地网,考虑在他们调用你的回调时所有出错的可能性。这里是你得到的分析工具可能不正常运行的方式的大致列表:

  • 调用回调过早(在它开始追踪之前)
  • 调用回调过晚 (或不调)
  • 调用回调太少或太多次(就像你遇到的问题!)
  • 没能向你的回调传递必要的环境/参数
  • 吞掉了可能发生的错误/异常
  • ...

这感觉像是一个麻烦清单,因为它就是。你可能慢慢开始理解,你将要不得不为 每一个传递到你不能信任的工具中的回调 都创造一大堆的特殊逻辑。

不仅仅是其他人或者第三方的代码,自己函数参数的检查/规范化是相当常见的,即便是我们理论上完全信任的代码。用一个粗俗的说法,编程好像是地缘政治学的“信任但验证”原则的等价物。

现在你更全面地理解了“回调地狱”有多地狱。

4. 尝试拯救回调

举个例子,为了更平静地处理错误,有些 API 设计提供了分离的回调(一个用作成功的通知,一个用作错误的通知):

function success(data) {console.log(data);
}function failure(err) {console.error(err);
}ajax("http://some.url.1", success, failure);

回调从不被调用的问题,可以设置一个超时来取消事件:

function timeoutify(fn, delay) {var intv = setTimeout(function() {intv = null;fn(new Error("Timeout!"));}, delay);return function() {// 超时还没有发生?if (intv) {clearTimeout(intv);fn.apply(this, [null].concat([].slice.call(arguments)));}};
}

这是你如何使用它:

// 使用“错误优先”风格的回调设计
function foo(err, data) {if (err) {console.error(err);} else {console.log(data);}
}ajax("http://some.url.1", timeoutify(foo, 500));

对于被调用的“过早”,可以总是异步地调用回调,即便它是“立即”在事件轮询的下一个迭代中,这样所有的回调都是可预见的异步。

复习

一个 JavaScript 程序总是被打断为两个或更多的代码块儿,第一个代码块儿 现在 运行,下一个代码块儿 稍后 运行,来响应一个事件。虽然程序是一块儿一块儿地被执行的,但它们都共享相同的程序作用域和状态,所以对状态的每次修改都是在前一个状态之上的。

不论何时有事件要运行,事件轮询 将运行至队列为空。事件轮询的每次迭代称为一个“tick”。用户交互,IO,和定时器会将事件在事件队列中排队。

在任意给定的时刻,一次只有一个队列中的事件可以被处理。当事件执行时,他可以直接或间接地导致一个或更多的后续事件。

并发是当两个或多个事件链条随着事件相互穿插,因此从高层的角度来看,它们在 同时 运行(即便在给定的某一时刻只有一个事件在被处理)。

在这些并发“进程”之间进行某种形式的互动协调通常是有必要的,比如保证顺序或防止“竞合状态”。这些“进程”还可以 协作:通过将它们自己打断为小的代码块儿来允许其他“进程”穿插。


更多专业前端知识,请上 【猿2048】www.mk2048.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/366576.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

c# 计算空格宽度像素_关于C#中换算像素和毫米的说明

在C#中是以像素作为尺寸单位的,像素是一种相对的尺寸概念,与毫米的转换跟当前显示器的分辨率有关,在不同分辨率下转换的系数也不同。借助C#中的GDI可以实现像素与毫米的换算:一、根据Win32 API定义函数获取显示器设备信息&#xf…

使用Apache Commons Net SMTP以Java(和Android)发送邮件:STARTTLS,SSL

最近我正在做一个Android实验,我想使用SMTP服务器通过android应用程序通过身份验证和加密来发送电子邮件。 好吧, 我发现Android上的javax.mail并不是一个很好的选择 ,因为它取决于awt类(我猜是传统); 有些…

列名无效

数据库增加新,视图没有更新转载于:https://www.cnblogs.com/Neil-Peng/p/9283355.html

天津天河计算机技术有限公司,“天河一号”超级计算机落户天津,命名为“天河”,取天津与“银河团队...

注意问题:一、对语句排序试题一般应注意以下几个问题:1.揣摩语段的整体意义,理清选项内容所提供的信息和表达内容的主旨。2.分析选项内容与整体语段的语境联系。3.进行对比分析,排除干扰项。该类试题几个选项在语句的数量或内容上…

tan和cot的梗_cot和tan的关系

各位家长好,我是51学霸(51xueba.cn)专栏作者,甜老师全文共计521字,建议阅读2分钟cot和tan的关联:tanαcotα1。在三角函数中,cotθcosθ/sinθ,当θ≠kπ,k∈Z时,cotθ1/tanθ&#…

基于 Webpack2、Vue2、iView2 的可视化脚手架 iView Cli 发布 2.0 版本

谷歌今天发布了一系列“性感”的软件,我们也发布了一款大家期待已久的开发者工具,同样很性感 :) iView 2.0 已经发布有两个月了,在 2.0 发布后,npm 下载量、issues 数量都提升了很多(可以 watch 下项目&…

在OSGi中为Karaf构建Camel-CXF REST服务–组播和聚合

请查看我在Karaf的OSGi中构建普通CXF服务(不使用Camel)的其他文章 。 这是有关如何 创建一个CXF REST服务 使用骆驼多播(并并行化)传入的请求 来自两个不同服务的源数据 汇总响应并 最后将合并结果作为JSON返回给最终用户。…

Myeclipse debug时出现跳行问题的解决

1.删除Myeclipse中部署的类似的重复项目(例如将测试项目中的代码包直接复制粘贴到另一个项目中)。 2.project->clean 3.删除服务器中部署的项目并重新部署。 4.开启debug模式,问题解决。转载于:https://www.cnblogs.com/abortre/p/9999323…

2017计算机三级哪个好考,快速突破2017年计算机三级考试的几大复习阶段

原标题:快速突破2017年计算机三级考试的几大复习阶段为某种原因,本人共考过2次笔试,3次上机。其中笔试在70左右,3次上机均为满分,看着大批的同学在为三级发愁不知道如何准备和应考。我想我应该把经验和学习的方法写下来…

cgcs2000大地坐标系地图_为什么要从北京54和西安80统一到CGCS2000?测绘人必知!...

导 读北京54坐标和西安80坐标,使用了很多年,为何要统一成CGCS2000坐标?启用CGCS2000坐标有何重大意义?概述北京54坐标系和西安1980坐标系的建立极大的促进了新中国测绘的发展,然而随着空间大地测量技术的兴起,这两种经典的局部大地…

Amazon Elastic Map Reduce使用Apache Mahout计算建议

Apache Mahout是一个“可扩展的机器学习库”,其中包含各种单节点和分布式推荐算法的实现。 在我的上一篇博客文章中, 我描述了如何在单个节点上实现在线推荐系统来处理数据。 如果数据太大而无法放入内存(> 100M首选项数据点)怎…

基于element-ui实现table可配置化

写在前面 感谢 饿了么前端团队提供组件化框架elememt-ui,本文基础组件使用element-ui。 大背景 在开发一些系统过程中,使用table作数据展示在所难免。先来看看el-table组件。 非常简单易用的组件,根据提供的data数据,配置table…

一种思路,隐藏input标签,通过label关联

一种思路&#xff0c;隐藏input标签&#xff0c;通过label关联 <label class"btn btn-default btn-file">上传图片 <input hidden type"file" class"btn btn-default"></label>posted on 2018-07-09 16:59 .Tang 阅读(...)…

微信小程序 反编译

反编译处于技术学习角度&#xff0c;瞻仰大神代码。 使用工具 node mumu安卓模拟器获取小程序文件 小程序的代码压缩之后 会形成一个后缀为 wxapkg 的文件&#xff0c;下载到微信客户端打开mumu模拟器&#xff0c;安装微信登录之后&#xff0c;打开步步换小程序 &#xff0c;这…

麟龙指标通达信指标公式源码_通达信指标公式源码波段极限副图源码

做价值的传播者&#xff0c;一路同行&#xff0c;一起成长问题&#xff1a;怎样才能每天都收到这类文章&#xff01;答案&#xff1a;只需点击上方《通达信公式指标》{买卖公式}AA:(2*CHIGHLOW)/4;BB:AA-REF(C,12);CC:EMA(BB,13);DD:EMA(CC,2);EE:EMA(BB,34);FF:EMA(BB,55);GG:…

计算机系统备份的原则和策略,计算机系统数据备份机制与策略

计算机系统数据备份机制与策略20年第5 05期华中电力第 l卷 8计算机系统数据备份机制与策略耿煜(樊学院机械系&#xff0c;北襄樊襄湖 4 15 ) 4 03摘要&#xff1a;针对当今计算环境中不断增长的数据量&#xff0c;系统地分析、论述了完整的数据备份机制&#xff0c;出了相应的策…

用Java递增Map值的最有效方法–仅搜索一次键

这个问题可能被认为太基础了&#xff0c;但是在论坛上经常被问到。 在本文中&#xff0c;我将讨论一种仅在Map ONCE中搜索键的方法。 让我们首先来看一个例子。 假设我正在使用Map创建一个字符串频率列表&#xff0c;其中每个键是一个正在计数的String &#xff0c;值是一个In…

[译] 帮助你成为一名成功的 Web 开发工程师的 21 步

前言 随着 Web 开发的蓬勃发展&#xff0c;许多人都在问这样一个问题&#xff1a;我如何才能成为一名 Web 开发者&#xff1f;我认为这个问题不应该这样问&#xff0c;而应该是&#xff1a;我如何才能成为一名成功的 Web 开发者&#xff1f;这样的问题是很有必要的&#xff0c;…

小白_Unity引擎_Mathf

Ceil 1 //向上取值&#xff0c;向大取值 2 Debug.Log(Mathf.Ceil(0.1f)); //1 3 Debug.Log(Mathf.Ceil(0.9f));//1 4 Debug.Log(Mathf.Ceil(-0.1f));//0 5 Debug.Log(Mathf.Ceil(-0.9f));//0 Floor 1 //向下取值&#xff0c;向…

循环卷积和周期卷积的关系_基于单口RAM读写的卷积电路(下)

这是迟到很久的卷积电路verilog设计的下篇。。。你看我还有机会吗。。。上回我们给出系统的层次结构、卷积计算模块以及用于数据缓存的fifo模块&#xff0c;今天我们首先回顾一下上一次的关键内容。系统结构回顾RTL代码文件可以分为结构如下所示 ~|--top_conv_tb.v|--top_conv.…