anjularjs中的雙向綁定很有意思,下面模仿其源碼,寫了一個簡單的版本,就是魯棒性差了點。
從"wo zi ji de"中可以看到,當輸入框的內容變化時,其上2排內容也在動態變化。有點小興奮啊。其中x變量用angularjs的雙向綁定,y變量用的自己的(zi ji de)。
HTML:
<p>angularjs</p> <p ng-bind='x'></p> <input type='text' ng-model='x'; class='form-control' /> <p>wo zi ji de</p> <p by-bind='y'></p> <p by-bind='y'></p> <input type='text' by-model='y'; class='form-control' />
實現原理:
1.$watch函數的2個參數,第一個是要監聽的變量,第二個是變量改變時運行的函數。這2個存入JSON格式的watcher中,watchFn:該函數返回當前時刻的值,可能變了,可能沒變。如下所示,last代表上次的值,初始爲1.
var watcher = { watchFn:function(watchValue,self){ //return self.data.get(watchValue); return self.result2[0].value; }, listenerFn:listenerFunction, last:1 };
2.由於不止監聽一個變量,所以會有很多個watcher,所有watcher存入$$watcher中。兩個美元代表私有變量,anjularjs中是代表$scope的私有變量,不讓用戶接觸的。
this.$$watchers.push(watcher);
3.有了要監聽的變量,下一步就是實現監聽了。就是$digest對象的職責了。$digest對$$watcher中的每一個watcher進行排查,看其當前值與last值是否不同,不同當然就說明被監聽的值已經變了。那麼對應使用ng-bind(by-bind)顯示的值,也應該更新。這就是watcher中的listenerFn函數要乾的事了。
4.說明:ByScope對象來模仿$scope對象。$scope.x 對應html中<p ng-bind='x'/>和<input ng-model='x'/>。而ByScope模仿時(<p by-bind='y'/><input by-model='y'/>),是使用了一個map鍵值數組,'y'是key,key對應的值就相當於$scope.x;
$scope.x 只需一句話,不用自己初始$scope變量,這些工作我都沒做-_-!。你沒聽錯,所以要自己建立ByScope對象,並將y值傳入。 爲什麼呢?
var bysowhat = new ByScope("y");
anjularjs源碼中,使用下面代碼中的 get = compileToFn(watchExp, 'watch')。compileToFn可以把按照字符串的值轉爲對應的變量和函數,只有腳本語言可以辦到哦。這個函數的源碼我是編不完的。
$watch: function(watchExp, listener, objectEquality) { var scope = this, get = compileToFn(watchExp, 'watch'), array = scope.$$watchers, watcher = { fn: listener, last: initWatchVal, get: get, exp: watchExp, eq: !!objectEquality };
JAVASCRIPT:
function ListControler($scope){ $scope.x = 100; } (function($){ //深度優先遍歷DOM樹,找by-bin='y'的節點 //node:節點 //attri:"by-bind" //返回值:目標結點數組 function bianli2(node,attri,value,resultNode){ var allNodes = node.childNodes; if(allNodes != undefined){ //有子節點,繼續遞歸 for(var i=0;i<allNodes.length;i++){ if(allNodes[i].nodeType == 1 && allNodes[i].nodeName!="SCRIPT"){ //所有元素節點,除script //查找自定義屬性 var attrStr = allNodes[i].getAttribute(attri); //找到時,將節點傳給resultNode if(attrStr == value) resultNode.push(allNodes[i]); //alert(allNodes[i].nodeName); bianli(allNodes[i]); } } } } //實現$watch function ByScope(watchValueName){ //存儲所有watch this.$$watchers = []; this.data = new Map(); //this.data.add("y",123); this.result1=[]; this.result2=[]; //var self = this; bianli2(document.body,"by-bind",watchValueName,this.result1); bianli2(document.body,"by-model",watchValueName,this.result2); //alert(this.data.get("y")); } //監聽器存入$$watchers ByScope.prototype.$watch = function(watchValue, listenerFunction){ var self = this; var watcher = { watchFn:function(watchValue,self){ //return self.data.get(watchValue); return self.result2[0].value; }, listenerFn:listenerFunction, last:1 }; this.$$watchers.push(watcher); //每0.1秒運行一次$digest setInterval(function(){self.$digest(watchValue)},100); //this.$digest(watchValue); }; //對比監聽器內容:$digest ByScope.prototype.$digestOnce = function(watchValue){ var self = this; var dirty; for(var i=0;i<this.$$watchers.length;i++){ var newValue = this.$$watchers[i].watchFn(watchValue,self); var oldValue = this.$$watchers[i].last; //被監視的值發生變化 if(newValue != oldValue){ this.$$watchers[i].listenerFn(); dirty = true; } //更新監視值 this.$$watchers[i].last = newValue; } return dirty; }; ByScope.prototype.$digest = function(watchValue){ var dirty; do{ dirty = this.$digestOnce(watchValue); }while(dirty); }; var bysowhat = new ByScope("y"); bysowhat.$watch('y',function(){ for(var i=0;i<bysowhat.result1.length;i++){ bysowhat.result1[i].innerHTML = bysowhat.result2[0].value; } }); })(jQuery);
我的代碼看完了,再來看看源碼的逐行解析
1.
get = compileToFn(watchExp, 'watch'),
由於watchExp是通過字符串傳遞的,所以要把字符串轉爲變量或者函數(函數也是變量)。就像setInterval('myfunction',100)中的函數名字符串'myfunction'。
2.
array = scope.$$watchers, watcher = { fn: listener, last: initWatchVal, get: get, exp: watchExp, eq: !!objectEquality };
watcher對象存入監聽過程中的基本信息: exp:要監聽的變量;fn:發現變量數值變化時所執行的函數;last:變量上一次的值;eq:深度監聽,get:如前所述。
3.
// in the case user pass string, we need to compile it, do we really need this ? if (!isFunction(listener)) { var listenFn = compileToFn(listener || noop, 'listener'); watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; }
如果通過$scope.$watch傳遞的第二個變量不是函數,是字符串,那麼就把字符串轉爲函數,話說誰會傳字符串啊_-! 你會放棄火車票,而拿火車票的相片給檢票員看嗎?這也是爲什麼源碼註釋成"do we really need this?"
4.
if (typeof watchExp == 'string' && get.constant) { var originalFn = watcher.fn; watcher.fn = function(newVal, oldVal, scope) { originalFn.call(this, newVal, oldVal, scope); arrayRemove(array, watcher); }; }
爲啥呢
5.
if (!array) { array = scope.$$watchers = []; } // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher);
源碼中,第一個watcher加入時,還沒有array存在,所以這時要創建array。
unshift代表在數組頭插入數據,相當於把數組當棧用。爲了後面$digest的循環速度。
6.
return function() { arrayRemove(array, watcher); };
這構成了閉包,目的是讓下面這行
arrayRemove(array, watcher);
在想被執行的時候再執行。
*/ $watch: function(watchExp, listener, objectEquality) { var scope = this, get = compileToFn(watchExp, 'watch'), array = scope.$$watchers, watcher = { fn: listener, last: initWatchVal, get: get, exp: watchExp, eq: !!objectEquality }; // in the case user pass string, we need to compile it, do we really need this ? if (!isFunction(listener)) { var listenFn = compileToFn(listener || noop, 'listener'); watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; } if (typeof watchExp == 'string' && get.constant) { var originalFn = watcher.fn; watcher.fn = function(newVal, oldVal, scope) { originalFn.call(this, newVal, oldVal, scope); arrayRemove(array, watcher); }; } if (!array) { array = scope.$$watchers = []; } // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher); return function() { arrayRemove(array, watcher); }; },