3

问题描述

不确定度指令,传入参量类别,然后该指令列出该类别下的所有不确定度。

新增页面用到了三个该指令,只有最后一个成功,前两个都没有数据。

clipboard.png

clipboard.png

clipboard.png

探究

源码

以下是指令源码:

'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,果然有问题。

clipboard.png

前两个都是空,最后一个数组有值。

clipboard.png

想不明白,这里明明监听参量类别,并将scopeaccuracyList设置了值啊?为什么没有呢?

clipboard.png

scope

尝试打印一下scope

clipboard.png

clipboard.png

clipboard.png

去关注scope$id就行了。

依次打印的是:

504
508         // 第一个指令
506
508         // 第二个指令
508
508         // 第三个指令

前两个指令执行时赋值的是一个scope,而过滤的又是另一个scope,所以过滤不出数据,最后一个是同一scope,所以正常输出。

原因

官方文档

HTML Compiler - AngularJS

HTML Compiler允许开发者教会浏览器一些新的语法,AngularJS称这个为指令。

Compiler是一个遍历DOM去搜寻属性的AngularJS服务,编译分为以下两个阶段。

  1. Compile:遍历DOM并收集所有的指令,返回结果是一个linking函数。
  2. Link:使用scope整合指令并产生动态视图,任何scope模型上的改变都会反映到视图上,任何视图上的用户交互也会反映到scope模型上。

指令如何编译

AngularJS操作DOM节点而不是字符串,这很重要。但通常,你不需要关注这个,因为当页面加载时,浏览器会自动把HTML转换为DOM

指令编译有以下三阶段:

  1. $compile遍历DOM并匹配指令,如果compiler发现有匹配指令的元素,就会将该指令添加到指令列表中。一个元素可能匹配多个指令。
  2. 一旦所有匹配DOM元素的指令都被确定,然后compiler会根据优先级对指令进行排序。每一个指令的compile函数都会被执行,每一个compile函数都有操作DOM的机会。compile会返回link函数,这些函数被组合成一个“组合的”link函数,它能调用每个指令返回的link函数。
  3. $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函数的入参。

clipboard.png

上面的事件监听都是没毛病的,将传入的scope绑定到视图,然后添加到DOM中,然后就与这个link函数无关了。

clipboard.png

但是这个filter就不行了。

第一个scope调用,filter功能是过滤第一个scopeaccuracyList,第二个scope调用,filter功能是过滤第二个scopeaccuracyList

所以第三次执行时,第三个scope将之前的两个都覆盖了,link函数中的filter的作用变成了过滤最后一个scopeaccuracyList

clipboard.png

<!-- 不确定度 -->
<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测试一下。

clipboard.png

clipboard.png

所以,这块视图绑定的scope是正确的,只是时间监听之后去过滤数据,因为过滤的并不是当前scope的数据,所以accuracy._value就没有值,是undefined,所以显示一个空的字符串。

clipboard.png

解决方案

明白了原理之后解决问题自然易如反掌,只需将filterscope独立即可,这样就不受每次执行不同scope的影响了。

clipboard.png

总结

很多东西,书上是没有的,需要我们自己去发现,去分析,去解决。

翻开了之前遇到指令编译问题时从别人博客里学习来的手动编译方法。

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中,但是这段代码中有指令,因为该指令不是初始时就有的,所以,这个指令是不会被编译的。

所以需要编写一个重新编译的指令,手动编译动态创建的指令。

记得当时,看这段代码也不是那么完全理解,现在学习完指令的编译之后,再去翻看之前的代码,一切原来是如此简单。

思想与能力,是需要时间沉淀的。


张喜硕
2.1k 声望423 粉丝

浅梦辄止,书墨未浓。


引用和评论

0 条评论