【问题标题】:Why use callback in JavaScript, what are its advantages?为什么在 JavaScript 中使用回调,它有什么优势?
【发布时间】:2021-07-22 05:09:10
【问题描述】:

谁能解释一下,为什么我们在 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 网站:

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

有人可以用一个例子解释一下吗?

【问题讨论】:

  • 一个类似的问题已经被问到这里也得到了很好的解释stackoverflow.com/questions/5485495/…
  • "但是它们可以通过使用普通函数来实现" 请告诉我一个异步 AJAX 处理程序,它处理 HTTP 响应而没有回调函数。
  • @Tomalak Geret'kal:我想你误解了我的意思。当我说“通常,我发现它在 AJAX 中使用”时,我的意思是我找到了使用回调的 AJAX 示例。除了需要回调的 AJAX 之外,我还想要其他示例。另外,在 AJAX 中,监听过程是如何进行的?
  • 另外,在 AJAX 中,监听过程是如何进行的?如何在用户发出另一个请求的同时监听请求? (链接没有解释这一点)。

标签: javascript jquery


【解决方案1】:

主浏览器进程是一个单线程事件循环。如果您在单线程事件循环中执行长时间运行的操作,则该进程会“阻塞”。这很糟糕,因为该进程在等待您的操作完成时停止处理其他事件。 '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();

【讨论】:

  • 我发现您的回答中的这句话最有帮助:“没有理由使用回调,除非您想将某些内容绑定到事件处理程序,或者您的操作可能会阻塞,因此需要异步编程接口。”
  • 这个答案对@EMI 很有帮助。我只有一个查询,这可以写成link 的回调吗? (请参阅链接中的 MakeNestedTree())
【解决方案2】:

不太像多线程...

当您需要等待主 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 是单线程的,但您永远不会用 waiting 绑定该线程。如果您的 JS 线程很忙,那应该只是因为它正在积极运行代码。因此,即使 JS 是单线程的,您的代码也很容易隐式保存任意数量的任意类型异步任务的状态。

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

【讨论】:

  • 解释得很好。感谢您分享您的知识。
【解决方案3】:

因为正在执行的 javascript 是异步的,因此如果您只是在发出异步请求后放置任何旧函数,它很可能会在原始请求完成之前被调用。原始请求将在它开始(发送出去)后立即返回,而不是完成。

如果您需要对异步请求的结果进行处理,或者将请求链接在一起等,您将需要一个回调来确保在上一步完成之前不会开始下一步。

【讨论】:

    【解决方案4】:

    回调在 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”非常相似,基本上表示“剩余的计算”。不同之处在于,延续代表整个计算的其余部分,而回调代表子计算的其余部分。延续是被称为“continuation passing style”(CPS)的整个编程风格的一部分。通过延续,您可以创建各种有趣的控制结构。例如,可以使用延续来实现异常和coroutines

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

    【讨论】:

    • * 现在每个人都必须试一试。
    【解决方案5】:

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

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

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

    由于您需要 AJAX 以外的示例,因此异步特性的另一个常见用途是用于动画。回调对于动画来说是必需的,因为它需要允许 UI 进行绘制。

    假设您想在 5 秒内将 div 向右 100px 设置动画。您的第一直觉可能会说创建一个循环并在其间休眠。但是 Javascript 中没有 sleep,即使有,它也会冻结 UI,因为它在休眠时不会发生任何事情。

    相反,您需要按照将位置增加 10 的方式执行一些操作,然后调用 setTimeout 500 毫秒并通过回调执行下一帧。这可能会以递归方式完成。

    另一个用途只是简单地将函数作为参数传递,尽管我不确定“回调”一词是否适合该用例。这就是您在示例中使用它的方式,some_function 可以与用作回调的各种函数一起重用,因此有点注入代码。

    【讨论】:

    • @Harke 用于类似目的,但是回调比多线程更不容易出错。例如,您永远不会在 Javascript 中出现竞争条件或死锁(尽管您可以有无限循环,这对用户来说与死锁相同,但从程序员的角度来看更容易调试)。
    • 回调的缺点是它需要一种不同于某些程序员习惯的思维模式(公平地说,正确的多线程也是如此)。使用回调时,您也确实无法访问正确的堆栈跟踪,因为它们是异步执行的。
    【解决方案6】:

    为什么在 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);
    

    在 Web 应用程序的后端,回调用于浏览器和服务器之间的异步数据传输。它们还被用作确保包含异步功能(例如数据库查询、网络调用、文件系统操作、从外部设备加载数据等)的后端 JS 脚本以所需顺序执行的原始手段。基本上,通过包含一个回调函数,该函数执行需要在异步过程之后立即完成的操作作为异步函数的参数,我们可以确保其正确排序 w.r.t。异步进程。回调函数的范围权限也有助于编码。

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

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

    【讨论】:

      【解决方案7】:

      回调本身,顾名思义(call-back)每当你需要在第一个函数执行后调用一个函数,在那些场景中我们需要使用回调。 基本上,当我们需要将第一个函数的结果用于另一个函数时,回调的使用可以帮助我们,当然我们可以直接使用它,但是如果第一个函数没有响应任何结果并且我们已经传递了它怎么办降低函数然后它会导致 undefined 并且稍后如果有人试图改变传递的值,它将导致错误,比如 can not mutate or assign value of 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 来使您的代码更加模块化,并且看起来更像同步方式

      【讨论】:

        【解决方案8】:

        另外一点是代码的可测试性。

        假设您有这种情况:

        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 工作的Repl。

        【讨论】:

          【解决方案9】:

          异步编程类似于多线程吗?

          是的。

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

          假设您有一个长时间运行的计算。这对于一个简单的 js 演示来说可能很难想象,但是想象一个长时间的解压程序,或者游戏中长时间运行的寻路算法等一些需要超过一秒的数值计算。

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

          【讨论】:

          • 这是不正确的,Javascript 是单线程的,因此如果您有一个计算密集型算法,则无法“在后台”执行此操作。但是,您可以将计算分成块并调用 setTimeout(0, function () { /*next chunk of code */});,这将让给 UI,但与简单地将计算抛到后台线程不同。
          • Here's an example 这说明了为什么这个答案不正确。使用持续时间为 10 毫秒的 setTimeout 会异步调用 long_running_calc 函数,该函数需要 5000 毫秒才能完成。紧接着,它在setTimeout 中调用另一个快速函数,将执行延迟1000 毫秒。第二个函数在 1000 毫秒后不运行,而是阻塞直到 long_running_calc 完成。这是由于 JavaScript 的单线程特性。 Web Worker 是在不同线程上操作的唯一方法。
          • 您所描述的内容仅适用于 HTML5 网络工作者,否则所有异步计算都在与主事件循环相同的线程中执行。
          猜你喜欢
          • 2010-09-06
          • 2016-10-27
          • 2019-10-23
          • 2013-04-08
          • 2023-02-05
          • 2010-09-19
          • 2012-12-19
          • 2010-09-08
          • 1970-01-01
          相关资源
          最近更新 更多