你真的了解回调?

前言

本文首发于微信公众号平台(itclancoder),若是你想阅读体验更好,能够戳后连接你真的了解回调?javascript

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

开始

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

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

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

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操做完成,不然它将中止运行并退出命令行bash

当读取完成文件(这可能须要几毫秒到几秒钟到几分钟,取决于硬盘的速度),它将运行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()等等,总之凡是某个功能须要在依赖某个函数下进行执行的都是回调,回它的好处是高效执行,同时作多项工做,固然,你听得最多的或许就是回调地狱,至于怎么避免避免回调地狱,下一节将为你揭晓...

相关文章
相关标签/搜索