为什么在JavaScript中使用回调,它的优点是什么?

时间:2011-08-15 20:33:10

标签: javascript jquery

有人可以解释一下,为什么我们在JavaScript中使用回调?我找到了示例,但它们可以通过使用普通函数来实现。使用它有什么好处?我得到了“如何”使用它的答案,而不是“为什么和何时”我们需要使用它。

通常,我发现它在AJAX中使用。在httpRequest.onreadystatechange上。这类似于Java的多线程吗?响应的听众如何以及在哪里? 异步编程是否类似于多线程?

在以下代码中,控制流程如何:

function some_function(arg1, arg2, callback) {
  var my_number = Math.ceil(Math.random() * (arg1 - arg2) + arg2);
  callback(my_number);
  some_different_function_not_callback(arg1);
}
some_function(5, 15, function(num) {
   console.log("callback called! " + num);
});

来自JQuery网站:

  

关于回调的特殊之处在于,“父”之后出现的函数可以在回调执行之前执行“(ref:http://docs.jquery.com/Tutorials:How_jQuery_Works

有人可以用一个例子向我解释这一行吗?

10 个答案:

答案 0 :(得分:20)

主浏览器进程是单线程事件循环。如果在单线程事件循环中执行长时间运行的操作,则该进程会“阻塞”。这很糟糕,因为进程在等待操作完成时停止处理其他事件。 'alert'是为数不多的阻止浏览器方法之一:如果您调用alert('test'),则无法再单击链接,执行Ajax查询或与浏览器UI交互。

为了防止长时间运行的阻塞,XMLHttpRequest提供了一个异步接口。你在操作完成后传递一个回调函数来运行,当它正在处理时,它会控制回主事件循环而不是阻塞。

没有理由使用回调,除非您想要将某些东西绑定到事件处理程序,或者您的操作可能阻塞,因此需要异步编程接口。

This is an excellent video discussing more about the event loop used in the browser, as well as on the server side in node.js.

编辑:来自jQuery文档的那个错综复杂的行只是意味着回调在控件被转回主事件循环时异步执行。

parent_function(function () { console.log('Callback'); });
parent_doesnt_block(); // <-- function appears after "parent"
therefore_execution_continues();
// Maybe we get 'Callback' in the console here? or maybe later...
execution_still_continues();

答案 1 :(得分:7)

不太喜欢多线程......

只要您需要等待主JS代码外部的某些内容,就可以使用回调。在浏览器中,这用于AJAX,在node.js中,它用于调用系统的每一件事(文件访问,网络访问,数据库请求等)。

假设您想在每次用户单击按钮时触发ajax请求。现在让我们说ajax请求需要10秒才能完成。然后用户在10秒钟之前点击其中的10个按钮。这会反复调用这样的函数:

var clicked = function() {
  doAjax('/some/path.json', function(result) {
    updatePageWith(result.widgets);
  });
};

这会在JS引擎中运行代码的时间足以产生请求。然后它在等待时空转。其他JS可以在这一点上运行,UI完全流畅和互动,一切都很棒。然后突然间,所有这10个请求立即解决。然后我们的回调被调用了10次,就像魔法一样。

这是有效的,因为每次调用clicked()时,我们都会创建一个新的函数对象,并将其传递给doAjax()函数。因此,内存中有10个唯一的回调函数对象,每个函数对象都绑定到doAjax()函数的特定请求。当请求返回时,它会找到关联的回调对象并调用它。

这里的巨大优势在于,尽管javascript是单线程的,但您永远不会将该线程与等待捆绑在一起。如果你的JS线程很忙,那应该只是因为它正在积极地运行代码。因此,即使JS是单线程的,您的代码隐含地保存任意数量的任何异步任务的状态也是微不足道的。

回调的同步方法通常用于不同的目的。喜欢听众或代表。就像告诉对象A在数据发生变化时回调一样。虽然不是严格的异步,但通常不会立即调用该回调。相反,它会在稍后响应事件的某种用户操作而被调用。

答案 2 :(得分:3)

因为正在执行的javascript是异步的,所以如果你在发出异步请求之后只放了任何旧函数,它可能会在原始请求完成之前被调用。

,原始请求会在BEGINS(发出)后立即返回。

如果您需要对异步请求的结果执行某些操作,或将请求链接在一起等,则需要回调以确保在上一步完成之前不会开始下一步。

答案 3 :(得分:3)

回调在JS中被广泛使用,特别是在jQuery中。

您需要使用回调的地方是语言无法协调的任何地方。协调意味着代码在它需要的数据准备好之前不会执行。同步呼叫是协调的;在子计算完成之前,调用不会返回。异步调用不会给你协调,所以你需要一个回调。

  • 事件驱动的计算可能不协调,因此事件处理程序是回调:

    <input id="a" /> <button id='add'>+</button> <input id="b" /> = <span id="c"></span>
    <script type="text/javascript">
      $('#add').click(
          function() {
              $('#c').text(parseInt($('#a').val()) + parseInt($('#b').val()));
          }
      );
    </script>
    

    在JS中,事件调度的协调(例如DOM dispatchEvent,jQuery trigger和IE fireEvent方法)未指定(nested DOM events除外)同步)。如果触发事件,则可以延迟处理程序并立即执行返回触发后的任何内容。事件处理程序通常是同步调用的,但它们不一定是。

  • JQuery effects通常会在完成后执行回调。动画函数是异步的,因此它们不会阻止脚本的其余部分。

回调对于定义某些计算的外部部分的函数也很有用,但是内部部分未定义。以下是一些例子:

  • 您可以使用回调来过滤集合:

    // get odd items
    $([0,1,2,3,4,5,6,7,8,9]).filter(function (i) {return this % 2;})
    // or:
    $.grep([0,1,2,3,4,5,6,7,8,9], function (x, i) {return x % 2;});
    
  • Array.sort接受回调,因此您可以定义元素的比较方式。

    [{name: 'foo', id: 1}, {name: 'bar', id: 5}, {name: 'baz', id: 3}]
        .sort(function (a,b) { 
            return a.name.localeCompare(b.name); 
        })
    

jQuery的一些DOM操作方法(例如appendprependwrap采用回调来构建基于jQuery方法提供的上下文的元素。您可以查看回调作为提供计算的内部部分或作为协调的问题:当外部计算开始时,构建新DOM元素所需的数据不可用;回调完成子计算以在数据时创建元素变得可用。

setTimeoutsetInterval都会在延迟后执行回调。

从版本1.5开始,jQuery提供deferred objects作为管理多个回调的方法,回调之间存在各种执行依赖性。

回调非常类似于“continuation”,这基本上意味着“其余的计算”。不同之处在于,继续表示计算的其余部分的全部,而回调表示子计算的其余部分。 Continuations是整个编程风格的一部分,称为“continuation passing style”(CPS)。通过continuation,您可以创建各种有趣的控制结构。例如,continuation可用于实现异常和coroutines

根据语言引擎的功能(特别是您需要tail call optimization),CPS可以提供​​更有效的方法。一些控制结构(例如协程)需要尾调用,否则你会得到堆栈溢出 *

答案 4 :(得分:2)

回调允许单线程操作(Javascript是单线程的)异步执行。

<击> 最明显的例子是AJAX调用,你有一个在AJAX请求完成后执行的回调。 AJAX请求可能需要一段时间,如果是正常的函数调用,则在请求加载时整个页面将被冻结(无法滚动,无法选择文本等)。

<击>

这是通过setTimeoutsetInterval实现的,divsleep将稍后调用的函数排入队列,同时允许其他代码在其间执行。因此,当您等待AJAX​​调用完成时,允许执行其他代码(包括浏览器更新)。

由于您需要AJAX之外的其他示例,因此异步性质的其他常见用法是动画。动画的回调必需,因为它需要允许UI绘制。

假设您希望在5秒内向右侧setTimeout 100px设置动画。你的第一直觉可能是创造一个循环并在两者之间睡觉。但Javascript中没有some_function,即使有,也会冻结用户界面,因为睡觉时不会发生任何事情。

相反,你需要按照将位置递增10的方式做一些事情,然后使用回调调用{{1}} 500 ms来执行下一帧。这可能是递归完成的。

另一个用途就是将函数作为参数传递,虽然我不确定术语“回调”是否适合该用例。这就是你在你的例子中使用它的方式,{{1}}可以用作回调的各种函数重用,所以注入代码。

答案 5 :(得分:1)

顾名思义,

回调本身(调用-回叫)在执行第一个函数后需要调用某个函数时使用,在这种情况下,我们需要使用回调。 基本上,当需要将第一个函数的结果用于另一个函数时,使用回调可以帮助我们,当然我们可以直接使用它,但是如果第一个函数没有响应任何结果并且我们已经传递了该结果,该怎么办降低功能,则结果将为undefined,稍后,如果尝试对传递的值进行突变,则会导致错误,例如无法突变或分配undefined。

function first(){
  //any call or request that is giving us result say abc
  function second(abc){
    //mutate abc, it can be either array, object or can be another function
  }
}

在这些情况下,我们需要使用回调,因为第一个函数将导致abc,但没人知道它真正需要多少时间。

除回调外,还应使用promise或async / await使您的代码更具模块化,并看起来更像是同步方式

答案 6 :(得分:0)

当前效果结束后执行回调函数。 更详细的示例here

答案 7 :(得分:0)

另一点是代码可测试性。

假设您有这种情况:

function pushToDatabase(data) {
  // do some API call...
}

function handleData(someData) {
  // some logic

  pushToDatabase(someData);
}

在进行单元测试时,您可能不想查看数据库中是否有新数据,而只是检查是否使用正确的数据调用了pushToDatabase函数,因此可以将其重构为:

function pushToDatabase(data) {
  // do some API call...
}

function handleData(someData, callback) {
  // some logic

  callback(someData);
}

,然后用handleData(someData, pushToDatabase)进行调用。

这可能是对Jest的测试:

const aModule = require('./aModule');

describe('aModule', () => {
  it('should push data to a database', () => {
    const mockFn = jest.fn();
    const myData = 'Hello!';

    aModule.handleData(myData, mockFn)

    expect(mockFn).toHaveBeenCalledWith(myData);
  });
});

Link才能正常工作。

答案 8 :(得分:0)

为什么在JavaScript中使用回调,其优点是什么?

回调函数基本上是传递给另一个函数的调用中的参数(命名或匿名函数)。 JavaScript语法允许我们将函数视为对象,因此可以毫无问题地将函数 name 传递为另一个函数的参数。但是我们也可以通过匿名函数的完整代码作为参数,例如在XMLHttpRequest响应处理程序中进行表单字段验证过程:

req.onreadystatechange = function()
    {
        if (req.readyState === 4 && req.status === 200)
        {
            if (req.responseText !== 'Valid')                                                   // Check for invalid field value ...
            {
                document.getElementById(field + 'err').value = req.responseText;                // ... and output detail to error field
                document.getElementById(field + 'err').style.color = 'red';                     // Field comment in red
                document.getElementById(field + '-errchk').innerHTML = '\u00D7';                // Cross mark opposite data field
                document.getElementById(field + '-errchk').style.color = 'red';                 // ... in red
                document.getElementById(field + '-errchk').style.fontWeight = 800;              // ... and bold
            }
            else
            {
                document.getElementById(field + 'err').value = '';                              // Clear error info field
                document.getElementById(field + 'err').style.color = 'green';                   // Field comment in green
                document.getElementById(field + '-errchk').innerHTML = '\u2713';                // Check mark opposite data field
                document.getElementById(field + '-errchk').style.color = 'green';               // ... in green
                document.getElementById(field + '-errchk').style.fontWeight = 800;              // ... and bold
            }
        }
    }

在JavaScript中,使用回调函数比使用非回调函数(即在另一个函数中调用的函数,该函数不将其作为参数)的优势在于回调函数的可访问数据范围,即范围

非回调函数只能访问自己的内部范围,自己的全局范围以及输入给它们的参数值。除此之外,它们没有单方面访问使用它们的任何代码块的范围。因此,如果我们想要一个非回调函数在调用它的代码中“看到”某些vars / consts / functions / collections,则需要在非回调函数的范围内移动这些声明。这总是有点麻烦,有时非常棘手。否则,我们可能需要重新定义非回调函数,以便将调用代码范围内的不可共享数据作为参数传递给它。当然,这会更改非回调函数的声明,并影响使用该函数的任何其他代码块。

回调函数既可以访问它们自己的范围,也可以访问调用它们的代码的范围,还可以访问调用代码的全局范围。 因此,回调函数更易于管理代码,特别是在大型JS应用程序中,在执行任务期间,数据跨多个模块边界传递。

JavaScript几乎从一开始就存在回调函数。 在Web应用程序的前端,您将看到它们用于事件侦听器之类的事情,例如,如果我们要删除悬停在网页日历的“下个月”箭头时显示的鼠标悬停文本, :

var mouseOut = function(e)      
{
    if (document.getElementById("right-info").innerHTML != "")
    {
        document.getElementById("right-info").innerHTML = "";   
    }
};

nextM.addEventListener('mouseout', mouseOut, false);

在后端,回调用于浏览器和服务器之间的异步数据传输。它们还被用作确保包括异步功能(例如数据库查询,网络调用,文件系统操作,从外部设备加载数据等)的后端JS脚本按所需顺序执行的原始方法。基本上,通过包含一个回调函数来作为异步函数的参数来执行需要在异步过程之后立即执行的操作,以确保其正确的排序顺序。异步过程。回调函数的作用域特权也对编码有所帮助。

如今很少使用后端的回调函数,因为现代JavaScript包含一个 Promise 对象,该对象以更清晰,更可扩展的方式封装了异步流程的处理。

但是知道它们是什么以及可以在哪里有效使用它们仍然是很好的。

答案 9 :(得分:-6)

  

异步编程是否类似于多线程?

是肯定的。

Javascript的异步模型提供了一种“在后台”工作的方法。

假设您有一个长时间运行的计算。对于一个简单的js演示,这可能很难想象,但想象一个长的解压缩例程,或游戏中长时间运行的路径查找算法等等。一些数值计算需要花费一秒多的时间。

通过直接调用该函数进行计算将工作,但它会在计算期间暂停浏览器UI。异步运行意味着浏览器UI保持响应,因为计算继续在后台线程上运行。当calc完成时,根据异步编程模式,该函数调用回调函数,通知应用层计算完成。