提示:我打算分解这一处理变化的过程,因为我想演示关于指令在AngularJS与js之间经常发生的一个问题,并解释解决方案。
在directives.html文件中改变值:
<!DOCTYPE html> <html lang="en" ng-app="exampleApp"> <head> <meta charset="UTF-8"> <title>Directives</title> <script src="angular.js"></script> <link rel="stylesheet" href="bootstrap.css"/> <link rel="stylesheet" href="bootstrap-theme.css"/> <script> angular.module("exampleApp", []) .directive("unorderedList", function () { return function (scope, element, attrs) { var data = scope[attrs["unordered-list"]]; var propertyExpression = attrs["list-property"]; if(angular.isArray(data)){ var listElem = angular.element("<ul>"); element.append(listElem); for(var i = 0; i<data.length; i++){ listElem.append(angular.element("<li>").text(scope.$eval(propertyExpression, data[i]))); } } } }) .controller("defaultCtrl", function ($scope) { $scope.products = [ {name: "Apples", price: 1.20}, {name: "Bananas", price: 2.42}, {name: "Pears", price: 2.02} ]; $scope.incrementPrices = function () { for(var i = 0; i<$scope.products.length; i++){ $scope.products[i].price++; } } }) </script> </head> <body ng-controller="defaultCtrl"> <div class="panel panel-default"> <div class="panel-heading"> <h3>Products</h3> </div> <div class="panel-body"> <button class="btn btn-primary" ng-click="incrementPrices()">Change Prices</button> </div> <div class="panel-body"> <div unordered-list="products" list-property="price | currency"></div> </div> </div> </body> </html>1.添加监听器
$watch方法来监控作用域中的变化。这一过程对于自定义指令来说要更复杂一些,因为是从一个属性值中获取待计算的表达式,正如你所看到的,这需要一个额外的预备过程。
if(angular.isArray(data)){ var listElem = angular.element("<ul>"); element.append(listElem); for(var i = 0; i<data.length; i++){ var itemElem = augular.element('<li>'); listElem.append(itemElem); var watcherFn = function (watchScope) { return watchScope.$eval(propertyExpression, data[i]); }; $scope.$watch(watcherFn, function (newValue, oldValue) { itemElem.text(newValue); }) } }在本例中,使用了两个函数。第一个函数(监听器函数)基于作用域的数据计算出一个值,该函数在每次作用域发生变化时都会被调用。如果该函数的返回值发生了变化,处理函数就会被调用。
提供一个函数来进行监听,让我能从容面对表达式中有可能包含带有过滤器的数据值的情况。这里定义了这样一个监听器函数:
var watcherFn = function (watchScope) { return watchScope.$eval(propertyExpression, data[i]); };每次监听器函数被重新计算时,该函数作为参数被传给作用域,另外使用了$eval函数计算在使用的表达式,并使用一个数据对象作为属性值的来源。我可以将这个监听器函数传给$watch方法并指定回调函数,该回调函数使用jqLite的text函数更新li元素的文本内容,以反映数据值的变化。
scope.$watch(watcherFn, function (newValue, oldValue) { itemElem.text(newValue); })效果是指令能够监控beili元素显示的属性值,并在其值被改变时更新元素的内容。
2.修复词法作用域的问题
在浏览器中加载文件,指令并不会将li元素一直保持为更新。如果查看DOM中的HTML元素,将会看到li元素下没有包含任何内容。这是一个如此常见的问题,以至于我想演示一下如何修复它,即使是由于js的特性而不是angularjs的特性造成。
问题在于这条语句:
var watcherFn = function (watchScope) {
return watchScope.$eval(propertyExpression, data[i]);
};
JS支持闭包,允许函数引用其作用域之外的变量。没有闭包,就得确保为你的函数要访问的每一个对象和值定义参数。
容易混淆之处在于,函数所访问的变量是在函数被调用时进行计算的,而不是函数被定义时。对于此处的监听器函数,意味着知道angularjs调用这个函数时变量i才会被计算,也就是说时间的发生顺序是这样的:
1.angularjs调用链式函数来建立指令
2.for循环开始遍历products数组的各个元素
3.i的值为1,对应数组的第一个元素
4.for循环将i加1,变为1,对应数组的第二个元素
5.i=2
6.i变为3,已经大于数组的长度
7.for循环结束
8.angular计算这三个分别涉及data[i]的监听器函数
在第8步发生时,i=3,这意味着所有三个监听器函数都试图访问一个数据数组中并不存在的对象,这就是指令为什么不工作的原因了。
要解决这一问题,需要对比包的特性加以控制,以便使用一个固定的或有界的变量来引用数据对象,也就是说只需要让赋值给变量的值在第3~5步中设置,而不是angularjs在计算监听器函数是设置就可以了。
if(angular.isArray(data)){ var listElem = angular.element("<ul>"); element.append(listElem); for(var i = 0; i<data.length; i++){ (function () { var itemElem = angular.element('<li>'); listElem.append(itemElem); var index = i; var watcherFn = function (watchScope) { return watchScope.$eval(propertyExpression, data[index]); }; scope.$watch(watcherFn, function (newValue, oldValue) { itemElem.text(newValue); }) })(); } }这里在for循环内部定义了一个“自执行函数”,允许我定义一个index变量,并将i的当前值赋值给它。因为自执行函数是在一定义是就被执行的,所以index的值将不会被for循环的下一个迭代所更新,这也意味着在监听器函数里可以从数组数据访问到正确的对象。