AngularJS 多指令 Scope 问题
问题描述
不确定度指令,传入参量类别,然后该指令列出该类别下的所有不确定度。
新增页面用到了三个该指令,只有最后一个成功,前两个都没有数据。
探究
源码
以下是指令源码:
'use strict'; /** * @ngdoc directive * @name webappApp.directive:yunzhiAccuracyUncertainty * @description * # yunzhiAccuracyUncertainty * 不确定度指令 * zhangxishuo */ angular.module('webappApp') .directive('yunzhiAccuracyUncertainty', function($filter) { return { templateUrl: 'views/directive/yunzhiAccuracyUncertainty.html', restrict: 'E', scope: { parameterCategory: '=', // 参量类别 ngModel: '=' // 不确定度 }, link: function postLink(scope, element, attrs) { var self = this; // 初始化 self.init = function() { // 初始化不确定度空列表 scope.accuracyList = []; // 监听参量类别 scope.$watch('parameterCategory', self.watchParameterCategory); // 监听不确定度 scope.$watch('ngModel', self.watchNgModel); }; // 监听参量类别 self.watchParameterCategory = function(newValue) { if (newValue && newValue.id) { // 设置不确定度列表 scope.accuracyList = newValue.accuracyUncertaintyList; // 过滤数据 self.filter(); } }; // 监听不确定度 self.watchNgModel = function(newValue) { if (newValue && newValue.id) { // 设置默认选中 scope.selected = newValue; } }; // 过滤数据 self.filter = function() { angular.forEach(scope.accuracyList, function(accuracy) { // 过滤不确定度 accuracy._value = $filter('yunzhiAccuracyWithUnit')(accuracy); }); }; // 更新模型 self.updateModel = function(selected) { // 更新数据 scope.ngModel = selected; }; // 传给视图 scope.updateModel = self.updateModel; self.init(); } }; });
尝试
尝试打印了一下scope.accuracyList
,果然有问题。
前两个都是空,最后一个数组有值。
想不明白,这里明明监听参量类别,并将scope
的accuracyList
设置了值啊?为什么没有呢?
scope
尝试打印一下scope
。
去关注scope
的$id
就行了。
依次打印的是:
504 508 // 第一个指令 506 508 // 第二个指令 508 508 // 第三个指令
前两个指令执行时赋值的是一个scope
,而过滤的又是另一个scope
,所以过滤不出数据,最后一个是同一scope
,所以正常输出。
原因
官方文档
HTML Compiler
允许开发者教会浏览器一些新的语法,AngularJS
称这个为指令。
Compiler
是一个遍历DOM
去搜寻属性的AngularJS
服务,编译分为以下两个阶段。
Compile
:遍历DOM
并收集所有的指令,返回结果是一个linking
函数。Link
:使用scope
整合指令并产生动态视图,任何scope
模型上的改变都会反映到视图上,任何视图上的用户交互也会反映到scope
模型上。
指令如何编译
AngularJS
操作DOM
节点而不是字符串,这很重要。但通常,你不需要关注这个,因为当页面加载时,浏览器会自动把HTML
转换为DOM
。
指令编译有以下三阶段:
$compile
遍历DOM
并匹配指令,如果Compiler
发现有匹配指令的元素,就会将该指令添加到指令列表中。一个元素可能匹配多个指令。- 一旦所有匹配
DOM
元素的指令都被确定,然后Compiler
会根据优先级对指令进行排序。每一个指令的Compile
函数都会被执行,每一个Compile
函数都有操作DOM
的机会。Compile
会返回Link
函数,这些函数被组合成一个“组合的”Link
函数,它能调用每个指令返回的Link
函数。 $compile
会调用上一步中的“组合的”Link
函数来链接scope
和模板。
下面是官方的示意代码:
// HTML字符串 var html = '<div ng-bind="exp"></div>'; // 将HTML字符串转换为DOM模板 var template = angular.element(html); // 编译DOM模板返回link函数 var linkFn = $compile(template); // 将编译后的模板与scope链接 var element = linkFn(scope); // 添加到DOM中 parent.appendChild(element);
分析
Compile
只在编译时执行一次,只要页面中存在一个该指令,该指令的Link
方法就执行一次。
所以,AngularJS
使用$compile
编译我的指令,然后看我页面中用到了三个该指令,并且都是独立scope
,所以就创建了三个scope
。
然后使用这三个scope
去调用Link
函数。
前面已经提到,AngularJS
会将Link
函数统一组合成一个“组合的”Link
函数,所以我们可以猜想,组合函数中的Link
函数的数量与指令的数量一致,所以三次调用的是一个Link
函数,Link
函数只有一个实例!
linkFn(scope)
将scope
传进去作为Link
函数的入参。
上面的事件监听都是没毛病的,将传入的scope
绑定到视图,然后添加到DOM
中,然后就与这个Link
函数无关了。
但是这个filter
就不行了。
第一个scope
调用,filter
功能是过滤第一个scope
的accuracyList
,第二个scope
调用,filter
功能是过滤第二个scope
的accuracyList
。
所以第三次执行时,第三个scope
将之前的两个都覆盖了,Link
函数中的filter
的作用变成了过滤最后一个scope
的accuracyList
。
<!-- 不确定度 --> <ui-select ng-model="selected" theme="bootstrap" ng-change="updateModel(selected)"> <ui-select-match placeholder="请选择"> {{ $select.selected._value }} </ui-select-match> <ui-select-choices repeat="accuracy in accuracyList"> <div ng-bind-html="accuracy._value"></div> </ui-select-choices> </ui-select>
所以这里下拉框显示的是不确定度过滤后的_value
的值,这里的空字符串看起来不明显,加上test
测试一下。
所以,这块视图绑定的scope
是正确的,只是时间监听之后去过滤数据,因为过滤的并不是当前scope
的数据,所以accuracy._value
就没有值,是undefined
,所以显示一个空的字符串。
解决方案
明白了原理之后解决问题自然易如反掌,只需将filter
与scope
独立即可,这样就不受每次执行不同scope
的影响了。
总结
很多东西,书上是没有的,需要我们自己去发现,去分析,去解决。
翻开了之前遇到指令编译问题时从别人博客里学习来的手动编译方法。
angular.module('webappApp') .directive('reCompile', function($compile) { return { restrict: 'A', link: function postLink(scope, element, attrs) { // 监听使用该指令的元素上的ngBindHtml attrs.$observe('ngBindHtml', function() { // 如果元素使用了ngBindHtml指令 if (attrs.ngBindHtml) { // 重新编译 $compile(element[0].children)(scope); } }); } }; });
记得之前的需求是,数据经过过滤器过滤,返回的是一段HTML
代码,虽然使用ng-bind-html
能将该段代码添加到DOM
中,但是这段代码中有指令,因为该指令不是初始时就有的,所以,这个指令是不会被编译的。
所以需要编写一个重新编译的指令,手动编译动态创建的指令。
记得当时,看这段代码也不是那么完全理解,现在学习完指令的编译之后,再去翻看之前的代码,一切原来是如此简单。
思想与能力,是需要时间沉淀的。