你真的瞭解回調?

前言

本文首發於微信公衆號平臺(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()等等,總之凡是某個功能須要在依賴某個函數下進行執行的都是回調,回它的好處是高效執行,同時作多項工做,固然,你聽得最多的或許就是回調地獄,至於怎麼避免避免回調地獄,下一節將爲你揭曉...

相關文章
相關標籤/搜索