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); }; },