化解使用 Promise 時的競態條件

網絡時代,建立現代軟件時其中一個很大的限制是所須要的數據每每在遠程服務器上。應用程序在等待網絡請求時簡單地鎖死是不現實(甚至不可能)的。相反,咱們必須讓應用程序在等待時保持響應。。前端

爲此,咱們須要寫出併發的代碼。當應用的某一部分正在等待網絡請求的響應時,其餘部分必須繼續運行。 Promise 對於編寫非阻塞型的代碼是很不錯的工具,並且你的瀏覽器就支持這個。promise

Promise 能讓潛在可怕的異步代碼變得很是友好。下面假設一個博客的文章視圖這樣從遠程服務器加載一篇文章並顯示它:瀏覽器

// Called from `componentWillMount` and `componentWillReceiveProps`:
ArticleView.prototype.updateArticle = function (props) {
    this.setState({
        error: null,
        title: null,
        body: null
    });
    ArticleStore.fetch(props.articleID).then(article => {
        this.setState({
            title: article.title,
            body: article.body
        });
    }).catch(err => {
        this.setState({ error: 'Oh Noes!' });
    });
};

注意:這個例子使用了 React,可是這個概念適用於絕大多數前端視圖系統。服務器

這樣的代碼是很優雅的。許多複雜的異步調用消失了,取而代之的是直接明瞭的代碼。然而,使用 promise 並不能保證代碼是正確的。網絡

注意到我例子中引入的不易察覺的競態條件了嗎?併發

提示:競態條件出現的緣由是沒法保證異步操做的完成會按照他們開始時一樣的順序。框架

輪子掉了異步

爲了闡明競態條件,假設有這樣一個左側是文章列表,右側是選中的文章內容的博客:
圖片描述
App with Article 1 Selectedasync

讓咱們從第一個選中的文章標題開始。而後,選中第二個文章標題。該應用發送一個請求去加載文章的內容(this.store.fetchArticle(2)),而且用戶能夠看見一個加載的指示器,就像這樣:
6819cb9bgy1g2v5epeq3vj20je09bmxn.jpg
App with Article 2 Selected函數

由於網絡緣由,文章內容的加載須要一小會兒。數秒以後,用戶以爲厭煩就(又)選擇了第一篇文章。因爲這篇文章已經加載過,它的內容幾乎當即顯示,應用彷彿回到最開始的狀態。
圖片描述
App with Article 1 Reselected

可是接着發生了奇怪的事情:應用最終收到了第二篇文章的內容,文章視圖只好盡職地更新它的標題和主體來顯示新加載的內容,致使用戶看到這樣的厭惡的東西:
圖片描述
App with Article 1 Selected but Article 2 Displayed

文章列表(也多是 URL 和其餘 UI 元素)代表選中的是第一篇文章,可是用戶看到的倒是第二篇文章的內容。

這個問題很嚴重,更糟糕的是在開發環境你未必能發現。在你的本機上(或者本局域網等等),加載更快並且更少出現意外。所以,代碼運行時,在等待請求完成的過程當中你極可能不會以爲厭煩。

裝回輪子
首先要明白髮生了什麼才能解決這個問題。咱們遇到的競態條件過程以下: 1.在狀態 A 時開始異步操做(選中第二篇文章)。 2.應用變換至狀態 B (選中第一篇文章)。 3.異步操做完成,然而代碼仍然按應用處於狀態 A 來處理。

找出問題以後,咱們就能夠設計解決方案了。跟絕大多數 bug 同樣,也有不少備選方案。理想的方案是從一開始就杜絕產生 bug 的可能。例如,不少路由庫將 promise 做爲路由選擇的一部分,從而避免了此類 bug。若是你手上有這樣的工具能夠直接使用。

然而,在這種狀況下須要咱們自行管理這些 promise。這裏要杜絕產生競態條件不大現實,因此只好退而求其次,使競態條件簡單明瞭的抵消。

我最喜歡的『簡單明瞭』的方案是這樣的: 1.異步操做開始時記錄應用的相關狀態。 2.異步操做完成後校驗應用是否仍處於同一狀態。

舉例以下:

ArticleView.prototype.updateArticle = function (props) {
    this.setState({
        error: null,
        title: null,
        body: null
    });
    // 記錄應用的狀態:
    var id = props.articleID;

    ArticleStore.fetch(id).then(article => {

        // 校驗應用的狀態:
        if (this.props.articleID !== id) return;

        this.setState({
            title: article.title,
            body:article.body
        });
    }).catch(err => {
        // 校驗應用的狀態:
        if (this.props.articleID !== id) return;

        this.setState({
            error: 'Oh Noes!'
        });
    });
};

之因此喜歡這個方案是由於記錄和校驗狀態的全部代碼都在一塊,正好緊挨着異步操做的代碼。

結語
這個問題並非基於 promise 的代碼特有的,Node 式的回調代碼也有一樣的問題。基於 promise 的代碼看起來愈來愈無害處,儘管它能輕鬆避免這樣的問題。雖然我很樂意使用 async 函數和 await 關鍵字,但有點擔憂他們更容易致使忽略這些問題(這裏有個例子):

我在本文中所舉的例子並不是子虛烏有,它來自我在實際產品應用中看到的代碼。

異步代碼是開發者最難搞懂的事情之一 。執行順序的數量會隨着異步操做的數量呈指數增加,很快使代碼變得很是的複雜。

若是可能,利用平臺或框架級的抽象來管理所以增長的複雜性。不然,最好將異步操做當作嚴格的界限。(異步操做完成)代碼恢復時,將一切都當成已改變,由於它也許改變了。

相關文章
相關標籤/搜索