由Knockout跨到Angular半年,對於NG的Dirty Check機制卻始終沒好感,老覺得它髒,為了偷懶不宣告Observable跟少寫一些訂閱連動,卻無法預期程式觸發次數與時機,讓我很沒安全感。如果可以選擇,我寧可乖乖多寫一些Code,100%掌控程式運作,避免陷入程式 一旦複雜就可能失控的擔憂。(註:我想Angular RD也認同這點,在2.0將另推Observable。參考:… One approach is to replace the dirty checking that AngularJS currently does with Object.observe, which is a proposal to add native support for model change listeners and data binding. AngularJS 2.0 will totally use this to significantly speed up the whole data-binding and update cycle. …)

不過,既然用了NG 1.x,再怎麼不喜歡Dirty Check也得跟它和平共處。只是在實務上,我常遇到一項困擾:$watch()機制可以掌握資料被更改的時點,卻無從得知修改來源。尤其當程式碼龐大,互動複雜(尤其涉及AJAX、setTimeout等運作時),追不出變動來源讓偵錯除錯的困難度上升不少。

用一個簡單範例展示:

<!DOCTYPE html>
<html ng-app="app">
<head>
 
    <meta charset="utf-8">
    <title>JavaScropt property change tracing</title>
</head>
<body ng-controller="ctrl as m">
    <span>{{m.prop}}</span>
    <input type="button" value="Change" ng-click="m.change()" />
    <script src="//code.jquery.com/jquery-2.1.1.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.js"></script>
    <script>
  function ViewModel($scope) {
    var self = this;
    self.prop = "darkthread";
    self.change = function() {
      self.prop = "Jeffrey";
    };
    $scope.$watch(function() {
      return self.prop;
    }, function(newValue, oldValue) {
      console.log("nv=" + newValue + ", ov=" + oldValue);
      debugger;
    });
  }
  angular.module("app", []).controller("ctrl", ViewModel);
    </script>
</body>
</html>

在以上程式中,我們用$scope.$watch()捕追prop異動的時點,用debugger指令觸發偵錯中斷,但在Callstack(呼叫堆疊)中,只見angular.js的$apply(), $digest(),看不出prop是被self.chnage()中的self.prop = "Jeffrey"所更動。

JavaScript語言的彈性眾所皆知,自然有神妙的方法解決這類困境。參考網路上高人的文章,透過一些JavaScript技巧將屬性改成透過getter及setter存取,就有機會在屬性被設定時加入自訂邏輯。但原文直接設在Object.prototype.watch的做法容易跟jQuery、Angular等程式庫打架,故我改寫成共用函式版本:

<!DOCTYPE html>
<html ng-app="app">
<head>
 
    <meta charset="utf-8">
    <title>JavaScropt property change tracing</title>
</head>
<body ng-controller="ctrl as m">
    <span>{{m.prop}}</span>
    <input type="button" value="Change" ng-click="m.change()" />
    <script src="//code.jquery.com/jquery-2.1.1.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.js"></script>
    <script>
function watchPropSet(instance, prop, handler) {
    var val = instance[prop],
        getter = function () {
            return val;
        },
        setter = function (newval) {
            return val = handler.call(instance, newval, val);
        };
    if (delete instance[prop]) { // can't watch constants
        if (Object.defineProperty) { // ECMAScript 5
            Object.defineProperty(instance, prop, {
                get: getter,
                set: setter
            });
        }
        else if (Object.prototype.__defineGetter__ &&
            Object.prototype.__defineSetter__) //legacy
        {
            Object.prototype.__defineGetter__.call(instance, prop, getter);
            Object.prototype.__defineSetter__.call(instance, prop, setter);
        }
    }
}
function unwatchPropSet(instance, prop) {
    var val = instance[prop];
    delete instance[prop]; // remove accessors
    instance[prop] = val;
}
 
  function ViewModel($scope) {
    var self = this;
    self.prop = "darkthread";
    self.change = function() {
      self.prop = "Jeffrey";
    };
    watchPropSet(self, "prop", function(newValue, oldValue) {
      console.log("nv=" + newValue + ", ov=" + oldValue);
      debugger;
    });
  }
  angular.module("app", []).controller("ctrl", ViewModel);
</script>
</body>
</html>

依循$scope.$watch()的概念,我們為prop加上setter函式,在其中可取得新值及舊值,並埋入debugger中斷,這樣就能輕鬆追出變更屬性的凶手囉~

[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

Comments

Be the first to post a comment

Post a comment