【譯】JavaScript中的Callbacks

你是否遇到過"callbacks"一詞,可是不知道這意味着什麼?彆着急。你不是一我的。許多JavaScript的新手發現回調也很難理解。javascript

儘管callbacks可能使人疑惑,可是你仍然須要完全瞭解它們,由於它們是JavaScript中的一個重要的概念。若是你不知道callbacks,你不可能走得很遠🙁。html

這就是今天的文章(要講的)!你將瞭解callbacks是什麼,爲何它們很重要,以及如何使用它們。😄前端

備註:你會在這篇文章中看到ES6箭頭函數。若是你不是很熟悉它們,我建議你在往下讀以前複習一下ES6這篇文章(只瞭解箭頭函數部分就能夠了)。java

callbacks是什麼?

callback是做爲稍後要執行的參數傳遞給另外一個函數的函數。(開發人員說你在執行函數時「調用」一個函數,這就是被命名爲回調函數的緣由)。git

它們在JavaScript中很常見,你可能本身潛意識的使用了它們而不知道它們被稱爲回調函數。es6

接受函數回調的一個示例是addEventLisnter:github

const button = document.querySelector('button')
button.addEventListener('click', function(e) {
  // Adds clicked class to button
  this.classList.add('clicked')
})
複製代碼

看不出是回調函數嗎?那麼,這種寫法怎樣?ajax

const button = document.querySelector('button')

// Function that adds 'clicked' class to the element
function clicked (e) {
  this.classList.add('clicked')
}

// Adds click function as a callback to the event listener
button.addEventListener('click', clicked)
複製代碼

在這裏,咱們告訴JavaScript監聽按鈕上的click事件。若是檢測到點擊,則JavaScript應觸發clicked函數。所以,在這種狀況下,clicked是回調函數,而addEventListener是一個接受回調的函數。編程

如今,你明白什麼是回調函數了嘛?:)json

咱們來看另一個例子。這一次,假設你但願經過過濾一組數據來獲取小於5的列表。在這裏,你將回調函數傳遞給filter函數:

const numbers = [3, 4, 10, 20]
const lesserThanFive = numbers.filter(num => num < 5)
複製代碼

如今,若是你想經過命名函數執行上面的代碼,則過濾函數將以下所示:

const numbers = [3, 4, 10, 20]
const getLessThanFive = num => num < 5

// Passing getLessThanFive function into filter
const lesserThanFive = numbers.filter(getLessThanFive)
複製代碼

在這種狀況下,getLessThanFive是回調函數。Array.filter是一個接受回調的函數。

如今明白爲何了吧?一旦你知道回調函數是什麼,它們就無處不在!

下面的示例向你展現如何編寫回調函數和接受回調的函數:

// Create a function that accepts another function as an argument
const callbackAcceptingFunction = (fn) => {
  // Calls the function with any required arguments
  return fn(1, 2, 3)
}

// Callback gets arguments from the above call
const callback = (arg1, arg2, arg3) => {
  return arg1 + arg2 + arg3
}

// Passing a callback into a callback accepting function
const result = callbackAcceptingFunction(callback)
console.log(result) // 6
複製代碼

請注意,當你將回調函數傳遞給另外一個函數時,你只傳遞該函數的引用(並無執行它,所以沒有括號()

const result = callbackAcceptingFunction(callback)
複製代碼

你只能在callbackAcceptingFunction中喚醒(調用)回調函數。執行此操做時,你能夠傳遞迴調函數可能須要的任意數量的參數:

const callbackAcceptingFunction = (fn) => {
  // Calls the callback with three args
  fn(1, 2, 3)
}
複製代碼

這些由callbackAcceptingFunction傳遞給回調函數的參數,而後再經過回調函數(執行):

// Callback gets arguments from callbackAcceptingFunction
const callback = (arg1, arg2, arg3) => {
  return arg1 + arg2 + arg3
}
複製代碼

這是回調的解剖。如今,你應該知道addEventListener包含一個event參數:)

// Now you know where this event object comes from! :)
button.addEventListener('click', (event) => {
  event.preventDefault()
})
複製代碼

唷!這是callbacks的基本思路!只須要記住其關鍵:將一個函數傳遞給另外一個函數,而後,你會想起我上面提到的機制。

旁註:這種傳遞函數的能力是一件很重要的事情。它是如此重要,以致於說JavaScript中的函數是高階函數。高階函數在編程範例中稱爲函數編程,是一件很重大的事情。

但這是另外一天的話題。如今,我確信你已經開始明白callbacks是什麼,以及它們是如何被使用的。可是爲何?你爲何須要callbacks呢?

爲何使用callbacks

回調函數以兩種不一樣的方式使用 -- 在同步函數和異步函數中。

同步函數中的回調

若是你的代碼從上到下,從左到右的方式順序執行,等待上一個代碼執行以後,再執行下一行代碼,則你的代碼是同步的

讓咱們看一個示例,以便更容易理解:

const addOne = (n) => n + 1
addOne(1) // 2
addOne(2) // 3
addOne(3) // 4
addOne(4) // 5
複製代碼

在上面的例子中,addOne(1)首先執行。一旦它執行完,addOne(2)開始執行。一旦addOne(2)執行完,addOne(3)執行。這個過程一直持續到最後一行代碼執行完畢。

當你但願將部分代碼與其它代碼輕鬆交換時,回調將用於同步函數。

因此,回到上面的Array.filter示例中,儘管咱們將數組過濾爲包含小於5的數組,但你能夠輕鬆地重用Array.filter來獲取大於10的數字數組:

const numbers = [3, 4, 10, 20]
const getLessThanFive = num => num < 5
const getMoreThanTen = num => num > 10

// Passing getLessThanFive function into filter
const lesserThanFive = numbers.filter(getLessThanFive)

// Passing getMoreThanTen function into filter
const moreThanTen = numbers.filter(getMoreThanTen)
複製代碼

這就是爲何你在同步函數中使用回調函數的緣由。如今,讓咱們繼續看看爲何咱們在異步函數中使用回調。

異步函數中的回調

這裏的異步意味着,若是JavaScript須要等待某些事情完成,它將在等待時執行給予它的其他任務。

異步函數的一個示例是setTimeout。它接受一個回調函數以便稍後執行:

// Calls the callback after 1 second
setTimeout(callback, 1000)
複製代碼

若是你給JavaScript另一個任務須要完成,讓咱們看看setTimeout是如何工做的:

const tenSecondsLater = _ = > console.log('10 seconds passed!')

setTimeout(tenSecondsLater, 10000)
console.log('Start!')
複製代碼

在上面的代碼中,JavaScript會執行setTimeout。而後,它會等待10秒,以後打印出"10 seconds passed!"的消息。

同時,在等待setTimeout10秒內完成時,JavaScript執行console.log("Start!")

因此,若是你(在控制檯上)打印上面的代碼,這就是你會看到的:

// What happens:
// > Start! (almost immediately)
// > 10 seconds passed! (after ten seconds)
複製代碼

啊~異步操做聽起來很複雜,不是嗎?但爲何咱們在JavaScript中頻繁使用它呢?

要了解爲何異步操做很重要呢?想象一下JavaScript是你家中的機器人助手。這個助手很是愚蠢。它一次只能作一件事。(此行爲被稱爲單線程)。

假設你告訴你的機器人助手爲你訂購一些披薩。但機器人是如此的愚蠢,在打電話給披薩店以後,機器人坐在你家門前,等待披薩送達。在此期間它沒法作任何其它事情。

你不能叫它去熨衣服,拖地或在等待(披薩到來)的時候作任何事情。(可能)你須要等20分鐘,直到披薩到來,它才願意作其餘事情...

此行爲稱爲阻塞。當你等待某些內容完成時,其餘操做將被阻止。

const orderPizza = flavour => {
  callPizzaShop(`I want a ${flavour} pizza`)
  waits20minsForPizzaToCome() // Nothing else can happen here
  bringPizzaToYou()
}

orderPizza('Hawaiian')

// These two only starts after orderPizza is completed
mopFloor()
ironClothes()
複製代碼

而阻止操做是一個無賴。🙁

爲何?

讓咱們把愚蠢的機器人助手放到瀏覽器的上下文中。想象一下,當單擊按鈕時,你告訴它更改按鈕的顏色。

這個愚蠢的機器人會作什麼?

它專一於按鈕,忽略全部命令,直到按鈕被點擊。同時,用戶沒法選擇任何其餘內容。看看它都在幹嗎了?這就是異步編程在JavaScript中如此重要的緣由。

可是,要真正瞭解異步操做期間發生的事情,咱們須要引入另一個東西 -- 事件循環。

事件循環

爲了設想事件循環,想象一下JavaScript是一個攜帶todo-list的管家。此列表包含你告訴它要作的全部事情。而後,JavaScript將按照你提供的順序逐個遍歷列表。

假設你給JavaScript下面五個命令:

const addOne = (n) => n + 1

addOne(1) // 2
addOne(2) // 3
addOne(3) // 4
addOne(4) // 5
addOne(5) // 6
複製代碼

這是JavaScript的待辦事項列表中出現的內容。

todo-list

相關命令在JavaScript待辦事項列表中同步出現。

除了todo-list以外,JavaScript還保留一個waiting-list來跟蹤它須要等待的事情。若是你告訴JavaScript訂購披薩,它會打電話給披薩店並在等候列表名單中添加「等待披薩到達」(的指令)。與此同時,它還會作了其餘已經在todo-list上的事情。

因此,想象下你有下面代碼:

const orderPizza (flavor, callback) {
  callPizzaShop(`I want a ${flavor} pizza`)

  // Note: these three lines is pseudo code, not actual JavaScript
  whenPizzaComesBack {
    callback()
  }
}

const layTheTable = _ => console.log('laying the table')

orderPizza('Hawaiian', layTheTable)
mopFloor()
ironClothes()
複製代碼

JavaScript的初始化todo-list以下:

initial todo-list

訂披薩,拖地和熨衣服!😄

而後,在執行orderPizza時,JavaScript知道它須要等待披薩送達。所以,它會在執行其他任務時,將「等待披薩送達」(的指令)添加到waiting list上。

waiting

JavaScript等待披薩到達

當披薩到達時,門鈴會通知JavaScript,當它完成其他雜務時。它會作個**心理記錄(mental note)**去執行layTheTable

mental-note

JavaScript知道它須要經過在其 mental note 中添加命令來執行layTheTable

而後,一旦完成其餘雜務,JavaScript就會執行回調函數layTheTable

lay-table

其餘全部內容完成後,JavaScript就會去佈置桌面(layTheTable)

個人朋友,這個就被稱爲事件循環。你可使用事件循環中的實際關鍵字替換咱們的管家,類比來理解全部的內容:

  • Todo-list -> Call stack
  • Waiting-list -> Web apis
  • Mental note -> Event queue

event-loop

JavaScript的事件循環

若是你有20分鐘的空餘時間,我強烈建議你觀看Philip Roberts 在JSconf中談論的事件循環。它將幫助你理解事件循環的細節。

厄...那麼,爲何callbacks那麼重要呢?

哦~咱們在事件循環繞了一大圈。咱們回正題吧😂。

以前,咱們提到若是JavaScript專一於按鈕並忽略全部其餘命令,那將是很差的。是吧?

經過異步回調,咱們能夠提早提供JavaScript指令而無需中止整個操做

如今,當你要求JavaScript查看點擊按鈕時,它會將「監聽按鈕」(指令)放入waiting list中並繼續進行雜務。當按鈕最終得到點擊時,JavaScript會激活回調,而後繼續執行。

如下是回調中的一些常見用法,用於告訴JavaScript要作什麼...

  1. 當事件觸發時(好比addEventListener
  2. 在AJAX調用後(好比jQuery.ajax
  3. 在讀/寫文件以後(好比fs.readFile
// Callbacks in event listeners
document.addEventListener(button, highlightTheButton)
document.removeEventListener(button, highlightTheButton)

// Callbacks in jQuery's ajax method
$.ajax('some-url', {
  success (data) { /* success callback */ },
  error (err) { /* error callback */}
});

// Callbacks in Node
fs.readFile('pathToDirectory', (err, data) => {
  if (err) throw err
  console.log(data)
})

// Callbacks in ExpressJS
app.get('/', (req, res) => res.sendFile(index.html))
複製代碼

這就是它(異步)的回調!😄

但願你清楚callbacks是什麼以及如今如何使用它們。在開始的時候,你不會建立不少回調,因此要專一於學習如何使用可用的回調函數。

如今,在咱們結束(本文)以前,讓咱們看一下開發人員(使用)回調的第一個問題 -- 回調地獄。

回調地獄

回調地獄是一種屢次回調相互嵌套的現象。當你執行依賴於先前異步活動的異步活動時,可能會發生這種狀況。這些嵌套的回調使代碼更難閱讀。

根據個人經驗,你只會在Node中看到回調地獄。在使用前端JavaScript時,你幾乎從不會遇到回調地獄。

下面是一個回調地獄的例子:

// Look at three layers of callback in this code!
app.get('/', function (req, res) {
  Users.findOne({ _id:req.body.id }, function (err, user) {
    if (user) {
      user.update({/* params to update */}, function (err, document) {
        res.json({user: document})
      })
    } else {
      user.create(req.body, function(err, document) {
        res.json({user: document})
      })
    }
  })
})
複製代碼

而如今,你有個挑戰 -- 嘗試一目瞭然地破譯上面的代碼。很難,不是嗎?難怪開發者在看到嵌套回調時會毛骨悚然。

克服回調地獄的一個解決方案是將回調函數分解爲更小的部分以減小嵌套代碼的數量:

const updateUser = (req, res) => {
  user.update({/* params to update */}, function () {
    if (err) throw err;
    return res.json(user)
  })
}

const createUser = (req, res, err, user) => {
  user.create(req.body, function(err, user) {
    res.json(user)
  })
}

app.get('/', function (req, res) {
  Users.findOne({ _id:req.body.id }, (err, user) => {
    if (err) throw err
    if (user) {
      updateUser(req, res)
    } else {
      createUser(req, res)
    }
  })
})
複製代碼

更容易閱讀了,是吧?

還有其餘解決方案來對抗新版JavaScript中的回調地獄 -- 好比promisesasync / await。可是,解釋它們是咱們另外一天的話題。

結語

今天,你瞭解到了回調是什麼,爲何它們在JavaScript中如此重要以及如何使用它們。你還學會了回調地獄和對抗它的方法。如今,但願callbakcs再也不嚇到你了😉。

你對回調還有任何疑問嗎?若是你有,請隨時在下面發表評論,我會盡快回復你的。【PS:本文譯文,若需做者解答疑問,請移步原做者文章下評論】

感謝閱讀。這篇文章是否幫助到你?若是有,我但願你考慮分享它。你可能會幫助到其餘人。很是感謝!

後話

原文:zellwk.com/blog/callba…

文章首發:github.com/reng99/blog…

下一篇文章關於 promises

更多內容:github.com/reng99/blog…

by the way, Happy International Workers' Day!

相關文章
相關標籤/搜索