競態問題與RxJs

競態問題與RxJs

競態問題通常指的是在多線程的編程中,輸入了相同的條件,但是會輸出不確定的結果的情況。雖然Js是單線程語言,但由於引入了異步編程,所以也會存在競態的問題,而使用RxJs通常就可以解決這個問題,其使得編寫異步或基於回調的代碼更容易。

競態問題

前邊提到了競態問題通常指的是在多線程的編程中,輸入了相同的條件,但是會輸出不確定的結果的情況。發生這種情況的主要原因是,當多個線程都對一個共享變量有讀取-修改的操作時,在某個線程讀取共享變量之後,進行相關操作的時候,別的線程把這個變量給改了,從而導致結果出現了錯誤。在這裏的多個線程中,起碼有一個線程有更新操作,如果所有的線程都是讀操作,那麼就不存在什麼競態條件。總體來說,最低是需要thread1#load - thread2#update這種的模式,當其中一個線程進行更新共享變量操作的時候,另一個線程不管是讀取變量還是更新變量都容易出現錯誤,要麼讀取髒數據,要麼丟失更新結果,通常會使用加鎖或者原子操作的方式來消除競態的影響。
回到Js當中,雖然Js是單線程語言,但由於引入了異步編程,所以也會存在競態的問題。舉一個簡單的例子,我們經常會發起網絡請求,假如我們此時需要發起網絡請求展示數據,輸入A時彈出B,輸入B時彈出C,要注意反悔的數據都是需要通過網絡發起請求來得到的,假設此時我們快速的輸入了A又快速輸入了B,如果網絡完全沒有波動的情況下,我們就可以正常按照順序得到BC的彈窗,但是如果網絡波動了呢,假設由於返回B的數據包正常在路上阻塞了,而C先返回來了,那麼最後得到的執行順序可能就是CB的彈窗了。在這裏只是一個順序問題,如果我們做搜索的時候,更加希望的是展示輸入的最後的值的搜索結果,那麼按照上邊的例,我們希望得到最後輸入的那個字母的下一個字母,也就是順序輸入AB希望得到C,但是卻也有可能得到B

const fetch = text => {
    if(!text) return Promise.resolve("");
    const response = String.fromCharCode(text[text.length - 1].charCodeAt(0) + 1);
    return new Promise(resolve => {
        setTimeout(resolve, Math.random() * 1000, response);
    })
}

// 模擬快速輸入`A B`
// 輸出時而 `B C` 時而 `C B`
// 如果不是打印而是將值寫到頁面上 那麼頁面顯示就出現錯誤了 
fetch("A").then(console.log);
fetch("AB").then(console.log);

通常來說,對於這類需求,我們會在輸入的時候加一個防抖函數,這樣的話第一個輸入就會被抹掉,這樣在這裏就不會造成快速輸入的競態問題了,這是屬於從降低頻率入手,儘量確保請求的有序。爲什麼說盡量呢,因爲如果用戶中間停頓了300ms也就是下邊設置的值之後,再進行輸入的話,依舊無法確保解決網絡的原因造成的競態問題,如果你把這個延時設置的非常大的話,那麼就會造成用戶最少等待n ms才能響應,用戶體驗並不好。

const fetch = text => {
    if(!text) return Promise.resolve("");
    const response = String.fromCharCode(text[text.length - 1].charCodeAt(0) + 1);
    return new Promise(resolve => {
        setTimeout(resolve, Math.random() * 1000, response);
    })
}

const d = function(time, fn){
    let timer = null;
    return (...args) => {
        clearTimeout(timer);
        timer = null;
        timer = setTimeout(() => fn(...args), time);
    }
}

const request = param => {
    fetch(param).then(console.log);
}
const debouncedRequest = d(300, request);
debouncedRequest("A");
debouncedRequest("AB");

那麼還有什麼辦法呢,或許我們也可以從確保順序入手,請求攜帶一個標識,請求返回後根據標識判斷是否渲染,這樣的話就需要改動一下我們的fetch,把請求的參數也一併帶上返回。這樣看起來是完全解決了競態的問題,但是似乎看起來並不是非常的漂亮,追求完美的同學可能會眉頭一皺,覺得事情並不簡單,這一段代碼的執行結果依賴兩個異步邏輯的彼此的執行順序,而需要我們編寫其他的代碼去控制這個執行順序,這個問題通常稱作競態危害。

const fetch = param => {
    if(!param) return Promise.resolve({param, response: ""});
    const response = String.fromCharCode(param[param.length - 1].charCodeAt(0) + 1);
    return new Promise(resolve => {
        setTimeout(resolve, Math.random() * 1000, {param, response});
    })
}

let tag = "";
const request = param => {
    tag = param;
    fetch(param).then((res) => {
        if(res.param === tag) console.log(res.response);
    });
}
request("A");
request("AB");

當然還有很多其他的方案可以處理這個問題,例如輸入節流輸入後開始請求的時候加一個全局的loading遮罩層,來阻止服務響應之前用戶繼續輸入,或者在進行第二次請求的時候,取消前一次的請求,類似於useEffect返回的函數,取消上次的副作用。
對於請求取消的這個問題,並不是真的服務端收不到數據包了,只是說瀏覽器不處理這次請求的響應了,或者乾脆我們自己直接本地不處理服務端的響應了,其實也很好理解,大部分情況下網絡波動實際上是比較小的,當發起請求的時候數據包已經出去了,當你進行取消操作的時候,假如我們的取消操作是發出去了一個包用來告訴服務器取消前一個請求,這個取消數據包大部分情況下是不能追上之前發出去的請求數據包的,等這個數據包到的時候服務器都可能已經處理完了,所以實際上如果採用這個操作的話基本是個無效操作,由此現在的請求取消只是說瀏覽器取消了對於這個請求的響應處理而已,並不是服務器真的收不到數據了。

RxJs

RxJsReactive Extensions for JavaScript的縮寫,起源於Reactive Extensions,是一個基於可觀測數據流Stream結合觀察者模式和迭代器模式的一種異步編程的應用庫,RxJsReactive ExtensionsJavaScript上的實現。其通過使用Observable序列來編寫異步和基於事件的程序,提供了一個核心類型Observable,附屬類型ObserverSchedulersSubjects和受[Array#extras]啓發的操作符mapfilterreduceevery等等,這些數組操作符可以把異步事件作爲集合來處理。RxJs有中文文檔https://cn.rx.js.org/manual/overview.html,可以定義函數在https://rxviz.com/中看到可視化的效果。
RxJs中用來解決異步事件管理的的基本概念是:

  • Observable: 可觀察對象,表示一個概念,這個概念是一個可調用的未來值或事件的集合。
  • Observer: 觀察者,一個回調函數的集合,它知道如何去監聽由Observable提供的值。
  • Subscription: 訂閱,表示Observable的執行,主要用於取消Observable的執行。
  • Operators: 操作符,採用函數式編程風格的純函數pure function,使用像mapfilterconcatflatMap等這樣的操作符來處理集合。
  • Subject: 主體,相當於EventEmitter,並且是將值或事件多路推送給多個Observer的唯一方式。
  • Schedulers: 調度器,用來控制併發並且是中央集權的調度員,允許我們在發生計算時進行協調,例如setTimeoutrequestAnimationFrame或其他。

RxJs上手還是比較費勁的,最直接的感受還是: 一看文章天花亂墜,一寫代碼啥也不會。在這裏也僅僅是使用RxJs來處理上邊我們提出的問題,要是想深入使用的話可以先看看文檔。
那麼我們就用RxJs來解決一下最初的那個問題,可以看到代碼非常簡潔,在這裏我們取了個巧,直接將Observable.createobserver暴露了出來,實際上因爲是事件觸發的,通常都會使用Observable.fromEvent來綁定事件,在這裏演示我們是需要自己觸發的事件了,也就是runner.next,這裏最重要的一點就是藉助了switchMap,他幫助我們管理了在流上的順序,取消了上次回調的執行。在下邊這個示例中,可以看到其只輸出了C,達到了我們想要的效果。

// 這塊代碼可以在`https://cn.rx.js.org/`的控制檯直接運行
const fetch = text => {
    if(!text) return Promise.resolve("");
    const response = String.fromCharCode(text[text.length - 1].charCodeAt(0) + 1);
    return new Promise(resolve => {
        setTimeout(resolve, Math.random() * 1000, response);
    })
}

let runner;
const observable = Rx.Observable.create(observer => runner = observer);
observable
//   .debounceTime(300) // 可以加入防抖
  .switchMap(param => fetch(param))
  .subscribe(console.log);

runner.next("A");
runner.next("AB");

每日一題

https://github.com/WindrunnerMax/EveryDay/

參考

https://cn.rx.js.org/
https://zhuanlan.zhihu.com/p/104024245
https://www.zhihu.com/question/324275662
https://juejin.cn/post/6910943445569765384
https://juejin.cn/post/6844904051046350862
https://juejin.cn/post/7098287689618685966
https://juejin.cn/post/6970710521104302110
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章