你有無意間看到 "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 列表里。
命令在 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 的初始列表将会是:
定披萨,拖地和熨烫衣服!
这是,当执行到orderPizza
,JavaScript 知道需要等待披萨送来。因此,在把"等待披萨送来"加到等待列表中的同时会处理剩下的工作。
JavaScript 等待披萨到达。
当披萨送到时,按门铃会通知 JavaScript并做一个标记,当处理完其他杂事时,会去执行layTheTable
。
JavaScript 知道通过标记里的命令需要去执行layTheTable
。
然后,一旦处理完了其他的杂务,JavaScript 就会执行回调函数layTheTable
。
当其他一切都完成时, JavaScript 会将其放置。
这就是我的朋友,事件循环。你可以用事件循环中的实际关键字来替代我们的巴特勒类比来理解所有的事情。
Todo-list-> Call stack
Waiting-list-> Web apis
Mental note-> Event queue
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 版本里,还有一些新的解决回调地狱的方法,比如: promises
和 async/await
。但是,会在另一个话题中解析它们。
今天,我们学习了什么是回调,为什么会如此重要以及如何去使用它们。同时学习到了什么是回调地狱,以及如何避免。希望,回调不会让你感到害怕。
关于回调你还有其他的问题么?如果你有的话,请在下面留言,我会尽快回复你的。
相关免费学习推荐:js视频教程
以上是了解JS中的回調的詳細內容。更多資訊請關注PHP中文網其他相關文章!