不知道大家有没有这样的疑惑,为什么需要回调函数的概念呢?直接调用函数还不行吗?回调函数有什么作用?程序员如何理解回调函数?本文就是为您解答这些问题。阅读本文后,您将为自己的武器库增添一件强大的武器。一切都是从这样的需求开始的。假设你的公司要开发下一代国民APP“明天油条”,一款主要解决国民早餐问题的APP。为了加快开发进度,这个app由A组和B组共同开发,有一个核心模块由A组开发,然后由B组调用,这个核心模块被封装成一个函数make_youtiao()。如果make_youtiao()这个函数执行的很快,可以马上返回,B组的同学只需要:调用make_youtiao()等待函数执行完成函数执行完成后,继续后续流程从程序的角度执行,这个过程是这样的:保存当前执行函数的上下文,开始执行make_youtiao()。执行函数make_youtiao()后,控制权将转回调用函数。如果世界上所有的函数都像make_youtiao()这么简单,那程序员就很可能要失业了,幸好编程的世界是复杂的,让程序员有了存在的价值。现实并不容易。实际上,函数make_youtiao()需要处理大量的数据。假设有10000条数据,那么make_youtiao(10000)不会立即返回,可能需要10分钟完成返回。你在这一点上做什么?想想这个问题。可能有同学会问,不能像以前那样直接调用吗?这是多么简单。是的,这样做没有错,但正如爱因斯坦所说“一切都应该尽可能简单,但又不能太简单”。想想直接调用有什么问题?显然,如果直接调用,调用线程会被阻塞挂起,等待10分钟后才能继续运行。在这10分钟内,线程不会被操作系统分配CPU,也就是说线程不会得到任何进展。这不是一种有效的方法。没有程序员愿意在获得结果之前盯着屏幕10分钟。那么有没有更高效的方法呢?想想我们上一篇文章中一直盯着你写代码的老板(见《从小白到高手,你需要理解同步与异步》),我们已经知道这种等待另一个任务完成的模式称为同步。如果你是老板,你会什么都不做,一直盯着员工写代码吗?所以,更好的做法是,程序员在码字的时候,老板该干什么就干什么,程序员写完自然会通知老板。这样老板和程序员都不需要互相等待。这种模式称为异步。回到我们的话题,这里更好的做法是调用make_youtiao()函数,不等待函数完成,而是直接返回继续后续流程,这样A组的程序就可以和make_youtiao()function同时,像这样:在这种情况下,必须播放回调(callback)。为什么需要callback有些同学可能不明白为什么这种情况下需要callback,别着急,我们慢慢说。假设我们的“明天油条”App第一版代码是这样写的:make_youtiao(10000);卖();可以看到这是最简单的写法,意思很简单,做好的油条卖掉。我们已经知道,由于make_youtiao(10000)函数需要10分钟返回,你不想盯着屏幕10分钟等待结果,所以更好的方法是让make_youtiao()函数知道什么make_youtiao做完油条后要做的,也就是make_youtiao更好的调用方式是这样:“做10000个油条,炸了卖”,所以调用make_youtiao会变成这样:make_youtiao(10000,sell);看,现在make_youtiaothis函数多了一个参数。除了指定要制作的油条数量外,还可以指定制作完成后要做什么。make_youtiao函数调用的第二个函数叫做callback,回调。现在你应该看到了吧,虽然sell函数是你自己定义的,但是这个函数是被其他模块调用执行的,就像这样:make_youtiao这个函数是怎么实现的呢?很简单:voidmake_youtiao(intnum,funccall_back){//做油条call_back();//执行回调}这样就不用盯着屏幕了,因为你把任务交给了make_youtiao函数执行到make_youtiao函数后完成。函数做完油条后知道该干什么就可以释放你的程序了。可能有的同学还有疑问。为什么写make_youtiao的团队不直接定义sell函数然后调用呢?别忘了,明天油条这个App是A组和B组同时开发的。写make_youtiao的时候A组是怎么知道B组的?这个模块怎么用,假设A队真的自己定义了sell函数,会这样写:voidmake_youtiao(intnum){real_make_youtiao(num);sell();//执行回调}和A队设计的模块非常容易使用。当时C队也想用这个模块,但是C队的需求是把油条放到仓库里,而不是直接卖。满足这个需求,A组应该怎么写?voidmake_youtiao(intnum){real_make_youtiao(num);if(Team_B){sell();//执行回调}elseif(Team_D){store();//放入仓库}}故事还没结束,假设此时TeamD要再次使用它,是否继续Addifelse呢?这样A组的同学只需要维护make_youtiao函数就可以实现满载。显然这是一个非常糟糕的设计。所以你会看到,做好油条之后接下来做什么,并不是实现make_youtiao的A组应该关心的。显然只有调用make_youtiao函数的用户知道。因此,make_youtiao的A组可以通过回调函数将下一步的操作完全交给调用者。A组的同学只需要针对回调函数这个抽象概念进行编程,这样调用者就可以把油条做成不管是卖掉、入库、自己吃等等。可以为所欲为。A队的make_youtiao函数根本不需要做任何改动,因为A队是针对回调函数这个抽象概念来编程的。这就是回调的作用,当然这就是针对抽象而不是具体实现进行编程的思想的力量。面向对象中的多态性本质上是您用来针对抽象而不是实现进行编程的东西。异步回调的故事并没有就此结束。在上面的例子中,虽然我们使用了回调的概念,即调用者实现了回调函数,然后将该函数作为参数传递给其他模块进行调用。不过这里还有一个问题,就是make_youtiao函数的调用方式还是同步的,请参考同步和异步,也就是说调用者是这样实现的:make_youtiao(10000,sell);//make_youtiao函数返回所有如果我们做不到,我们可以看到调用者必须等待make_youtiao函数返回,然后才能继续后续过程。再看make_youtiao函数的实现:voidmake_youtiao(intnum,funccall_back){real_make_youtiao(num);call_back();//执行回调}看,既然要做10000个油条,需要10分钟make_youtiao函数去执行,也就是说即使我们使用回调,调用者也不需要关心油条做好后的后续流程,但是调用者还是会被调用阻塞10分钟,就是这个问题与同步调用。如果你真的理解了上一节,你应该能想到更好的方法。没错,那就是异步调用。反正做完油条之后的后续过程不是调用者应该关心的,也就是说调用者并不关心make_youtiao函数的返回值,所以更好的办法是把任务放在在另一个线程(进程)甚至另一台机器中制作油条。如果用线程实现,那么make_youtiao是这样实现的:voidmake_youtiao(intnum,funccall_back){//在新线程中执行处理逻辑create_thread(real_make_youtiao,num,call_back);}看,当我们调用make_youtiao时,它会立即返回,即使油条还没有真正开始制作,调用者也不需要等待油条制作过程,可以立即执行后处理:make_youtiao(10000,sell);//立即返回//此时执行后续流程调用者的后续流程可以和做油条同时进行。这是函数的异步调用。当然,这也是异步的效率。新的编程思维让我们仔细看看这个过程。程序员最熟悉的思维模式是这样的:调用某个函数,获取结果并对获取到的结果进行处理res=request();handle(res);这是函数的同步调用,只有request()函数返回得到结果后,才能调用handle函数进行处理。我们必须等待请求函数返回。这是一个同步调用,它的控制流程是这样的:但是如果我们想要更高效,那么我们就需要异步调用。我们不直接调用handle函数,而是作为参数传递给request:request(handle);我们不关心请求何时真正得到结果。这是请求应该关心的。我们只需要告诉请求得到结果后要做什么就可以了,这样请求函数就可以立即返回,而实际得到结果的处理可能是在另一个线程、进程,甚至是另一台机器上完成的。这就是异步调用,它的控制流程如下:从编程思维的角度来看,异步调用和同步是有很大区别的。如果我们把处理流程看成一个任务,那么同步下的整个任务就是我们自己实现的,但是异步情况下任务的处理流程分为两部分:第一部分是我们处理的,也就是调用请求之前的部分,第二部分不是我们处理的,而是在其他线程中,进程,甚至是处理过的另一台机器。我们可以看到,由于任务分为两部分,第二部分的调用是我们无法控制的,只有调用者知道该做什么,所以回调函数在这种情况下是一个必要的机制。也就是说,回调函数的本质是“只有我们知道该做什么,但我们不知道什么时候做,只有其他模块知道,所以我们必须把我们知道的封装成一个回调函数,告诉其他人模块”。现在你应该能看出异步回调和同步的编程思维模式的区别了。接下来,我们给回调一个更学术的定义。回调函数在计算机科学中的正式定义是指以参数形式传递给其他代码的一段可执行代码。这是回调函数的定义。回调函数只是一个函数,与其他函数没有区别。注意,回调函数是一种软件设计概念,与某种编程语言无关。几乎所有的编程语言都可以实现回调函数。对于一般的函数,我们自己写的函数在自己的程序内部会被调用,也就是说函数是我们自己写的,调用者也是我们自己。但是回调函数不是这样的。虽然函数编写者是我们自己,但是函数调用者不是我们,而是我们引用的其他模块,也就是第三方库。我们调用第三方库中的函数,并传递回调函数对于第三方库,第三方库中的函数调用我们写的回调函数,如图:回调函数需要的原因指定给第三方库是因为第三方库的编写者并不清楚具体的Nodes,比如我们的例子,做完油条之后要做什么,接收到网络数据,文件是什么read等,只有库的使用者知道这些,所以第三方库的编写者无法编写具体实现的代码。而是只能对外提供一个回调函数,库的使用者自己实现,第三方库在特定节点调用回调函数。还有一点值得注意的是,从图中我们可以看出,回调函数与我们的主程序位于同一层,我们只负责编写回调函数,而不是调用它。最后一点值得注意的是调用回调函数的时间节点。回调函数只在某些特定的节点被调用,比如上面说的油条完成,网络数据的接收,文件读取完成等,这些都是事件,也就是事件,本质上我们写的回调函数就是用来处理事件的,所以从这个角度来说,回调函数只是一个事件处理器,所以回调函数自然适合事件驱动编程事件驱动,我们将在后续文章中再次回到这个主题。回调的类型我们已经知道有两种类型的回调。这两种回调的区别在于调用回调函数的时机。注意,接下来会用到同步和异步的概念。对这两个概念不熟悉的同学可以参考之前的文章《从小白到高手,你需要理解同步和异步》。同步回调这种回调我们俗称同步回调,也有的叫阻塞回调,或者不作任何修饰,就是callback,callback,也就是我们最熟悉的回调方式。当我们调用一个函数A并传入回调函数作为参数时,回调函数会在A返回之前执行,也就是说我们的主程序会等待回调函数完成,也就是所谓的同步打回来。哪里有同步回调,哪里就有异步回调。异步回调不同于同步回调。当我们调用一个函数A,并传入回调函数作为参数时,A函数会立即返回,也就是说函数A不会阻塞我们的主程序,回调函数会在一段时间后被调用。这个时候我们的主程序可能正在忙于其他任务,而回调函数的执行是和我们主程序的运行同时进行的。由于我们的主程序和回调函数的执行是可以同时发生的,所以一般情况下,主程序的执行和回调函数的执行是位于不同的线程或者进程中。这就是所谓的asynchronouscallback,异步回调,有的资料叫它deferredcallbacks,名字很形象,延迟回调。从上面两张图我们也可以看出,异步回调比同步回调更能充分利用机器资源。原因是主程序在同步模式下会“偷懒”,因为其他函数的调用被阻塞,运行被挂起,但是异步调用就没有这个问题,主程序会一直运行下去。因此,异步回调在I/O操作中更为常见,天然适用于Web服务等高并发场景。回调对应的编程思维模式让我们用简单的几句话概括一下回调与常规编程思维模式的区别。假设我们要处理某个任务,需要依赖某个服务S,我们可以把任务的处理分成两部分,调用服务S之前的PA部分,调用服务S之后的PB部分。普通模式下,PA和PB都是由服务调用者执行的,也就是我们自己执行PA部分,等待服务S返回后再执行PB部分。但在回调方法上有所不同。在这种情况下,我们自己执行PA部分,然后告诉服务S:“完成服务后执行PB部分”。所以我们可以看到现在一个任务是由不同的模块协同完成的。即:普通模式:调用S服务后,我执行X任务,回调模式:调用S服务后,再执行X任务,其中X由服务调用者制定,区别在于谁来执行。为什么异步回调越来越重要在同步模式下,服务调用者会因为服务执行而被阻塞暂停执行,从而导致整个线程被阻塞,所以这种编程方式自然不适合高端应用有几万或者几十万的并发连接场景,对于高并发场景,异步其实效率更高,原因很简单,不需要原地等待,可以更好的利用机器资源,并且回调函数在异步机制下是必不可少的。Callbackhell,回调地狱有些同学可能会觉得有了异步回调的机制来应对所有高并发场景,就可以高枕无忧了。事实上,计算机科学中没有可以包治百病的技术,现在没有,在可预见的未来也没有,一切都是妥协的结果。那么异步回调的机制有什么问题呢?其实我们已经看到,异步回调的机制和程序员最熟悉的同步模式是不一样的。它不像同步那样易于理解。如果业务逻辑比较复杂,比如我们在处理某个任务的时候,需要调用的服务不只是一个,而是几个甚至十几个item。如果这些服务调用全部由异步回调来处理,那么我们很可能会陷入回调地狱。例如,假设我们需要调用四个服务来处理某个任务,每个服务都需要依赖前一个服务的结果。如果以同步方式实现,可能是这样: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){....});});});我不觉得没必要强调什么,你觉得这两种写法哪个更容易理解,代码更容易维护呢?博主有幸维护过这种代码,每次都不得不说加了个新功能,恨不得变成两个克隆人,一个要重读代码,一个骂自己当初为什么选择维护这个项目,异步回调代码会掉到回调里一不留神就会陷入陷阱,那么有没有更好的方法将异步回调的高效与同步编码的易读性结合起来呢?幸运的是,答案是肯定的,我们会在后续文章中详细讲解这项技术综上所述,在本文??中,我们从一个实际的例子中详细解释了回调函数机制的来龙去脉,h是处理高并发高性能场景的一种极其重要的编码机制,异步回调可以充分利用机器资源。其实异步回调本质上就是事件驱动编程,这也是我们接下来要重点讲的。
