瞭解JS中的回撥

2020-10-21 21:00:39

你有無意中看到 "callback" 但並不知道其中的意思麼?不用擔心。不是隻有你一個人這樣。很多JavaScript 新手都難以理解回撥。

雖然回撥比較令人困惑,你仍然需要徹底的學習理解它們,因為它在 JavaScript 中是一個很關鍵的概念。如果你不知道回撥,那麼你無法走的長遠。

這就是今天這篇文章需要講解的!你將要學習什麼是回撥以及它們為什麼如此重要和怎麼去使用。

這篇文章中你會看到 ES6 裡的箭頭函數。如果你還不熟悉它們,我建議你先看看ES6 post。(只要閱讀箭頭函數部分)。

什麼是回撥?

回撥是一個函數,會作為一個引數傳遞到另一個函數中,並稍後去執行。(開發人員說在執行函數時呼叫另一個函數,這就是為什麼 callbacks 稱之為回撥的原因)。

它們在 JavaScript 中很常見,以至於你可能不知道它們是回撥函數的時候已經使用過它們。

一個可以接收回撥函數的例子是addEventLisnter

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

沒看出來這是個回撥?來看看下個例子。

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函數。所以,在這個例子中,當addEventListener函數接收一個回撥函數時,clicked是一個回撥。

現在知道回撥是什麼了麼?:)

我們來看看另外一個例子。這一次,我們假設你想過濾一個數位陣列來得到一個小於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()
})

唷!這是回撥的基本含義!只要記住關鍵字:將一個函數傳遞到另一個函數中,你將回想起上面提到的機制。

這種傳遞函數的能力是一個很大的事情。它是如此之大,以至於 JavaScript 中的函數都是高階函數。高階函數是函數語言程式設計正規化中非常重要的東西。

但我們現在並不討論這個話題。現在,我確信你已經知道了回撥以及如何使用了。但是,你為什麼需要使用回撥呢?

為什麼使用回撥?

回撥有二種不同的使用方式 - 在同步函數和在非同步函數中。

同步函數中的回撥

如果你的程式碼執行是一個從上到下,從做到右的方式,順序地,在下一行程式碼執行前會等到程式碼執行完成,那麼你的程式碼是同步的。

我們來看個例子,以便於更早的理解:

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!」。

同時,在等到10秒去執行setTimeout時,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 的5個命令如下:

const addOne = (n) => n + 1

addOne(1) // 2
addOne(2) // 3
addOne(3) // 4
addOne(4) // 5
addOne(5) // 6

這將會出現在 JavaScript 的todo 列表裡。

1.png

命令在 JavaScript 的 todo 列表裡同步顯示。

除了 todo 列表,JavaScript 還儲存了一個 waiting 列表,這個列表可以跟蹤需要等待的東西。如果你告訴 JavaScript 需要定披薩,它會給披薩店打電話,並把"等待披薩送來"加到等到列表裡。同時,它會做 todo 列表已經有的事情。

所以,想象一下有這樣的程式碼。

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 的初始列表將會是:

2.png

定披薩,拖地和熨燙衣服!

這是,當執行到orderPizza,JavaScript 知道需要等待披薩送來。因此,在把"等待披薩送來"加到等待列表中的同時會處理剩下的工作。

3.png

JavaScript 等待披薩到達。

當披薩送到時,按門鈴會通知 JavaScript並做一個標記,當處理完其他雜事時,會去執行layTheTable

4.png

JavaScript 知道通過標記裡的命令需要去執行layTheTable

然後,一旦處理完了其他的雜務,JavaScript 就會執行回撥函數layTheTable

5.png

當其他一切都完成時, JavaScript 會將其放置。

這就是我的朋友,事件迴圈。你可以用事件迴圈中的實際關鍵字來替代我們的巴特勒類比來理解所有的事情。

  • Todo-list-> Call stack

  • Waiting-list-> Web apis

  • Mental note-> Event queue

6.png

JavaScript 事件迴圈

如果你有20分鐘空閒時間的話,我強烈推薦你看Philip Roberts在 JSConf 上關於事件迴圈的演講。它會幫助你瞭解事件迴圈裡的細節。

為什麼回撥如此重要?

哦。我們在事件迴圈上轉了個大圈。現在我們回頭來看。

之前,我們提到如果 JavaScript 專注地盯著一個按鈕並忽略其他所有的命令,這是非常糟糕的。是吧?

通過非同步回撥,我們可以提前給 JavaScript 指令而不需要停止整個操作。

現在,當你讓 JavaScript 監聽一個按鈕的點選事件時,它將"監聽按鈕"放在等待列表裡,然後繼續做家務。當按鈕最終獲取到點選事件時,JavaScript 會啟用回撥,然後繼續執行

下面是一些常見的回撥函數,告訴 JavaScript 應該怎麼做:

  • 當事件被觸發(比如:addEventListener

  • Ajax 執行之後(比如:jQuery.ajax

  • 檔案讀寫之後(比如: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))

這就是回撥!

希望,你現在已經弄清楚了回撥是什麼並且怎麼去使用。在最開始的時候,你沒必要建立很多的回撥,更多的去專注於學習如何使用可用的回撥函數。

現在,在結束之前,我們來看看回撥的第一個問題 - 回撥地獄

回撥地獄

回撥地獄是在多個回撥巢狀出現時的一個現象。它發生在一個非同步回撥執行依賴上一個非同步回撥執行的時候。這些巢狀的回撥會導致程式碼非常難以理解。

在我的經驗裡,你只會在 Node.js 裡看到回撥地獄。當你的 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。但是,會在另一個話題中解析它們。

結語

今天,我們學習了什麼是回撥,為什麼會如此重要以及如何去使用它們。同時學習到了什麼是回撥地獄,以及如何避免。希望,回撥不會讓你感到害怕。

關於回撥你還有其他的問題麼?如果你有的話,請在下面留言,我會盡快回復你的。

相關免費學習推薦:

以上就是了解JS中的回撥的詳細內容,更多請關注TW511.COM其它相關文章!