回调函数是什么?

833

什么是回调函数?


13
您可以在此处找到有关回调函数的最佳解释:https://dev59.com/D2kw5IYBdhLWcg3w2-XH#9652434 - Fakher
5
这是我在YouTube上找到的最好的回调函数解释视频:youtube.com/watch?v=xHneyv38Jro - sss
7
在维基百科上有一篇不简单的解释,可是我们可以更简单地表达:使用回调原理就像给别人一张名片并告诉他:“如果你需要我,请回电话,号码在名片上。” 在编程中,一个函数会将自己的引用留给另一段代码,例如通过注册,其他代码会在适当时候使用这个引用来调用(回调)函数,例如在某个事件发生时。在这种情况下,回调也被称为事件处理程序 - mins
22个回答

817

由于该死的名称,开发人员常常对回调函数感到困惑。

回调函数是指一个函数:

  • 可以被另一个函数访问,并且
  • 在第一个函数完成后被调用

想象一下回调函数的工作原理,它就像是一个被传递给其他函数的函数,在这个函数之后"被调用"。

也许更好的名称应该是"在之后调用"的函数。

当我们想要在前一个事件完成时执行某个活动时,这种结构非常有用,特别是在异步行为中。

伪代码:

// A function which accepts another function as an argument
// (and will automatically invoke that function when it completes - note that there is no explicit call to callbackFunction)
funct printANumber(int number, funct callbackFunction) {
    printout("The number you provided is: " + number);
}

// a function which we will use in a driver function as a callback function
funct printFinishMessage() {
    printout("I have finished printing numbers.");
}

// Driver method
funct event() {
   printANumber(6, printFinishMessage);
}

如果调用event(),将会得到什么结果:

The number you provided is: 6
I have finished printing numbers.
这里的输出顺序很重要。由于回调函数是在之后调用的,所以"I have finished printing numbers"会最后打印出来,而不是第一个。
回调函数因其在指针语言中的使用而得名。如果您不使用其中之一,请不要过于纠结于名称"callback"。只需理解它是描述一个作为另一个方法参数提供的方法的名称,使得当调用父方法(无论什么条件,如单击按钮、定时器滴答等)并且其方法体完成时,回调函数随后被调用。
一些语言支持构造多个回调函数参数的机制,并根据父函数的完成方式调用它们(即在父函数成功完成时调用一个回调,在父函数抛出特定错误的情况下调用另一个回调等)。

35
你的例子很好,但我不明白为什么要用术语“回调”。什么时候会“回调”meaningOfLife呢? - CodyBugstein
5
你好,关于"once its parent method completes, the function which this argument represents is then called",如果将一个函数作为参数传递给另一个函数,但是在父函数的运行时期间从中间调用函数,例如parent(cb) {dostuff1(); cb(); dostuff2()},那么这个函数不被视为“回调”函数吗? - Max Yari
3
在我看来,这仍被视为回调函数。重要的是,父函数将以某种方式使用输入函数(即回调)。它可以在中间或最后被调用,或者在满足某个条件时被调用。 - Kamran Bigdely
14
谢谢你的询问。但是,在printANumber函数中,meaningOfLife方法在哪里被调用了呢? - BenKoshy
9
这完全不正确: "在第一个函数完成后自动调用"。回调函数甚至不必被执行,更不会自动执行。实际上,回调函数经常在父函数完成之前就已经完成了。我非常不喜欢人们将回调函数描述为稍后执行的函数。这对于学习它们的人来说非常令人困惑。简单地说,回调函数只是作为参数传递给其他函数的函数。这是全部。更好的解释应该包括解释为什么要使用回调函数而不是函数引用。 - Jordan
显示剩余10条评论

258

不透明定义

回调函数是你提供给另一段代码的函数,允许它被该代码调用。

人为例子

为什么想要这样做?假设有一个需要调用的服务。如果服务立即返回,您只需执行以下操作:

  1. 调用它
  2. 等待结果
  3. 在结果返回后继续执行

例如,假设该服务是阶乘函数。当您需要得到 5! 的值时,您将调用 factorial(5),以下步骤将发生:

  1. 当前执行位置被保存(在栈上,但这不重要)

  2. 执行被交给 factorial

  3. factorial 完成后,它将结果放在某个可以访问到的地方

  4. 执行回到 [1] 中的位置

现在假设由于你传递给 factorial 的数字很大,导致阶乘函数非常耗时,比如需要在某个超级计算机集群上运行 5 分钟才能返回结果。那么,您可以:

  1. 保持当前设计,把程序在你睡觉时运行,这样你不用一半时间盯着屏幕

  2. 让程序在 factorial 运行的同时做其他事情

如果您选择第二个选项,则回调函数可能适合您。

端到端设计

为了利用回调模式,您希望以以下方式调用 factorial

factorial(really_big_number, what_to_do_with_the_result)
第二个参数what_to_do_with_the_result是一个函数,您可以将其发送到factorial中,希望在返回结果之前factorial会调用该函数。 是的,这意味着factorial需要支持回调。 现在假设您想要将参数传递给回调函数。但由于您不会调用它,而是factorial会调用,因此factorial需要被编写以允许您传入参数,并在调用回调时将它们直接传递给它。它可能看起来像这样:
factorial (number, callback, params)
{
    result = number!   // i can make up operators in my pseudocode
    callback (result, params)
}

现在 factorial 允许此模式,您的回调函数可能如下所示:

<code>logIt (number, logger)
{
    logger.log(number)
}
</code>

您调用 factorial 的方式如下:

<code>factorial(42, logIt, logger)
</code>

如果你希望从 logIt 中返回一些内容怎么办?很抱歉,你做不到,因为 factorial 不会注意它。

那么,为什么 factorial 不能返回回调函数的返回值呢?

使其非阻塞

由于执行应该在 factorial 完成后移交给回调函数,因此它实际上不应该向其调用者返回任何内容。最理想的情况是,它会在另一个线程/进程/机器中启动工作并立即返回,以便您可以继续进行,可能像这样:

factorial(param_1, param_2, ...)
{
    new factorial_worker_task(param_1, param_2, ...);
    return;
}

现在这是一个“异步调用”,意味着当您调用它时,它会立即返回,但实际上它还没有完成其工作。因此,您需要机制来检查它,并在完成时获取其结果,这使得您的程序更加复杂。

顺便说一下,使用这种模式,factorial_worker_task可以异步启动回调并立即返回。

那么你该怎么办呢?

答案是要保持在回调模式中。每当您想要编写

a = f()
g(a)

如果要异步调用f函数,你应该编写:

f(g)

其中 g 作为回调函数被传递。

这将从根本上改变程序的流拓扑结构,需要一些时间来适应。

你的编程语言可以通过提供一种创建临时函数的方式来帮助你。在上面的代码中,函数 g 可能只是一个小小的函数,比如 print (2*a+1)。如果你的语言要求你定义这个函数,并给它一个完全不必要的名字和参数,那么如果你经常使用这种模式,你的生活将会变得不愉快。

如果你的语言允许你创建 lambda 表达式,那么你就会更好地解决问题。你将编写类似于以下内容的代码:

f( func(a) { print(2*a+1); })

如何传递回调函数

你应该如何将回调函数传递给factorial呢?其实,你可以采用多种方式:

  1. 如果被调用的函数在同一个进程中运行,可以传递一个函数指针。

  2. 或者,你想在程序中维护一个字典,记录每个函数的名称和指针。

  3. 也可能你的语言支持在函数内部定义另一个函数,比如lambda表达式!语言内部会创建某种对象并传递指针,但你不用担心这些细节。

  4. 或许你正在调用的函数是在完全不同的机器上运行的,而你是通过类似HTTP的网络协议来调用它的。这时候你可以将你的回调函数公开为可通过HTTP调用的函数,并传递其URL。

你可以根据具体情况选择使用哪种方法。

回调函数的最近兴起

在我们进入Web时代后,我们调用的服务通常都在网络上。我们经常无法控制那些服务,即我们没有编写它们,也不能保证它们的正常运行或性能表现。

但我们不能期望我们的程序在等待这些服务响应时阻塞。因此,服务提供商通常使用回调模式设计API。

JavaScript非常好地支持回调函数,例如lambda表达式和闭包。在前端和后端,JavaScript世界中有很多活动,甚至正在开发适用于移动设备的JavaScript平台。

随着我们不断前进,越来越多的人会编写异步代码,而理解这种方式将变得至关重要。


是的,我了解JavaScript和Ruby中的Lambda函数是如何工作的,以及Java 8中也有Lambda函数。但是旧版本的Java并没有使用Lambda函数,而是使用类来实现回调功能,我想知道这种回调方式是如何工作的。这仍然是比其他答案更优秀的答案。 - Daniel Viglione
不是每个作为函数的参数都是回调函数。它可能是迭代器函数、排序函数或其他许多东西。请参阅“回调”一词的词源学。 - mikemaccana
这样说使用回调函数是一种“远程过程调用”公平吗? - drlolly

115

维基百科上的回调页面对此解释得非常清楚:

在计算机编程中,回调是指传递作为参数给其他代码的可执行代码或可执行代码片段的引用。这允许低层软件层调用在高层定义的子程序(或函数)。


4
这也以另一种方式回答了问题。名词“callback”是已经“被召回”的东西,就像经历了关机的东西被关闭一样,用于登录的东西是登录。 - Anonymous
2
维基百科实际上在其宝库中拥有一些非常棒的编程内容。我总是觉得术语“回调”最好用短语“我要回调到…”来解释。 - Thomas
在javascriptissexy.com/...里有非常好的解释,我会在这里转发一下;回调函数是作为参数传递给另一个函数的函数,并且回调函数在该其他函数中被调用或执行。//请注意,click方法参数中的项目是一个函数,而不是一个变量。//该项目是回调函数$("#btn_1").click(function() { alert("Btn 1 Clicked"); });正如您在前面的例子中所看到的,我们将一个函数作为参数传递给click方法以便执行 - - MarcoZen
@CodyBugstein 我认为这个问题非常简单,而且维基百科页面在第一段就解释了这个概念,没有必要写更长的答案。例如,这里的最佳答案只是用不同的措辞表达了维基百科第一段所说的内容,对我来说伪代码并没有展示维基百科例子中没有的任何东西。DRY ;-) - danio
这很有趣,因为这个答案引用了维基百科,而维基百科又引用了8bitjunkie在Stack Overflow上的回答。 - alain.janinm

55

类比简单解释

每天,我去上班。老板告诉我:

哦,当你完成那个任务后,我还有一个额外的任务给你:

太好了。他递给我一张纸条上面写着一个任务 - 这个任务是一个回调函数。它可以是任何东西:

ben.doWork(完成后洗我的车)

明天可能是:

ben.doWork(完成后告诉我我有多棒)

关键点在于回调必须在我完成工作之后进行!你最好阅读上面其他答案中包含的代码,我就不再重复了。


53

简单来说,这是一个由用户或浏览器在某些事件发生或处理完某些代码后调用而非由您自己调用的函数。


45

回调函数是一种在特定条件满足时应该调用的函数。与立即被调用不同,回调函数在将来的某个时间点被调用。

通常在启动异步任务(即在调用函数返回后某个时间点完成)时使用。

例如,一个请求网页的函数可能要求其调用者提供回调函数,当网页下载完毕时将被调用。


在你的第一句话中,你说“...当条件满足时”,但我认为回调函数是在父函数执行完成后被调用的,并不依赖于条件(?)。 - Ojonugwa Jude Ochalifu
“特定条件”只是指它们通常会因为某种原因而被调用,而不是随机调用。如果程序员没有预料到,在父/创建者仍在执行时回调可能会被调用,这可能会导致竞争条件。 - Thomas Bratt

44

回调函数最易于通过电话系统来描述。函数调用类似于打电话给某人,问一个问题,得到答案,然后挂断电话;添加回调函数可以改变这个比喻,使你在问完问题后还要把你的姓名和电话号码告诉她,这样她就可以回电话给你并告诉你答案。

-- Paul Jakubik,《C++中的回调实现》


1
那么我的名字和数字是一个函数? - Koray Tugay
不,如果“回调”是一个好的名称,那就是类比。相反,你要求电话操作员打电话。结束了。 - gherson
我从这个美妙的答案中推断出,"回调"的行为是指回调函数终止并返回到父函数。我错了吗? - Abdel Aleem

35

我认为“回调函数”这个术语被错误地在很多地方使用。我的定义类似于:

回调函数是一种你将其传递给其他人并允许他们在某个时间点调用它的函数。

我认为人们只读了维基百科定义中的第一句话:

回调是对可执行代码的引用,或者是作为参数传递给其他代码的可执行代码片段。

我一直在使用许多API,看到了许多糟糕的例子。许多人倾向于将函数指针(对可执行代码的引用)或匿名函数(可执行代码片段)命名为“回调”,如果它们只是函数,为什么需要另一个名称呢?

实际上,只有维基百科定义中的第二句话揭示了回调函数和常规函数之间的区别:

这允许较低级别的软件层调用高级别层定义的子例程(或函数)。

因此,区别在于您要将函数传递给谁以及如何调用您传入的函数。如果您只定义一个函数并将其传递给另一个函数,并在该函数体中直接调用它,请不要称其为回调。定义说您传入的函数将由“较低级别”函数调用。

我希望人们能停止在模棱两可的情况下使用这个词,这只会使人们更加困惑而不是更好地理解。


2
你的回答很有道理...但我很难想象。你能给个例子吗? - CodyBugstein
3
@Zane Wong:在你之前的回复中,你写到:“定义中说,你传入的函数将会被‘低级别’函数调用。”请问什么是“低级别”函数?如果能给出一个例子就更好了。 - Viku
一个例子会更好。 - Yousuf Azad
1
我认为经典函数调用和回调风格之间的区别与依赖方向有关:如果模块A依赖(“使用”)模块B,A调用B的函数,则这不是回调。如果A将对其函数的引用传递给B,那么B调用A的函数,这就是回调:与模块依赖性相比,调用是向后进行的。 - XouDo
@ZaneXY 先生说:“如果你只是定义了一个函数并将其传递给另一个函数,并直接在该函数体中调用它,那么不要称其为回调。” 这难道不是同步回调的定义吗?也就是立即执行的回调函数? - Abdel Aleem

31

回调函数 vs 回调

回调是指在另一个函数执行完毕后要执行的函数——因此得名“回调”

什么是回调函数

  • 接受函数(即功能对象)为参数或返回函数的函数称为高阶函数。
  • 任何作为参数传递的函数都被称为回调函数。
  • 回调函数是作为参数传递给另一个函数(我们将其称为otherFunction)并在otherFunction内部调用(或执行)的函数。
    function action(x, y, callback) {
        return callback(x, y);
    }
    
    function multiplication(x, y) {
        return x * y;
    }
    
    function addition(x, y) {
        return x + y;
    }

    alert(action(10, 10, multiplication)); // output: 100

    alert(action(10, 10, addition)); // output: 20

在SOA中,回调允许插件模块从容器/环境中访问服务。

来源


2
回调函数本身不是高阶函数,而是传递给高阶函数的。 - danio

21

这使得回调听起来像是在方法末尾的返回语句。

我不确定它们就是这样。

我认为回调实际上是对函数的调用,作为另一个函数被调用并完成的结果。

我也认为回调旨在处理原始调用,在某种意义上是“嘿!你要求的那件事?我已经完成了 - 只是想让你知道 - 回到你这里了。”


1
对于回调函数和返回语句的疑问,我会给予一点赞。我曾经也会因此陷入困境,许多与我共事的毕业生也是如此。 - 8bitjunkie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接