Data Binding
By binding a text input box to the person.name property, we can make our application a little more interesting. This step establishes a two-way binding between the text input box and the page.
In this context, "bidirectional" means that if the view changes the attribute value, the model will "see" the change, and if the model changes the attribute value, the view will also "see" the change. . Angular.js automatically sets up this mechanism for you. If you're curious about how exactly this is done, check out our upcoming article which discusses the workings of digest_loop in depth.
To create this binding, we use the ng-model directive attribute on the text input box, like this:
<p ng-controller="MyController"> <input type="text" ng-model="person.name" placeholder="Enter your name" /> <h5>Hello {{ person.name }}</h5> </p>
Now that we have established a data binding (yes, it’s that easy), let’s see how the view behaves Change the model:
Try it out:
When you enter in the text box, the name below will automatically change, which shows one direction of our data binding: from view to model.
We can also change the model in our (client) backend and see this change automatically reflected on the front end. To demonstrate this process, let's write a timer function in MyController's model to update a data on $scope. In the following code, we will create this timer function, which will time every second (like a clock) and update the clock variable data on $scope:
app.controller('MyController', function($scope) { $scope.person = { name: "Ari Lerner" }; var updateClock = function() { $scope.clock = new Date(); }; var timer = setInterval(function() { $scope.$apply(updateClock); }, 1000); updateClock(); });
You can see that when we Change the data of the clock variable in the model, and the view will automatically update to reflect this change. Using curly brackets, we can easily display the value of the clock variable in the view:
<p ng-controller="MyController"> <h5>{{ clock }}</h5> </p>
Interactive
Previously we bound the data to the text input box. Please note that data binding is not limited to data, we can also use binding to call functions in $scope (this has been mentioned before).
For buttons, links or any other DOM elements, we can use another directive attribute to implement binding: ng-click. This ng-click directive binds the mouse click event of the DOM element (i.e. the mousedown browser event) to a method. When the browser triggers a click event with the mouse on the DOM element, the bound method is called. Similar to the previous example, the binding code is as follows:
<p ng-controller="DemoController"> <h4>The simplest adding machine ever</h4> <button ng-click="add(1)" class="button">Add</button> <button ng-click="subtract(1)" class="button">Subtract</button> <h4>Current count: {{ counter }}</h4> </p>
Both buttons and links will be bound to all $scope objects of the controller containing their DOM elements when they are clicked by the mouse. , Angular will call the corresponding method. Notice that when we tell Angular what method to call, we write the method name into a quoted string.
app.controller('DemoController', function($scope) { $scope.counter = 0; $scope.add = function(amount) { $scope.counter += amount; }; $scope.subtract = function(amount) { $scope.counter -= amount; }; });
$scope.$watch
$scope.$watch( watchExp, listener, objectEquality );
To monitor changes in a variable, you can use the $scope.$watch function. This function has three parameters, which specify "what to watch" (watchExp), "what to happen when it changes" (listener), and whether you want to watch a variable or an object. When we are checking one parameter, we can ignore the third parameter. For example, in the following example:
$scope.name = 'Ryan'; $scope.$watch( function( ) { return $scope.name; }, function( newValue, oldValue ) { console.log('$scope.name was updated!'); } );
AngularJS will register your monitoring function in $scope. You can output $scope in the console to view the registered items in $scope.
You can see in the console that $scope.name has changed – this is because the previous value of $scope.name seemed to be undefined and now we assign it to Ryan!
For the first parameter of $wach, You can also use a string. This is exactly the same as providing a function. As you can see in the AngularJS source code, if you use a string, the following code will be run:
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); }; }
This will set our watchExp to a function, which will also automatically return the function In the domain we have defined variables with names.
$$watchers
The $$watchers variable in $scope stores all the monitors we defined. If you look at $$watchers in the console, you'll see that it is an array of objects.
$$watchers = [ { eq: false, // 表明我们是否需要检查对象级别的相等 fn: function( newValue, oldValue ) {}, // 这是我们提供的监听器函数 last: 'Ryan', // 变量的最新值 exp: function(){}, // 我们提供的watchExp函数 get: function(){} // Angular's编译后的watchExp函数 } ];
$watch function will return a deregisterWatch function. This means that if we use $scope.$watch to watch a variable, we can also stop watching it later by calling a function.
$scope.$apply
When a controller/directive/etc. is run in AngularJS, AngularJS internally runs a function called $scope.$apply. The $apply function will receive a function as a parameter and run it, before running the $digest function on the rootScope.
The $apply function code of AngularJS is as follows:
$apply: function(expr) { try { beginPhase('$apply'); return this.$eval(expr); } catch (e) { $exceptionHandler(e); } finally { clearPhase(); try { $rootScope.$digest(); } catch (e) { $exceptionHandler(e); throw e; } } }
上面代码中的expr参数就是你在调用$scope.$apply()时传递的参数 – 但是大多数时候你可能都不会去使用$apply这个函数,要用的时候记得给它传递一个参数。
下面我们来看看ng-keydown是怎么来使用$scope.$apply的。为了注册这个指令,AngularJS会使用下面的代码。
var ngEventDirectives = {}; forEach( 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), function(name) { var directiveName = directiveNormalize('ng-' + name); ngEventDirectives[directiveName] = ['$parse', function($parse) { return { compile: function($element, attr) { var fn = $parse(attr[directiveName]); return function ngEventHandler(scope, element) { element.on(lowercase(name), function(event) { scope.$apply(function() { fn(scope, {$event:event}); }); }); }; } }; }]; } );
上面的代码做的事情是循环了不同的类型的事件,这些事件在之后可能会被触发并创建一个叫做ng-[某个事件]的新指令。在指令的compile函数中,它在元素上注册了一个事件处理器,它和指令的名字一一对应。当事件被出发时,AngularJS就会运行scope.$apply函数,并让它运行一个函数。
只是单向数据绑定吗?
上面所说的ng-keydown只能够改变和元素值相关联的$scope中的值 – 这只是单项数据绑定。这也是这个指令叫做ng-keydown的原因,只有在keydown事件被触发时,能够给与我们一个新值。
但是我们想要的是双向数据绑定!
我们现在来看一看ng-model。当你在使用ng-model时,你可以使用双向数据绑定 – 这正是我们想要的。AngularJS使用$scope.$watch(视图到模型)以及$scope.$apply(模型到视图)来实现这个功能。
ng-model会把事件处理指令(例如keydown)绑定到我们运用的输入元素上 – 这就是$scope.$apply被调用的地方!而$scope.$watch是在指令的控制器中被调用的。你可以在下面代码中看到这一点:
$scope.$watch(function ngModelWatch() { var value = ngModelGet($scope); //如果作用域模型值和ngModel值没有同步 if (ctrl.$modelValue !== value) { var formatters = ctrl.$formatters, idx = formatters.length; ctrl.$modelValue = value; while(idx--) { value = formatters[idx](value); } if (ctrl.$viewValue !== value) { ctrl.$viewValue = value; ctrl.$render(); } } return value; });
如果你在调用$scope.$watch时只为它传递了一个参数,无论作用域中的什么东西发生了变化,这个函数都会被调用。在ng-model中,这个函数被用来检查模型和视图有没有同步,如果没有同步,它将会使用新值来更新模型数据。这个函数会返回一个新值,当它在$digest函数中运行时,我们就会知道这个值是什么!
为什么我们的监听器没有被触发?
如果我们在$scope.$watch的监听器函数中停止这个监听,即使我们更新了$scope.name,该监听器也不会被触发。
正如前面所提到的,AngularJS将会在每一个指令的控制器函数中运行$scope.$apply。如果我们查看$scope.$apply函数的代码,我们会发现它只会在控制器函数已经开始被调用之后才会运行$digest函数 – 这意味着如果我们马上停止监听,$scope.$watch函数甚至都不会被调用!但是它究竟是怎样运行的呢?
$digest函数将会在$rootScope中被$scope.$apply所调用。它将会在$rootScope中运行digest循环,然后向下遍历每一个作用域并在每个作用域上运行循环。在简单的情形中,digest循环将会触发所有位于$$watchers变量中的所有watchExp函数,将它们和最新的值进行对比,如果值不相同,就会触发监听器。
当digest循环运行时,它将会遍历所有的监听器然后再次循环,只要这次循环发现了”脏值”,循环就会继续下去。如果watchExp的值和最新的值不相同,那么这次循环就会被认为发现了脏值。理想情况下它会运行一次,如果它运行超10次,你会看到一个错误。
因此当$scope.$apply运行的时候,$digest也会运行,它将会循环遍历$$watchers,只要发现watchExp和最新的值不相等,变化触发事件监听器。在AngularJS中,只要一个模型的值可能发生变化,$scope.$apply就会运行。这就是为什么当你在AngularJS之外更新$scope时,例如在一个setTimeout函数中,你需要手动去运行$scope.$apply():这能够让AngularJS意识到它的作用域发生了变化。
创建自己的脏值检查
到此为止,我们已经可以来创建一个小巧的,简化版本的脏值检查了。当然,相比较之下,AngularJS中实现的脏值检查要更加先进一些,它提供疯了异步队列以及其他一些高级功能。
设置Scope
Scope仅仅只是一个函数,它其中包含任何我们想要存储的对象。我们可以扩展这个函数的原型对象来复制$digest和$watch。我们不需要$apply方法,因为我们不需要在作用域的上下文中执行任何函数 – 我们只需要简单的使用$digest。我们的Scope的代码如下所示:
var Scope = function( ) { this.$$watchers = []; }; Scope.prototype.$watch = function( ) { }; Scope.prototype.$digest = function( ) { };
我们的$watch函数需要接受两个参数,watchExp和listener。当$watch被调用时,我们需要将它们push进入到Scope的$$watcher数组中。
var Scope = function( ) { this.$$watchers = []; }; Scope.prototype.$watch = function( watchExp, listener ) { this.$$watchers.push( { watchExp: watchExp, listener: listener || function() {} } ); }; Scope.prototype.$digest = function( ) { };
你可能已经注意到了,如果没有提供listener,我们会将listener设置为一个空函数 – 这样一来我们可以$watch所有的变量。
接下来我们将会创建$digest。我们需要来检查旧值是否等于新的值,如果二者不相等,监听器就会被触发。我们会一直循环这个过程,直到二者相等。这就是”脏值”的来源 – 脏值意味着新的值和旧的值不相等!
var Scope = function( ) { this.$$watchers = []; }; Scope.prototype.$watch = function( watchExp, listener ) { this.$$watchers.push( { watchExp: watchExp, listener: listener || function() {} } ); }; Scope.prototype.$digest = function( ) { var dirty; do { dirty = false; for( var i = 0; i < this.$$watchers.length; i++ ) { var newValue = this.$$watchers[i].watchExp(), oldValue = this.$$watchers[i].last; if( oldValue !== newValue ) { this.$$watchers[i].listener(newValue, oldValue); dirty = true; this.$$watchers[i].last = newValue; } } } while(dirty); };
接下来,我们将创建一个作用域的实例。我们将这个实例赋值给$scope。我们接着会注册一个监听函数,在更新$scope之后运行$digest!
var Scope = function( ) { this.$$watchers = []; }; Scope.prototype.$watch = function( watchExp, listener ) { this.$$watchers.push( { watchExp: watchExp, listener: listener || function() {} } ); }; Scope.prototype.$digest = function( ) { var dirty; do { dirty = false; for( var i = 0; i < this.$$watchers.length; i++ ) { var newValue = this.$$watchers[i].watchExp(), oldValue = this.$$watchers[i].last; if( oldValue !== newValue ) { this.$$watchers[i].listener(newValue, oldValue); dirty = true; this.$$watchers[i].last = newValue; } } } while(dirty); }; var $scope = new Scope(); $scope.name = 'Ryan'; $scope.$watch(function(){ return $scope.name; }, function( newValue, oldValue ) { console.log(newValue, oldValue); } ); $scope.$digest();
成功了!我们现在已经实现了脏值检查(虽然这是最简单的形式)!上述代码将会在控制台中输出下面的内容:
Ryan undefined
这正是我们想要的结果 – $scope.name之前的值是undefined,而现在的值是Ryan。
现在我们把$digest函数绑定到一个input元素的keyup事件上。这就意味着我们不需要自己去调用$digest。这也意味着我们现在可以实现双向数据绑定!
var Scope = function( ) { this.$$watchers = []; }; Scope.prototype.$watch = function( watchExp, listener ) { this.$$watchers.push( { watchExp: watchExp, listener: listener || function() {} } ); }; Scope.prototype.$digest = function( ) { var dirty; do { dirty = false; for( var i = 0; i < this.$$watchers.length; i++ ) { var newValue = this.$$watchers[i].watchExp(), oldValue = this.$$watchers[i].last; if( oldValue !== newValue ) { this.$$watchers[i].listener(newValue, oldValue); dirty = true; this.$$watchers[i].last = newValue; } } } while(dirty); }; var $scope = new Scope(); $scope.name = 'Ryan'; var element = document.querySelectorAll('input'); element[0].onkeyup = function() { $scope.name = element[0].value; $scope.$digest(); }; $scope.$watch(function(){ return $scope.name; }, function( newValue, oldValue ) { console.log('Input value updated - it is now ' + newValue); element[0].value = $scope.name; } ); var updateScopeValue = function updateScopeValue( ) { $scope.name = 'Bob'; $scope.$digest(); };
使用上面的代码,无论何时我们改变了input的值,$scope中的name属性都会相应的发生变化。这就是隐藏在AngularJS神秘外衣之下数据双向绑定的秘密!
更多Example analysis of two-way binding application of data in AngularJS framework相关文章请关注PHP中文网!