Quantcast
Channel: 黑暗執行緒
Viewing all articles
Browse latest Browse all 2311

KO範例26 - ko.computed()的效能考量

$
0
0

ko.computed()能追蹤所依賴的ko.observable()或ko.observableArray(),在其變化時自動重算,開發時依直覺寫出關連邏輯,屬性間便會依預期變化。使用起來固然方便,但是當依賴對象連續變化時,要留意反覆重算的必要性以及對效能的衝擊。用一個範例來說明:

<!DOCTYPE html>
<html>
<head>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js">
</script>
<meta charset=utf-8 />
<title>KO範例26 - 利用throttle改善computed效率(改善前)</title>
</head>
<body>
<div>
    Max: <span data-bind="text: max"></span>
    Min: <span data-bind="text: min"></span>
    Sum: <span data-bind="text: sum"></span>
    Avg: <span data-bind="text: avg"></span>
</div>
<script>
    function myViewModel() {
      var self = this;
      self.items = ko.observableArray();
      self.max = ko.computed(function() {
        var ary = self.items();
        var max = null;
for (var i = 0; i < ary.length; i++) {
if (max == null || ary[i] > max) max = ary[i];
        }
return max;
      });
      self.min = ko.computed(function() {
        var ary = self.items();
        var min = null;
for (var i = 0; i < ary.length; i++) {
if (min == null || ary[i] < min) min = ary[i];
        }
return min;
      });
      self.sum = ko.computed(function() {
        var ary = self.items();
        var sum = 0;
for (var i = 0; i < ary.length; i++) {
          sum += ary[i];
        }
return sum
      });
      self.avg = ko.computed(function() {
        var ary = self.items();
if (ary.length == 0) return 0;
return parseFloat(self.sum()) / ary.length;
      });
    }
    var vm = new myViewModel();
    ko.applyBindings(vm);
for (var i = 1; i < 10000; i++) {
      vm.items.push(i);
    }
</script>
</body>
</html>

在以上範例中,ViewMode用了一個observableArray存放數字陣列,另外有四個屬性: max, min, sum, avg分別用以統計該數字陣列的最大值、最小值、總和及平均值,最直覺的寫法是四個屬性用ko.computed各寫各的,跑迴圈取出陣列的每一個數字處理。但可以預期,只要陣列每次加入新元素,就會觸發max, min, sum各跑一次迴圈(avg直接由sum值除以陣列長度計算,不必跑迴圈)。當我們連續在陣列加入從1到9999,共9999個數字,猜猜ko.computed要執行幾次? 理論上會觸發9999次,再上三組computed跑迴圈,第一次跑1圈,第2次2圈,第3次3圈,...,第9999次跑9999圈,CPU耗用量不少,使用IE10實測,耗時3.814秒。

先撇開Knockout特性不談,程式邏輯面存在一些無效率,先對三組computed跑迴圈的部分開刀,其實只需要跑一次迴圈,就可以一次算出max, min, sum及avg,故可以將max、min、avg都改為純ko.observable(),只留下sum為ko.computed(),加總同時一併求出最大值、最小值及平均,再分別寫入max、min及avg。

function myViewModel() {
var self = this;
      self.items = ko.observableArray();
      self.max = ko.observable();
      self.min = ko.observable();
      self.avg = ko.observable();
      self.sum = ko.computed(function() {
var ary = self.items();
var sum = 0;
var max = null;
var min = null;
for (var i = 0; i < ary.length; i++) {
if (min == null || ary[i] < min) min = ary[i];
if (max == null || ary[i] > max) max = ary[i];
          sum += ary[i];
        }
        self.max(max);
        self.min(min);
        self.avg(ary.length == 0 ? 0 : parseFloat(self.sum()) / ary.length);
return sum;
      });
    }
var vm = new myViewModel();
    ko.applyBindings(vm);
for (var i = 1; i < 10000; i++) {
      vm.items.push(i);
    }

改良後,發現原本會執行39,892次的evaluatePossibleAsync減少到9,955次,而耗時也由3.8秒降低到2.351秒。

還有改善空間嗎? 當然有! 我們關心的是最大值、最小值、加總、平均的最後計算結果,在將1-9999塞入陣列期間的變化過程一點也不重要,因此並不需要每塞一個數字就重新計算一次,等資料全部就緒再計算就好。先前在AJAX範例介紹過的throttle擴充方法是解決這個問題的好方法,只需在computed後方套上throttle,則observableArray的元素有變化時,不會立刻重算,會等待一小段時間,確認資料不再變化後才進行重算,如此便可抑制連續新增元素期間多餘的重算動作,有效改善效能。

function myViewModel() {
var self = this;
      self.items = ko.observableArray();
      self.max = ko.observable();
      self.min = ko.observable();
      self.avg = ko.observable();
      self.sum = ko.computed(function() {
var ary = self.items();
var sum = 0;
var max = null;
var min = null;
for (var i = 0; i < ary.length; i++) {
if (min == null || ary[i] < min) min = ary[i];
if (max == null || ary[i] > max) max = ary[i];
          sum += ary[i];
        }
        self.max(max);
        self.min(min);
        self.avg(ary.length == 0 ? 0 : parseFloat(self.sum()) / ary.length);
return sum;
      }).extend({ throttle: 200 });
    }
var vm = new myViewModel();
    ko.applyBindings(vm);
for (var i = 1; i < 10000; i++) {
      vm.items.push(i);
    }

最終改良版果然沒讓我們失望,耗時由2.351秒一舉縮短到136ms,而樹狀節點中的setTimeout、clearTimeout,便是throttle透過延遲執行改善效能的痕跡。

【結論】在設計ko.computed()時,記得評估其被呼叫次數與時機,避免短時間反覆大量執行,必要時可使用throttle擴充方法改善,才不會寫出吃光CPU的怪獸網頁。

[KO系列]

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

Viewing all articles
Browse latest Browse all 2311

Trending Articles