由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等運作時),追不出變動來源讓偵錯除錯的困難度上升不少。
用一個簡單範例展示:
<!DOCTYPEhtml>
<htmlng-app="app">
<head>
<metacharset="utf-8">
<title>JavaScropt property change tracing</title>
</head>
<bodyng-controller="ctrl as m">
<span>{{m.prop}}</span>
<inputtype="button"value="Change"ng-click="m.change()"/>
<scriptsrc="//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等程式庫打架,故我改寫成共用函式版本:
<!DOCTYPEhtml>
<htmlng-app="app">
<head>
<metacharset="utf-8">
<title>JavaScropt property change tracing</title>
</head>
<bodyng-controller="ctrl as m">
<span>{{m.prop}}</span>
<inputtype="button"value="Change"ng-click="m.change()"/>
<scriptsrc="//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
});
}
elseif (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中斷,這樣就能輕鬆追出變更屬性的凶手囉~