摘要:一个没有返回值的函数执行的效果其实是利用它的副作用一个没有返回值和利用副作用的函数其实就是一个黑洞。
本篇博客尚未上传 github
github 首页(star+watch,一手动态直达): https://github.com/HCThink/h-blog
掘金 link , 掘金 专栏
segmentfault 主页
原创禁止私自转载
异步处理方案系列- 1.callback 引言异步/异步操作,已经是前端领域一个老生常谈的话题.也是做前端开发中经常面临的一个问题.
然而异步的问题往往比较复杂且难于处理, 特别是异步问题还经常不是多带带出现,往往存在比较多样的组合关系.
在实际处理中就显得更加复杂而难于处理. 特别是在 io 操作频繁,或者 node server 中,经常遇到非常复杂的组合型异步。
举个业务开发中常见的例子:
eg: 省市县三级级联问题
这个问题非常常见, 假设数据量较大, 我们大多数情况下不会一次加载所有的数据, 然后做前端级联的方案.
而是采取三个数据接口,在下拉改变的时候去动态请求的方式.这就形成一种很常见的多个异步串行的模型.
怎么处理这样的问题, 怎么较好的维护多个异步之间的关系, 怎么让代码正常执行的同时,在逻辑和结构上更可读呢?
我将会梳理
callback
cps
thunk
defer / promise(非 es6)
promise(ES6)
generator -> co.
async / await
这几种处理方式. 加上两种模式
事件监听
订阅发布模型
列出一个系列的博客去讨论这个问题.
看我们在不同阶段, 使用不同技术,如何处理相同的问题. 在不同方案之间横向对比, 去深入了解
技术变迁以及背后的处理思路和逻辑的变化.
什么是回调呢? 这么问似乎有点多余, 每个写过 javascript 的开发者, 或多或少都会接触到回调. 回调的使用成本很低,
实现回调函数就像传递一般的参数变量一样简单.由于函数式编程极好的支持,以至于这项技术使用基本没有障碍.我们随手就能写出一个回调
Ajax.get("http://xxxx", {}, (resp) => { // ..... })
但是呢,要真给回调下一个定义, 也还真不好回答.
我们不妨从一些侧面去看看回调
回调是一种处理特定问题的模式, 伴随着函数式编程而生. 函数式编程中很重要的技术之一就是回调函数
当一个函数作为主调函数的参数时, 它往往会在特定的时间和场景(上下文)中执行.
执行过程中,回调函数选择性接收函数内部的数据, 或者状态(内存), 经过处理选择性返回,或者改变状态(hock).
callback 业务模型说这么多, 我们不如从代码的角度去解决一个串行的异步模型.
为了说明问题, 我们将问题简化成 A B C 三个异步(可能是 io, 网络请求, 或者其他.为了方面描述, 我们采用 settimeout 来模拟), 这三个异步耗时不确定, 但是必须按照 A B C 的顺序处理他们的返回结果.
处理这个问题, 我们基本上有两种思路:
控制异步发出的顺序, 在 a 返回之后再发 b 请求, 这样将问题串行化(省市县模型中经常需要省的返回值去请求省所对应的市).
同时发出异步请求,控制处理的顺序.
方案一: 串行化请求// 模拟 ajax 函数 function ajax(url) { return function (cb) { setTimeout(function() { cb({ url }); }, Math.random() * 3000); } } // 初始化出三个请求 const A = ajax("/ofo/a"); const B = ajax("/ofo/b"); const C = ajax("/ofo/c"); // 控制请求顺序 log("ajax A send..."); A(function (a) { log("ajax A receive..."); log("ajax B send..."); B(function (b) { log("ajax B receive..."); log("ajax C send..."); C(function (C) { log("ajax C receive..."); }); }) })
代码很简单, 大多是方案也是这么走的, 因为 A 的返回值可以作为 B 的参数.
但是相应的这个模式的总时间必定大于三个请求的时间之和.输出如下:
ajax A send... ajax A receive... ajax B send... ajax B receive... ajax C send... ajax C receive...方案二: 自由请求,串行化处理
是相对不那么通用的方案, 但是处理没有直接数据依赖的串行请求非常合适.
// 发送容器 const sender = []; // 稍作改造 function ajax(url, time) { return function(cb) { // 记录发送顺序, 必须有序 sender.push(url); setTimeout(function() { const data = { from: url, reso: "ok" }; // 将 data, 回调传递给一个处理函数 dealReceive({url, cb, data}); }, time); } } // 按照顺序处理返回结果 // 返回结果容器 const receiver = {}; function dealReceive({url, cb, data}) { // 记录返回结果.可以无序 receiver[url] = {cb, data}; for (var i = 0; i < sender.length; i++) { let operate = receiver[sender[i]]; if(typeof operate === "object") { operate.cb.call(null, operate.data); } else { return; } } } // 手动模拟出请求时间, A 最耗时.b 最快, 更好说明问题 const A = ajax("/ofo/a", 4000); const B = ajax("/ofo/b", 600); const C = ajax("/ofo/c", 2000); // 注意我们的调用方式 是没有任何控制的 // A,B,C 依次发出. 还可以按照这个顺序处理 A,B,C 的返回值 A(function (a) { log(a); }); B(function (b) { log(b); }); C(function (c) { log(c); });
输出:
{"from":"/ofo/a","reso":"ok"} {"from":"/ofo/b","reso":"ok"} {"from":"/ofo/c","reso":"ok"}
这种方案总耗时基本上是耗时最长的 ajax 的耗时。
值得注意的是, A,B,C 的调用上没有做任何控制. A 最耗时, 但是要最最先处理 A 的返回数据.
实现这一点的关键就在于我们 dealReceive 有个轮询, 这个轮询不是定时触发的,而是每当请求回来时, 触发轮询. 整个过程轮询 3 次.
基本上 callback 处理组合异步模型的思路说完了.串行是容易处理的一种模型, 如果出现 c 依赖 a,b 都正确返回的模型时, 基本上我们暴力一点就是转化为串行关系. 尽管 a, b 没有关系.
或者呢我们就在 a, b 的回调里做标志位. 和 dealReceive 类似.
单个异步不需要有太多处理, callback 的一些细节也不做讨论. 主要讨论是回调在实际场景中的处理问题方案
回调两面性我们还是落入俗套的分析一下回调的优缺点.其实主要是缺点.
优点: 使用成本低, 处理简单问题非常方便.能够拿到主调函数内部的环境.等等.
大多数人认为的缺点:
回调很 low: 可能是因为, 实现回调函数就像传递一般的参数变量一样简单.由于函数式编程极好的支持,以至于这项技术使用基本没有障碍.也没有比较严格的模式要求.大家习以为常了.
回调地狱(代码横向发展): 其实这并不是回调的错. 当我们遇到回调无底洞的时候,也无需惊慌,其实这根本不是什么问题, 因为同样有协程和 monad 无底洞。因为如果你把任何一个抽象使用地足够频繁的话,都同样会创造一个无底洞。
使用回调上的建议: 没有使用障碍导致回调的滥用, 大部分问题都用了简单的回调堆叠来解决. 实际上我们有很多基于回调的模式可以避免这些问题.比如: cps, cps 进一步转化为 thunk.等等.
这样看来, 回调没有缺点, 是这样么? 不是的. 回调有非常致命的机制上的缺点, 这个问题可能在 node 中爆发,除非自身改变,或者被吃掉。
所谓的机制就是:你可能在用回调处理复杂问题的时候,对自己能力产生怀疑,这些异步之间的关系是那么难以梳理清晰,而又难以写出容易维护的代码.
其实这都不是你的错.
使用回调处理异步往往意味着,你舍弃了返回值,而使用回调接收异步操作结果. 而这正是用回调风格来编程会很困难的根本原因: 回调风格不返回任何值,所以难以组合[函数式编程中函数有良好的输入和输出是函数可以组合的根本]。
一个没有返回值的函数执行的效果其实是利用它的副作用
一个没有返回值和利用副作用的函数其实就是一个黑洞。
所以,使用回调风格来编程无法避免会是指令式的,它实际上是通过把一系列严重依赖于副作用的操作安排好执行顺序,而不是通过函数的调用来把输入输出值对应好。如果你是通过回调组织程序执行流程, 而不是靠理顺值的关系来解决问题的, 是很难编写出正确的并行程序
这种问题也间接的导致了回调难于调试,定位问题和维护.
最终的结果就是: 你崩溃了
注:系列博客陆续推出,稍安勿躁。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/100060.html
摘要:编写异步代码可能是一种不同的体验,尤其是对异步控制流而言。回调函数的准则在编写异步代码时,要记住的第一个规则是在定义回调时不要滥用闭包。为回调创建命名函数,避免使用闭包,并将中间结果作为参数传递。 本系列文章为《Node.js Design Patterns Second Edition》的原文翻译和读书笔记,在GitHub连载更新,同步翻译版链接。 欢迎关注我的专栏,之后的博文将在专...
摘要:运行机制小程序启动会有两种情况,一种是冷启动,一种是热启动。建议小程序在必要时使用监听内存告警事件,进行必要的内存清理。 前言 以小程序为切入点,深入理解总结方方面面的知识点,做成系列文章,希望能得到大神的指点和帮助新人入门,承上启下才是好程序猿由于是系列第一篇文章,紧跟着的是一大段废话,只关心技术的可以跳过 转眼半年又要过去了,意味着来新公司快半年了,离上次写文章也半年了,浑浑噩噩...
摘要:异步问题回调地狱首先,我们来看下异步编程中最常见的一种问题,便是回调地狱。同时使用也是异步编程最基础和核心的一种解决思路。基于,目前也被广泛运用,其是异步编程的一种解决方案,比传统的回调函数解决方案更合理和强大。 关于 微信公众号:前端呼啦圈(Love-FED) 我的博客:劳卜的博客 知乎专栏:前端呼啦圈 前言 在实际编码中,我们经常会遇到Javascript代码异步执行的场景...
摘要:异步编程三座大山原型原型链作用域闭包同步异步。异步操作执行完毕后,再执行该回调函数,确保回调在异步操作之后执行。回调函数本身是我们约定俗成的一种叫法,我们定义它,但是并不会自己去执行它,它最终被其他人执行了。 JS异步编程 JS三座大山:原型原型链、作用域闭包、同步异步。之前有写过自己对闭包的理解,今天来总结一下JS中的异步。 思考(案例来自stackoverflow): functi...
摘要:工作当中经常会用到,在此进行深入学习异步编程解决方案是异步编程的一种解决方案,比传统的解决方案回调函数和事件更合理和更强大。所有源码注释见学习笔记 工作当中经常会用到Promise,在此进行深入学习 异步编程解决方案 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了...
阅读 1369·2019-08-30 12:54
阅读 1847·2019-08-30 11:16
阅读 1589·2019-08-30 10:50
阅读 2423·2019-08-29 16:17
阅读 1247·2019-08-26 12:17
阅读 1360·2019-08-26 10:15
阅读 2373·2019-08-23 18:38
阅读 772·2019-08-23 17:50