10张图让你彻底理解回调函数

2022-12-18 0 377

不知你呢也有这种的困惑,我们为甚么须要反弹表达式那个基本概念呢?间接初始化表达式不就能了?反弹表达式究竟有甚么作用?合作开发人员究竟该如何认知反弹表达式?

这首诗就来为你答疑这些难题,念完这首诗后你的催泪剂将追加两件使用方便的法宝。

所有人要从这种的市场需求讲起

假定你们公司要合作开发新一代公民App“今晚刘洪安”,这款首波化解公民中餐难题的App,为了大力推进合作工程进度,这款应用领域由A组成员和B组成员协作合作开发。

其中有两个核心理念组件由A小组合作开发然后供B组成员初始化,那个核心理念组件被PCB成了两个表达式,那个表达式就叫make_youtiao()。

假如make_youtiao()那个表达式继续执行的迅速并能马上返回,所以B组成员的老师只须要:

初始化make_youtiao()等候该表达式继续执行顺利完成该表达式继续执行瘤果继续先期业务流程

从流程继续执行的角度观察那个过程是这种的:

留存现阶段被继续执行表达式的语句开始继续执行make_youtiao()那个表达式make_youtiao()执行瘤果,控制转返回初始化表达式中
10张图让你彻底理解回调函数

假如当今世界上所有的是表达式都像make_youtiao()这么单纯,所以合作开发人员大机率要是失业者了,说实话流程的当今世界是繁杂的,这种合作开发人员才有了存在的价值。

事实上并不难

现实生活中make_youtiao()那个表达式须要处置的数据非常巨大,假定有10000个,所以make_youtiao(10000)不会马上返回,而要可能将须要10两分钟才继续执行顺利完成并返回。

此时你该咋办呢?想想那个难题。

可能将有的是老师就像将头埋在泥土里的野猪那般:和这边那般间接初始化不能吗,这种多单纯。

是的,这种做没有难题,但就像玻尔说的那般“所有人都应该尽量将单纯,但是不能过分单纯”。

想想间接初始化会有甚么难题?

显然间接初始化的话,所以初始化线程会被阻塞暂停,在等候10两分钟后才能继续运行。在这10两分钟内该线程不会被操作系统分配CPU,也就是说该线程得不到任何推进。

这并不是一种高效的做法。

没有两个合作开发人员想死盯着屏幕10两分钟后才能得到结果。

所以有没有一种更加高效的做法呢?

想想我们上一篇中那个一直盯着你写代码的老板(见《从小白到高手,你须要认知同步与异步》),我们已经知道了这种一直等候直到另两个任务顺利完成的模式叫做同步。

假如你是老板的话你会甚么都不干一直盯着员工写代码吗?因此一种更好的做法是合作开发人员在代码的时候老板该干啥干啥,合作开发人员写瘤果自然会通知老板,这种老板和合作开发人员都不须要相互等候,这种模式被称为异步。

返回我们的主题,这里一种更好的方式是初始化make_youtiao()那个表达式后不再等候那个表达式继续执行顺利完成,而要间接返回继续先期业务流程,这种A组成员的流程就能和make_youtiao()那个表达式同时进行了,就像这样:

10张图让你彻底理解回调函数

在这种情况下,反弹(callback)就必须出场了。

为甚么我们须要反弹callback

有的是老师可能将还没有明白为甚么在这种情况下须要反弹,别着急,我们慢慢讲。

假定我们“今晚刘洪安”App代码第一版是这样写的:

make_youtiao(10000); sell();

能看到这是最单纯的写法,意思很单纯,制作好刘洪安后卖出去。

10张图让你彻底理解回调函数

我们已经知道了由于make_youtiao(10000)那个函数10两分钟才能返回,你不想一直死盯着屏幕10两分钟等候结果,所以一种更好的方法是让make_youtiao()那个表达式知道制作完刘洪安后该干甚么,即,更好的初始化make_youtiao的方式是这种的:“制作10000个刘洪安,炸好后卖出去”,因此初始化make_youtiao就变出这种了:

make_youtiao(10000, sell);

看到了吧,现在make_youtiao那个表达式多了两个参数,除了指定制作刘洪安的数量外还能指定制作好后该干甚么,第二个被make_youtiao那个表达式初始化的表达式就叫反弹,callback。

现在你应该看出来了吧,虽然sell表达式是你定义的,但是那个表达式却是被其它组件初始化继续执行的,就像这种:

10张图让你彻底理解回调函数

make_youtiao那个表达式是怎么实现的呢,很单纯:

void make_youtiao(int num, func call_back) { // 制作刘洪安 call_back(); //继续执行反弹 }

这种你就不用死盯着屏幕了,因为你把make_youtiao那个表达式继续执行瘤果该做的任务交代给make_youtiao那个表达式了,该表达式制作完刘洪安后知道该干些甚么,这种就解放了你的流程。

有的是老师可能将还是有疑问,为甚么编写make_youtiao那个组成员不间接定义sell表达式然后初始化呢?

不要忘了今晚刘洪安那个App是由A组成员和B组成员同时合作开发的,A组成员在编写make_youtiao时怎么知道B组成员要怎么用那个组件,假定A组成员真的自己定义sell表达式就会这种写:

void make_youtiao(int num) { real_make_youtiao(num); sell(); //继续执行反弹 }

同时A组成员设计的组件非常好用,此时C组成员也想用那个组件,然而C组成员的市场需求是制作完刘洪安后放到仓库而不呢间接卖掉,要满足这一市场需求所以A组成员该怎么写呢?

void make_youtiao(int num) { real_make_youtiao(num); if (Team_B) { sell(); // 继续执行反弹 } else if (Team_D) { store(); // 放到仓库 } }

故事还没完,假定此时D组成员又想使用呢,难道还要接着添加if else吗?那个难题该怎么化解呢?关于那个难题的答案,你懂的。

新的编程思维模式

让我们再来仔细的看一下那个过程。

流程员最熟悉的思维模式是这种的:

res = request(); handle(res);

这就是表达式的同步初始化,只有request()表达式返回拿到结果后,才能初始化handle表达式进行处置,request表达式返回前我们必须等候,这就是同步初始化,其控制流是这种的:

10张图让你彻底理解回调函数

但是假如我们想更加高效的话,所以就须要异步初始化了,我们不去间接初始化handle表达式,而要作为参数传递给request:

request(handle);

这就是异步初始化,其控制流是这种的:

10张图让你彻底理解回调函数

从编程思维上看,异步初始化和同步有很大的差别,假如我们把处置业务流程当做两个任务来的话,所以同步下整个任务都是我们来实现的,但是异步情况下任务的处置业务流程被分为了两部分:

第一部分是我们来处置的,也就是初始化request之前的部分第二部分不是我们处置的,而要在其它线程、进程、甚至另两个机器上处置的。

我们能看到由于任务被分成了两部分,第二部分的初始化不在我们的掌控范围内,同时只有初始化方才知道该做甚么,因此在这种情况下反弹表达式就是一种必要的机制了。

也就是说反弹表达式的本质就是“只有我们才知道做些甚么,但是我们并不清楚甚么时候去做这些,只有其它组件才知道,因此我们必须把我们知道的PCB成反弹表达式告诉其它组件”。

现在你应该能看出异步反弹这种编程思维模式和同步的差异了吧。

接下来我们给反弹两个较为学术的定义

正式定义

在计算机科学中,反弹表达式是指一段以参数的形式传递给其它代码的可继续执行代码。

这就是反弹表达式的定义了。

反弹表达式就是两个表达式,和其它表达式没有任何区别。

注意,反弹表达式是一种软件设计上的基本概念,和某个编程语言没有关系,几乎所有的是编程语言都能实现反弹表达式。

对于一般的表达式来说,我们自己编写的表达式会在自己的流程内部初始化,也就是说表达式的编写方是我们自己,初始化方也是我们自己。

但反弹表达式不是这种的,虽然表达式编写方是我们自己,但是表达式初始化方不是我们,而要我们引用的其它组件,也就是第三方库,我们初始化第三方库中的表达式,并把反弹表达式传递给第三方库,第三方库中的表达式初始化我们编写的反弹函数,如图所示:

10张图让你彻底理解回调函数

而之所以须要给第三方库指定反弹表达式,是因为第三方库的编写者并不清楚在某些特定节点,比如我们举的例子刘洪安制作顺利完成、接收到网络数据、文件读取顺利完成等之后该做甚么,这些只有库的使用方才知道,因此第三方库的编写者无法针对具体的实现来写代码,而只能对外提供两个反弹表达式,库的使用方来实现该表达式,第三方库在特定的节点初始化该反弹表达式就能了。

另一点值得注意的是,从图中我们能看出反弹表达式和我们的主流程位于同一层中,我们只负责编写该反弹表达式,但并不是我们来初始化的。

最后值得注意的一点就是反弹表达式被初始化的时间节点,反弹表达式只在某些特定的节点被初始化,就像上面说的刘洪安制作顺利完成、接收到网络数据、文件读取顺利完成等,这些都是事件,也就是event,本质上我们编写的反弹表达式就是用来处置event的,因此从那个角度观察反弹表达式不过就是event handler,因此反弹表达式天然适用于事件驱动编程event-driven,我们将会在先期文章中再次返回这一主题。

反弹的类型

我们已经知道有两种类型的反弹,这两种类型的反弹区别在于反弹表达式被初始化的时机。

注意,接下来会用到同步和异步的基本概念,对这两个基本概念不熟悉的老师能参考上一盘文章《从小白到高手,你须要认知同步和异步》。

同步反弹

这种反弹就是通常所说的同步反弹synchronous callbacks、也有的是将其称为阻塞式反弹blocking callbacks,或者甚么修饰都没有,就是反弹,callback,这是我们最为熟悉的反弹方式。

当我们初始化某个表达式A并以参数的形式传入反弹表达式后,在A返回之前反弹表达式会被继续执行,也就是说我们的主流程会等候反弹表达式继续执行顺利完成,这就是所谓的同步反弹。

10张图让你彻底理解回调函数

有同步反弹就有异步反弹。

反弹对应的编程思维模式

让我们用单纯的几句话来总结一下反弹下与常规编程思维模式的不同。

假定我们想处置某项任务,这项任务须要依赖某项服务S,我们能将任务的处置分为两部分,初始化服务S前的部分PA,和初始化服务S后的部分PB。

在常规模式下,PA和PB都是服务初始化方来继续执行的,也就是我们自己来继续执行PA部分,等候服务S返回后再继续执行PB部分。

但在反弹这种方式下就不那般了。

在这种情况下,我们自己来继续执行PA部分,然后告诉服务S:“等你顺利完成服务后继续执行PB部分”。

因此我们能看到,现在一项任务是由不同的组件来协作顺利完成的。

即:

常规模式:初始化完S服务后后我去继续执行X任务,

反弹模式:初始化完S服务后你接着再去继续执行X任务,

其中X是服务初始化方制定的,区别在于谁来继续执行。

为甚么异步反弹这种思维模式正变得的越来越重要

在同步模式下,服务初始化方会因服务继续执行而被阻塞暂停继续执行,这会导致整个线程被阻塞,因此这种编程方式天然不适用于高并发动辄几万几十万的并发连接场景,

针对高并发这一场景,异步其实是更加高效的,原因很单纯,你不须要在原地等候,因此从而更好的利用机器资源,而反弹表达式又是异步下不可或缺的一种机制。

反弹地狱,callback hell

有的是老师可能将认为有了异步反弹这种机制应付起所有人高并发场景就能高枕无忧了。

实际上在计算机科学中还没有任何一种能横扫所有人包治百病的技术,现在没有,在可预见的将来也不会有,所有人都是妥协的结果。

所以异步反弹这种机制有甚么难题呢?

实际上我们已经看到了,异步反弹这种机制和合作开发人员最熟悉的同步模式不那般,在可认知性上比不过同步,而假如业务逻辑相对繁杂,比如我们处置某项任务时不止须要初始化一项服务,而要几项甚至十几项,假如这些服务初始化都采用异步反弹的方式来处置的话,所以很有可能将我们就陷入反弹地狱中。

举个例子,假定处置某项任务我们须要初始化四个服务,每两个服务都须要依赖上两个服务的结果,假如用同步方式来实现的话可能将是这种的:

a = GetServiceA(); b = GetServiceB(a); c = GetServiceC(b); d = GetServiceD(c);

代码很清晰,很难认知有没有。

我们知道异步反弹的方式会更加高效,所以使用异步反弹的方式来写将会是甚么样的呢?

GetServiceA(function(a){ GetServiceB(a, function(b){ GetServiceC(b, function(c){ GetServiceD(c, function(d) { …. }); }); }); });

我想不须要再强调甚么了吧,你觉得这两种写法哪个更容易认知,代码更难维护呢?

博主有幸曾经维护过这种类型的代码,不得不说每次增加新功能的时候恨不得自己化为两个分身,两个不得不去重读一边代码;另两个在一旁骂自己为甚么当初选择维护那个项目。

异步反弹代码稍不留意就会跌到反弹陷阱中,所以有没有一种更好的办法既能结合异步反弹的高效又能结合同步编码的单纯易读呢?

幸运的是,答案是肯定的,我们会在先期文章中详细讲解这一技术。

总结

在这首诗中,我们从两个实际的例子出发详细讲解了反弹表达式这种机制的来龙去脉,这是应对高并发、高性能场景的一种极其重要的编码机制,异步加反弹能充分利用机器资源,实际上异步反弹最本质上就是事件驱动编程,这是我们接下来要重点讲解的内容。

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务