專案出現一個ko.computed()應用實例,剛好可以考驗開發者對ko.computed()了解是否透徹。各位同學,準備接招!

需求是這樣的,有A、B、C三個欄位,若使用者修改A或B,C需更新為A + "-" +B;若使用者直接修改C,則以其輸入字串為主,忽略前述由A/B計算C的邏輯。另外,若由Server端讀取,ViewModel要更新為由資料庫讀取所得的A、B和C。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>KO範例29 - ko.computed隨堂測驗A</title>
    <style>
        .disp
        {
            margin-top: 5px;
            width: 100px;
            border: 1px solid gray; 
            padding: 5px;
        }
    </style>
</head>
<body>
    <div>A: <input type="text" data-bind="value: A" /></div>
    <div>B: <input type="text" data-bind="value: B" /></div>
    <div>C: <input type="text" data-bind="value: C" /></div>
    <div><input type="button" value="模擬資料載入" data-bind="click: load"/></div>
    <div class="disp">C = <span data-bind="text: C"></span></div>
 
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js"></script>
    <script>
        function myViewModel() {
            var self = this;
            self.A = ko.observable("A");
            self.B = ko.observable("B");
            self.C = ko.observable();
            ko.computed(function () {
                self.C(self.A() + "-" + self.B());
            });
            //假裝由DB取得資料更新至ViewModel
            var dataFromDb = { B: "Y", A: "X", C: "Overwrited" };
            self.load = function () {
                for (var p in dataFromDb)
                    self[p](dataFromDb[p]);
            };
        }
        var vm = new myViewModel();
        ko.applyBindings(vm);
    </script>
</body>
</html>

宣告A, B, C三個observable,用一個computed建立C = A + "-" + B的關聯,如此當A或B被修改,C會跟著連動,而C也可以直接被修改。另外,加入一個小函式模擬由資料庫取得物件,將其屬性一一寫入ViewModel。測試結果如同預期,莫非這樣就可以搞定收工? 線上展示

並沒有!!

搗亂的時間到了。在程式加入陷阱: 來自資料庫的內容,C為使用者另外輸入,不等於A、B相加,而更新時不保證A、B、C屬性的更新順序。換句話說,有可能更新A、B再更新C,也有可能更新C,再更新A、B。

            //假裝由DB取得資料更新至ViewModel,但有個小問題,
            //Server傳來的資料屬性順序是C、B、A
            var dataFromDb = { C: "Overwrited", B: "Y", A: "X" };

動一點手腳,將dataFromDb屬性順序改為C、B、A。結果load函式會先更新C,再更新B、C,資料庫讀取到的C值("Overwrited")被罝換成"X - Y",與資料庫讀得內容不同,程式就壞了~ 線上展示

為克服這個問題,我們在ViewModel中增加載入旗標--loading,同時為了讓網頁能一併顯示
載入狀態,loading被宣告成ko.observable(false),方便<span data-bind="visible: loading">載入中</span>直接繫結。load()函式則加入邏輯,開始設定資料前將loading設為true,設定完畢後設為false,而computed則加入檢查,當loading()傳回true時,代表正將ViewModel覆寫為來自資料庫的內容,先停用C = A + "-" + B的運算。

        function myViewModel() {
            var self = this;
            self.A = ko.observable("A");
            self.B = ko.observable("B");
            self.C = ko.observable();
            //增加loading旗標,以便在載入過程停用連動
            self.loading = ko.observable(false);
            ko.computed(function () {
                if (self.loading()) return;
                self.C(self.A() + "-" + self.B());
            });
 
            //假裝由DB取得資料更新至ViewModel,但有個小問題,
            //Server傳來的資料屬性順序是C、B、A
            var dataFromDb = { C: "Overwrited", B: "Y", A: "X" };
 
            self.load = function () {
                self.loading(true);
                for (var p in dataFromDb)
                    self[p](dataFromDb[p]);
                self.loading(false);
            };
        }

程式範例如上,但其中有Bug,有人發現了嗎? 線上展示

按下【模擬資料載入】鈕後,C出現的是X - Y,不是Overwrited!! 原因出在computed為了判斷該不該更新C而讀取loading這個observable,於是computed訂閱了loading;當load()更新完資料執行self.loading(false),觸發原本要避免的C = A + "-" + B computed邏輯,一切白搭。

針對在computed裡取值卻又不想產生訂閱關係的情境,Knockout有個祕密武器 – peek()

            ko.computed(function () {
                //改用peek(),避免loading變動時觸發重算
                if (self.loading.peek()) return;
                self.C(self.A() + "-" + self.B());
            });

將self.loading()改成self.loading.peek(),重新試一次,這下總OK了吧? 但以上的寫法大有問題,有人猜到了嗎?

C終於出現Overwrited了耶!! 等等,載入資料後,C = A + "-" + B卻壞了?? 線上展示

這是Knockout新手常犯的錯誤之一,在官方文件 How dependency tracking works一節有段重要說明:

computed追蹤相依性的運作原理如下:

  1. Whenever you declare a computed observable, KO immediately invokes its evaluator function to get its initial value.
    當computed被宣告時,KO會立刻執行一次以取得初值。
  2. While your evaluator function is running, KO keeps a log of any observables (or computed observables) that your evaluator reads the value of.
    在執行過程中,KO會一一記下程式讀取哪些observable及computed
  3. When your evaluator is finished, KO sets up subscriptions to each of the observables (or computed observables) that you’ve touched. The subscription callback is set to cause your evaluator to run again, looping the whole process back to step 1 (disposing of any old subscriptions that no longer apply).
    執行完畢,KO會自動訂閱步驟2所記下的每一個observable或computed,只要其中任何一個有變動,整個computed就會再重新執行一次,重複步驟1,2並重新定義因讀取產生的訂閱關係,並抛棄不用的訂閱。
  4. KO notifies any subscribers about the new value of your computed observable.
    KO通知此computed的訂閱者新值。

值得注意的是,KO不只第一次會偵測computed對其他obervable、observableArray或computed的依賴關係,而是每次執行computed都要重新偵測、重新訂閱。因此,如果在computed中有if分支,某些條件下才會參考某個observable,便會出現因某次沒有執行到,便不再追蹤該observable的窘境。如同先前的實例,一旦if (self.loading.peek()) return;生效,後方的self.A(), self.B()沒被執行,整個computed就不再因A或B變動觸發重新執行。要解決很簡單,在if邏輯之前將A, B值先存為變數,就能避免因if邏輯差異影響訂閱範圍。程式只需簡單修改成:

            ko.computed(function () {
                //computed每次執行都會重新計算訂閱關係,
                //故要避免因if分支漏掉訂閱,這是初學者常犯的錯
                //最簡單的做法是if分支前就讀取observable
                var newValue = self.A() + "-" + self.B();
                //改用peek(),避免loading變動時觸發重算
                if (self.loading.peek()) return;
                self.C(newValue);
            });

終於,搞定收工! 線上展示

重點復習:

  1. computed每次執行都會重新偵測參考到的observable、observableArray及computed並建立訂閱,以便下次變動時觸發重算。
  2. computed中如有if分支,要確保所有要訂閱對象都要被讀取到,以免失去相依性。最簡單的做法是在if分支發生前讀取observable存成變數。
  3. 如在computed中只想參考某個observable,但不希望在其變動時觸發重算,可使用observable.peek()取值。

下課!!

[KO系列]

http://www.darkthread.net/kolab/labs/default.aspx?m=post

Comments

Be the first to post a comment

Post a comment