完全理解回调函数

什么是回调函数?

在任何一个函数是「一等公民」的编程语言里,这都不会成为一个问题。简单说,回调函数就是传递给其他代码的函数实体或引用,但其内涵远没有这么简单。

回调函数本质上提供了一种与常规的上层调用下层代码相反的模式,使得底层代码也有机会反调高层的代码,这大大提升了代码的能力,也同时给工程化项目带来了新的问题和挑战。

回调函数也是事件驱动式编程的基础,使得程序不必像传统的流程驱动式编程那样亦步亦趋的向下进行,而是可以被动性的由外来事件来触发进行,这几乎是所有图形化编程最基础和标准的实现方式。

一个典型的回调函数的例子就是在各个语言中都很常见的排序接口(比如 C++ STL 中 sort 函数),它们几乎都允许用户自己提供一个定制化的「比较函数」,这个比较函数就是典型的回调函数,它将会在排序接口的内部被执行。正是由于这样的回调函数的存在,使得排序接口不再仅仅局限于自然排序,大大提高了代码和接口的重用性。

回调函数分为两种,一种是同步回调函数,另一种是异步回调函数。上述排序接口的回调函数就是同步回调函数,而在 Node.js 中常见的回调函数是异步回调函数。同步回调和异步回调都可以使得调用者(caller)不再简单依赖于被调用者(callee),使得二者在代码空间分布上解耦,而异步回调函数更是在运行时从时间上将二者解耦。

回调函数背后其实隐藏着「控制反转(IoC,Inversion of Control )」的编程哲学,或者说回调函数是实现 IoC 的最常见的手段。IoC 的核心思想是 “Don’t call me, I’ll call you”,也被叫作「好莱坞原则」,据说是好莱坞经纪人的口头禅。控制反转其实也很常见,一般的库(library)中有回调函数的地方就有控制反转,这种控制反转可能还是局部的,而 Web 开发中几乎肯定会用的框架(framework)则是把控制反转作用到了全局,它使得基于上的更高层开发者不用像命题作文一样从零开始创作,而是把它变成了一道填空题,你只需要在约定好的地方按照具体的业务需求填入相应的内容即可,整个程序的运转流程被牢牢地把控在框架手中。

这样的框架重用度非常高,经过不断地迭代,越通用的东西会越来越沉淀到底层,服务于更广泛的上层代码,而多亏了回调函数的存在,使得上层开发者也能将自己独特的业务需求植入其中。

从事件驱动式编程的角度来说,回调函数也是实现 OCP(Open Close Principle,开闭原则)的手段之一。「开闭」指的是「对扩展开放,对修改封闭」,它要求代码在尽量少修改的情况下还有足够好的扩展性。举例来说,如果需要实现一个通用的消息消费者,如何做到在添加新的消息类型时却不必修改主函数呢?答案可以是让每种消息类型传递自己特有的回调函数,消费者的主体不变,在合适的地方调用随同消息一起传递过来的回调函数即可。

回调函数也不是全然没有问题。

Node.js 天然的异步特性设计使得大多数接口都是异步的,自然也充满了各种各样的异步回调函数。在 Node.js 里,回调函数最大的问题不仅仅是代码书写上产生了 callback hell,更本质的问题是回调函数的调用得不到有效的控制。因为外部异步接口不都像框架一样是基本可信任的,所以你并不能保证别人会如何对待你传入的回调函数。有趣的是,异步流程控制协议 Promise 再一次利用控制反转解决了回调函数调用的「信任」问题。Promise 把本来由异步接口控制的回调函数调用权收归自己所有,所有的异步调用都是通过 Promise 这个「中介」来完成,而回调函数的管理和调用也由 Promise 来一手掌握。

Reference

相关阅读