前端條件競態(tài)亂談——可能被我誤解的函數(shù)防抖

前言

故事發(fā)生的時(shí)間距離今天其實(shí)已經(jīng)快一個月了,由于一些特(tuo)殊(yan)的原因,今天終于動筆開始寫(shui)下了這篇博文, 再不寫都?明年了

ChatGPT 想必是過去的一個月里相當(dāng)火熱的話題了,抱著一些好奇心,我也注冊并嘗試了搜索了一些內(nèi)容。讓我覺得相當(dāng)驚艷的是它可以通過不斷的反問來驗(yàn)證自己對于一些概念理解的正確性,十分 nice。

今天要聊的這部分內(nèi)容也是在使用 ChatGPT 過程中發(fā)現(xiàn)的,文章內(nèi)容較主觀,歡迎討論。

1. 起因

事情的起因是某一天我在群里提起 ChatGPT ,順手截了幾張圖,這個時(shí)候有一位群友 Billdex 提到搜一下 singleflight 。

我:GoLang 中的 Singleflight 是什么?

ChatGPT: Singleflight 是一個 Go 語言庫, 它提供了一種方法來管理并發(fā)的函數(shù)調(diào)用,并確保只有一個函數(shù)在同一時(shí)間被調(diào)用。它的目的是為了防止并發(fā)的函數(shù)調(diào)用之間的競爭條件,從而避免不必要的資源浪費(fèi)和性能問題。

我:它的實(shí)現(xiàn)原理是什么

ChatGPT: Singleflight 庫實(shí)現(xiàn)的原理是通過一個獨(dú)占鎖來控制對函數(shù)的并發(fā)調(diào)用。當(dāng)調(diào)用singleflight.Do() 函數(shù)時(shí),它會對函數(shù)進(jìn)行攔截,并將它們放到一個隊(duì)列中,并通過獨(dú)占鎖來確保只有一個函數(shù)在同一時(shí)間被調(diào)用。如果有其他的并發(fā)調(diào)用請求,它們會被掛起直到當(dāng)前的函數(shù)調(diào)用完成。這樣就可以避免多個函數(shù)在同一時(shí)間被調(diào)用,從而避免競爭條件的產(chǎn)生。

我對 GoLang 可謂一竅不通,本來這件事到這里就該結(jié)束了,隨后會淹沒在更多的消息記錄的海洋里。

但好巧不巧群里有個寫 CPP 的小伙伴 hajimeru 碰巧路過,于是群里出現(xiàn)了這樣一段對話:

hajimeru: 這是事件隊(duì)列的思路?

Billdex: 不太一樣,倒是有點(diǎn)類似于前端的接口防抖,用在后端做并發(fā)控制

如前面所說,我既不會 GoLang 對后端了解的也不多,但你要說前端的防抖函數(shù)那我可不困了,這段對話一下就勾起了我的好奇心。

2. 防抖函數(shù)

函數(shù)防抖是前端常用的優(yōu)化手段之一,如果一個函數(shù)在 n 秒內(nèi)被反復(fù)調(diào)用,函數(shù)防抖會重新計(jì)算函數(shù)的執(zhí)行時(shí)間以確保在指定的時(shí)間內(nèi)只執(zhí)行一次。

一個比較常見的例子是搜索框?qū)崿F(xiàn),從產(chǎn)品的角度來說,產(chǎn)品每多一步操作都可能導(dǎo)致用戶的流失,因而許多產(chǎn)品都將搜索設(shè)計(jì)為隨著用戶輸入自動響應(yīng)搜索結(jié)果的交互形式。

做前端的同學(xué)都知道,實(shí)現(xiàn)這樣的功能是一定少不了防抖函數(shù)的,通過監(jiān)聽搜索框的輸入事件進(jìn)行請求會產(chǎn)生多次無用的請求,而用戶往往只需要最后一次請求作為結(jié)果,這樣很容易帶來服務(wù)的性能浪費(fèi)。

于是在這里,我過往對于防抖函數(shù)的一個錯覺就產(chǎn)生了: 防抖函數(shù)的作用是不斷的推后函數(shù)執(zhí)行的時(shí)間直到超時(shí) 。

那么自然而然的,我也認(rèn)為它與 Singleflight 用來規(guī)避條件競爭不是一碼事。

3. 問問 AI 吧

我:可以認(rèn)為 Singleflight 避免函數(shù)調(diào)用條件競爭的策略與 UI 編程中的函數(shù)防抖類似嗎?

ChatGPT: 是的,Singleflight 避免函數(shù)調(diào)用條件競爭的策略與 UI 編程中的函數(shù)防抖類似。函數(shù)防抖也是一種控制函數(shù)調(diào)用的方法,它的目的是為了避免頻繁的函數(shù)調(diào)用,從而降低系統(tǒng)的性能。它的實(shí)現(xiàn)原理是通過設(shè)置一個定時(shí)器, 在函數(shù)調(diào)用之后延遲一段時(shí)間來執(zhí)行函數(shù)。如果在定時(shí)器超時(shí)前再次調(diào)用該函數(shù),定時(shí)器會被重置,直到超時(shí)時(shí)才真正執(zhí)行函數(shù)。這樣就可以避免頻繁的函數(shù)調(diào)用,提高系統(tǒng)的性能。Singleflight 庫也是通過類似的方法來避免函數(shù)調(diào)用條件競爭,它通過獨(dú)占鎖來控制函數(shù)調(diào)用,確保只有一個函數(shù)在同一時(shí)間被調(diào)用,從而避免競爭條件的產(chǎn)生。

在看到這個答案以后的最初幾秒里,我是詫異的。隨后我的腦子里閃現(xiàn)出另外一件事情,我忽然意識到我可能錯過了什么東西,進(jìn)一步的,我意識到過去對函數(shù)防抖的理解可能是片面的。

4. 條件競態(tài)

提到條件競態(tài),長期使用 JavaScript 的我總是覺得與我無瓜,畢竟 JavaScript 是單線程的語言,而我通常見到談?wù)摋l件競態(tài)的場景幾乎都是多線程場景。

隨便翻閱了幾篇某金,某乎,某SDN的文章,大家都在用一個共同的例子來演示條件競態(tài)的現(xiàn)象,兩個線程交替執(zhí)行的過程中,由于 執(zhí)行順序是不可預(yù)估的 ,且兩者 都會修改各自持有的內(nèi)存里的同一個變量 ,于是最后的代碼執(zhí)行結(jié)果與執(zhí)行順序一樣變成了不可預(yù)估的。

稍等一下。

我們都知道,JavaScript 的異步任務(wù)執(zhí)行時(shí)機(jī)是不可預(yù)估的,假設(shè)有如下代碼,回調(diào)函數(shù)一定會在 1000ms 后執(zhí)行:

setTimeout(() => console.log('run!'), 1000);

但如果位于它上方的代碼是這樣的呢:

while (true) {}

setTimeout(() => console.log('run!'), 1000);

恐怕這個 setTimeout 這輩子都不會執(zhí)行了。

放在網(wǎng)絡(luò)請求場合下,這件事情就變得更加沒譜了,按順序發(fā)送的 n 個請求響應(yīng)的時(shí)間是不可預(yù)估的。

這也許是一個大家都知道,但又比較容易被忽視的細(xì)節(jié):JavaScript 不僅有能力,還非常容易達(dá)成這種 執(zhí)行順序不可預(yù)估 的情況。

另一方面,JavaScript 數(shù)據(jù)區(qū)分引用類型和值類型。被常用來做為業(yè)務(wù)數(shù)據(jù)抽象的對象,數(shù)組等均屬于引用類型,以下代碼很好的說明了問題:

const obj = {
    name: 'yuchi',
};

const copy1 = obj;
copy1.name = 'xiaoming';

console.log(copy1); // { name: 'xiaoming' }
console.log(obj); // { name: 'xiaoming' }

接下來讓我們把這兩者組合在一起,當(dāng)多個 不可預(yù)估執(zhí)行順序 的異步任務(wù)同時(shí)在回調(diào)函數(shù)里修改同一個 引用類型 時(shí),會發(fā)生什么?

fetchA().then(res => {
  setData(res);
});

fetchB().then(res => {
  setData(res);
});

fetchC().then(res => {
  setData(res);
});

fetchD().then(res => {
  setData(res);
});

由于多次請求的執(zhí)行順序是不可預(yù)估的,最終 data 的值可能是四個請求中的任意一個,也可能是請求發(fā)送前的初始值(四個請求都失敗了)。

當(dāng)自動搜索的需求不添加防抖的策略時(shí),其執(zhí)行過程本質(zhì)上與上述代碼是幾乎一致的。

具體來說的表現(xiàn)為:

  1. 伴隨用戶每一次輸入,keyword: abc 最終產(chǎn)生了 3 次請求,我們分別標(biāo)記為 search('a') | search('ab') | search('abc') ,它們對應(yīng)的響應(yīng)回調(diào)函數(shù)我們標(biāo)記為 callback1 | callback2 | callback3
  2. 由于接口請求返回的順序不可預(yù)估,則最后一次執(zhí)行的回調(diào)函數(shù)可能是 callback1 | callback2 | callback3 中的任意一個。
  3. 變更 UI ,最終呈現(xiàn)出的檢索結(jié)果可能是 search('a') | search('ab') | search('abc') 中的任意一個。

發(fā)現(xiàn)了嗎,這和前面提到的多個線程交替執(zhí)行修改共享內(nèi)存的變量導(dǎo)致結(jié)果不可預(yù)估的例子如出一轍。

也就是說,單線程異步的 JavaScript 代碼 不僅可能出現(xiàn)條件競態(tài),還很容易 。

5. 再看防抖函數(shù)

回頭過來再看看防抖函數(shù),我忽然間意識到,不斷推遲函數(shù)執(zhí)行時(shí)機(jī)的防抖策略實(shí)際上也一同幫我解決了這種異步函數(shù)高頻觸發(fā)場景下的條件競態(tài)問題。

當(dāng)然,這并不意味著在前端的條件競態(tài)場景下,我們只能或應(yīng)該通過函數(shù)防抖來解決,具體場景有具體的解決方案。

它帶給我的啟發(fā)是:一直以來我可能都忽略了 防抖函數(shù)通過放棄抖動請求規(guī)避條件競態(tài) 的這一使用場景。

也就是說,ChatGPT 的回答和 Billdex 的理解是對的,這很有趣??。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容