什麼是 debounce ?
debounce 是一種用於防止事件處理過於頻繁發生的技術,常用於處理使用者輸入,例如按鍵事件或搜尋框的輸入事件。
範例
以文字搜尋功能為例,使用者每次搜尋打的文字都會呼叫後端的 api 拿回資料,但通常使用者想搜尋的是最後打字的結果,而此時就適合使用 debounce 在一定時間過後才去真正的呼叫後端 api
以下面為例,搜尋框改變時,不會馬上觸發 onInput 方法,而是會等 500ms 後,如果搜尋框的文字沒有任何改變,才呼叫 onInput 方法
1 | <input type="text" onInput="debounceInput" /> |
1 | const onInput = (event) => { |
基礎版 - debounce
基礎的 debounce 是利用 setTimeout 延後 fn 函式的執行時間,等到 delay 時間過後才會實際呼叫 fn 函式執行
1 | function debounce(fn, delay = 500) { |
添加 this 版 - debounce
以上的部分是 debounce 主要的功用,拿來避免事件頻繁的觸發,但這篇文章想講的是在 debounce 中的 this 指向
如果查看 debounce 相關的教學文章,會發現有些文章的 debounce 是這樣寫的:
1 | function debounce(fn, delay = 500) { |
當使用 debounce 時如果沒有牽涉到 this 的指向,不論是 第一種基礎版 或是 第二種添加 this 版 的寫法都是一樣的,但如果牽扯到 this 的使用時就會有所差別
以下我們拿 vue2 的 option api 當作範例,來看看實際使用 debounce 時可能會遇到的 this 指向問題:
1 | <template> |
1 | <script> |
直接先講結論,寫法 1. 及 寫法 2. 不論是使用 基礎版 或是 添加 this 版 的 debounce,都是可以正常運作的
但 寫法 3. 如果搭配上 基礎版 - debounce 時,在 debounce 函式中沒有處理 this 的指向,this.onInput 中的 this 等於 undefined,因此也就沒有 onInput 方法可以呼叫,會直接報錯
1 | // 寫法 3. |
以下我們來看看這三種寫法,this 的指向有什麼樣的差別:
寫法 1.
第一種寫法 this.onInput 的 this 作用域是在 mounted 底下,所以 this 指向的是 vue instance,完全沒有問題
1 | mounted() { |
寫法 2.
第二種寫法,在 debounce 裡使用了箭頭函式
1 | mounted() { |
箭頭函式沒有自己的 this,在箭頭函式中的 this 指向就是在宣告它時的 this,也就是 mounted 中的 this
1 | mounted() { |
所以第 5 行的 this 一樣會指向 vue instance 沒有問題
寫法 3.
第三種寫法在 debounce 中使用了傳統的 function
1 | mounted() { |
所以接下來我們需要先釐清 debounce 中的 fn 函式如何被呼叫,才能夠知道 this.onInput 的 this 是指向誰
1 | mounted() { |
這裡我們回頭來看一開始寫的 基礎版 - debounce,可以看到 fn 在第 8 行被呼叫,而這裡呼叫 fn 的時候並沒有傳遞 this 的指向,因此在嚴格模式下 fn 函式中的 this 會是 undefined
1 | function debounce(fn, delay = 500) { |
1 | mounted() { |
Refs.
簡單呼叫 Callback Function (嚴謹模式)
那麼 添加 this 版 - debounce 中的 this 指向又是怎麼運作的呢?
在 debounce 函式中的第 5 行將 this 暫存到 self 變數裡,接著又在第 9 行利用 apply 將 fn 函式中的 this 指向 self,因此最後 fn 中 this 的指向將等同於第 5 行的 this
1 | function debounce(fn, delay = 500) { |
而 debounce 中第 5 行的 this 又是從哪裡來的呢?
這裡我們可以看到呼叫 debounce 時指向的 this 是由 this.debounceInput 中的 this 來的,也就是 vue instance
1 | mounted() { |
小結
this.debounceInput 中的 this 會傳遞到 debounce 函式第 5 行中的 this,this 賦值給 self 後又會經由第 9 行的 fn.apply 將 this 指向傳遞到 fn 中,因此最後 fn 中的 this.onInput 中的 this 也就等同於 this.debounceInput 中的 this
因此如果丟入 debounce 中的 fn 函式是採用傳統 function 的第三種寫法,這時 debounce 函式就必須正確的傳遞 this 指向 (添加 this 版 - debounce),才能夠使得 fn 函式中的 this 正常運作
Demo
結論
debounce 的教學文章有很多,但一直以來都不太懂為什麼有些文章使用 fn.apply 將 this 的指向傳入 fn 函式裡,而有些沒有。今天藉由 vue2 使用 debounce 的寫法,才瞭解傳遞 this 的指向是必要的
但現在的 vue3 或是 react16 後流行的 hooks 寫法,都幾乎不太會用到 this,因此可以放心不會遇到 寫法 3. 的這種 bug 了
參考資料
Day 21:箭頭函數 (Arrow Functions) 的 this 和你想的不一樣 (1)
淺談 JavaScript 頭號難題 this:絕對不完整,但保證好懂
What’s THIS in JavaScript ? [上]