你真的了解回调?

9,980 阅读10分钟

前言

本文首发于微信公众号平台(itclancoder),如果你想阅读体验更好,可以戳后链接你真的了解回调?

你将在本文中,学习到什么是回调,回调是一种异步操作手段,在平时的使用当中无处不在,究竟如何确定何时使用异步(跳跃式执行,稍后响应,发送一个请求,不等待返回,随时可以再发送下一个请求,例如订餐拿号等饭,发广播,QQ,微信等聊天)还是同步(顺序执行,逐行读取代码,会影响后续的功能代码,也就是发送一个请求,等待返回,然后再发送下一个请求,比如打电话,需要等到你女票回话了,才能继续下面虐狗情节),回调的重要不言而喻,然而当面试时,让你举例出哪些异步回调时,好像除了回答一个Ajax,貌似就再也难以举例了的,本文会让你认识不一样的回调,文若有误导地方,欢迎路过的老师多提意见和指正

开始

如果您想了解如何使用node,这是了解最重要的主题。几乎node中的所有内容都使用回调函数。它们不是由node发明的,它们只是JavaScript语言的一部分

回调函数是异步执行或稍后执行的函数。程序不是从顶部到底部读取代码,而是异步程序可以根据先前的功能(如http请求或文件系统读取)发生的顺序和速度,在不同的时间执行不同的功能

由于确定一个函数是否为异步,区别可能会让人困惑,这取决于上下文。这是一个简单的同步示例,这意味着你可以像书本一样从顶部到底部阅读代码

var myNumber = 1
// 声明定义一个功能函数,define the function
function addOne() { 
    myNumber++; 
} 
addOne() // 调用函数,run the function
console.log(myNumber) // 2 logs out 2

这里的代码定义了一个函数,然后在下一行调用该函数,而不用等待任何东西。当函数被调用时,它立即将数字加1,所以我们可以预期,在我们调用函数后,数字应该是2.这是对同步代码的期望 - 它从头到尾依次运行

但是,Node主要使用异步代码。让我们使用node从名为number.txt的文件中读取我们的号码:

    var fs = require('fs') // 引入文件 

    var myNumber = undefined 

    // 声明一个函数

    function addOne() {

    fs.readFile('number.txt', function doneReading(err, fileContents) {

       myNumber = parseInt(fileContents);

       myNumber++;

     })

    }
    addOne(); // 调用函数
    console.log(myNumber) // 未定义 - 此行在readFile完成之前运行 logs out undefined日志输出 --这行代码在fs.readfile之前运行,this line gets run before readFile is done

运行上面的代码

以下是在Node中设置代码断点调试
为什么我们这次打印输出时会变得不确定?在这段代码中,我们使用了fs.readFile方法,它恰好是一个异步方法。通常情况下,必须与硬盘驱动器或网络进行通信的操作将是异步的。如果他们只需要访问内存中的东西或者在CPU上做一些工作,它们就会是同步的。其原因是,I / O真的很慢。大概数字是与硬盘驱动器通信比谈内存(例如RAM)慢大约10万倍

当我们运行这个程序时,所有的功能都立即被定义,但是并不是全部立即执行。这是了解异步编程的基本知识。当addOne被调用时,它会启动一个readFile,然后继续下一个准备执行的事情。如果没有什么要执行,节点将等待未完成的fs / network操作完成,否则它将停止运行并退出命令行

当读取完成文件(这可能需要几毫秒到几秒钟到几分钟,取决于硬盘的速度),它将运行doneReading函数,并给它一个错误(如果有错误)和文件内容

我们上面未定义的原因是我们的代码中没有任何逻辑告诉console.log语句等到readFile语句完成后才打印出数字

如果你想要一次又一次地执行或稍后执行一些代码,则第一步是将该代码放入函数中。然后,只要你想运行你的代码,你就可以调用这个函数。它有助于给你的功能描述性名称

回调只是稍后执行的函数。了解回调的关键是要意识到,当你不知道何时会完成一些异步操作时会使用它们,但是你确实知道操作将完成的位置 - 异步函数的最后一行!你声明回调的从上到下的顺序并不一定重要,只有逻辑/层次嵌套。首先将代码分解为函数,然后使用回调声明一个函数是否依赖于另一个函数完成

fs.readFile方法由node提供,是异步的,需要很长时间才能完成。考虑它的作用:它必须转到操作系统,而操作系统又必须转到文件系统,该文件系统位于可能或不可能以每分钟数千转的速度旋转的硬盘驱动器上。然后,它必须使用磁头读取数据,并通过层将其发送回你的JavaScript程序。给readFile一个函数(称为回调函数),它将在从文件系统中检索到数据后调用它。它将检索到的数据放入JavaScript变量中,并用该变量调用函数(回调函数)。在这种情况下,该变量称为fileContents,因为它包含读取的文件的内容

想一想餐厅示例。在许多餐馆里,当你等待你的食物时,你会得到一个号码放在你的桌子上。这些很像回调。他们告诉服务器你的芝士汉堡完成后该做什么

让我们将我们的console.log语句放入一个函数中,并将其作为回调传入

var fs = require('fs')
var myNumber = undefined
function addOne(callback) {
  fs.readFile('number.txt', function doneReading(err, fileContents) {
    myNumber = parseInt(fileContents)
    myNumber++
    callback()
  })
}
function logMyNumber() {
  console.log(myNumber)
}
addOne(logMyNumber)

现在,logMyNumber函数可以作为一个参数传入,该参数将成为addOne函数内部的回调变量。 readFile完成后,将调用回调变量(callback())。只有函数可以被调用,所以如果你传入除函数以外的任何东西,它将会导致错误

当一个函数被javascript调用时,该函数中的代码将立即执行。在这种情况下,我们的日志语句将执行,因为回调实际上是logMyNumber。请记住,仅仅因为你定义了一个函数并不意味着它会被执行。你必须调用一个函数来实现

为了更好地分解这个例子,下面是我们运行这个程序时发生的事件的时间表

  1. 代码被解析,这意味着如果有任何语法错误,他们会使程序中断。在这个初始阶段,fs和myNumber被声明为变量,而addOne和logMyNumber被声明为函数。请注意,这些只是声明。这两个函数都没有被调用或调用
  2. 当我们的程序的最后一行被执行时,addOne被调用,其logMyNumber函数作为其回调参数被传递。调用addOne将首先运行异步fs.readFile函数。该计划的这一部分需要一段时间才能完成
  3. 由于它等待readFile完成,因此无需执行任何操作,node闲置一段时间。如果在此期间还有其他事情要做,node将可用于工作
  4. 只要readFile完成,它执行它的回调函数doneReading,它解析fileContents中的一个名为myNumber的整数,递增myNumber,然后立即调用addOne传入的函数(它的回调函数),logMyNumber

也许回调编程中最令人困惑的部分是函数如何只是可以存储在变量中并以不同名称传递的对象。给你的变量赋予简单和描述性的名字对于让你的代码可读是很重要的。一般来说,在node程序中,当你看到像回调或cb这样的变量时,你可以认为它是一个函数

你可能已经听说过'事件编程'或'事件循环'这两个术语。它们指的是readFile的实现方式。node首先调度readFile操作,然后等待readFile发送它已完成的事件。在等待node时可以去检查其他事情。在node内部有一个被分派但尚未报告的事物的列表,所以node一遍又一遍地循环查看列表是否完成。完成后,他们进行“处理”,例如任何依靠它们完成的回调都会被调用

这是上例的伪代码版本

function addOne(thenRunThisFunction) {
  waitAMinuteAsync(function waitedAMinute() {
    thenRunThisFunction()
  })
}

addOne(function thisGetsRunAfterAddOneFinishes() {})

想象一下你有3个异步函数a,b和c。每一个需要1分钟才能运行,并在完成后调用回调函数(在第一个参数中传递)。如果你想告诉node'开始运行a,然后在完成后运行b,然后在b完成后运行c',它看起来像这样

  a(function() {
    b(function() {
      c()
    })
  })

当这段代码被执行时,a会立即开始运行,然后一分钟后它会完成并调用b,然后一分钟后它会完成并调用c,最后3分钟后node将停止运行,因为没有更多事情要做。确实有更优雅的方法来编写上面的例子,但重点是如果你有代码需要等待其他异步代码完成,那么你可以通过将代码放在函数中来表达这种依赖性,这些函数可以作为回调函数传递

node的设计需要你非线性考虑。考虑这个操作列表

   read a file
   process that file

如果你想把它变成伪代码,你最终会得到这个结果

var file = readFile()
processFile(file)

这种线性(逐步,按顺序)的代码并不是node工作的方式。如果这段代码被执行,那么readFile和processFile都会在同一时间执行。这是没有意义的,因为readFile将需要一段时间才能完成。相反,你需要表示该processFile依赖于readFile完成。这正是回调的目的!由于JavaScript的工作方式,你可以用许多不同的方式编写这种依赖关系

    var fs = require('fs')
    fs.readFile('movie.mp4', finishedReading)

    function finishedReading(error, movieData) {
      if (error) return console.error(error)
      // do something with the movieData
    }

但是你也可以像这样构造代码,它仍然可以工作:

var fs = require('fs')

function finishedReading(error, movieData) {
  if (error) return console.error(error)
  // do something with the movieData
}

fs.readFile('movie.mp4', finishedReading)

甚至像这样

   var fs = require('fs')
  fs.readFile('movie.mp4', function finishedReading(error, movieData) {
  if (error) return console.error(error)
  // do something with the movieData
  })

原文阅读

总结

回调往往就意味着是异步,而异步就需要时间等待,也就是它是将来要发生,而不是现在立刻马上,它会稍后执行,它是使用JavaScript函数的一种约定俗成的称呼,往往字面上有些抽象变得难以捉摸,粗俗理解它就是定义声明函数的功能,只是它比较特殊,它必须得依赖另一个个函数执行,通常回调仅在进行I/O时使用,例如下载种子,阅读文件,与数据库交互等,对应的例子,事件绑定,委托,bind(),addEventListener(),on(),animate(),window.onload,以及setTimeout()等等,总之凡是某个功能需要在依赖某个函数下进行执行的都是回调,回它的好处是高效执行,同时做多项工作,当然,你听得最多的或许就是回调地狱,至于怎么避免避免回调地狱,下一节将为你揭晓...