創建自定義 AngularJS 指令:Part 3 孤立作用域與函數參數


在本系列的第二篇文章中,我介紹了孤立作用域,以及它怎樣使指令更具有複用性。孤立作用域一個大的方面是:依賴符號@,=和&進行數據和函數傳遞的指令內部scope屬性。通過這些屬性,你可以給指令傳入或從指令傳出數據。如果你是初次接觸指令內部作用域,我建議你首先閱讀本系列的第一和第二篇文章。在這篇文章中,我將介紹如何給傳入指令的函數添加參數。


孤立作用域和函數參數


在使用指令內部作用域屬性時,你可以將一個外部函數(例如:在外部控制器的$scope上定義的函數)傳遞給指令。這個是通過&符類型的內部屬性實現的。下邊例子是一個名爲name的內部屬性,它和一個外部函數相關聯:

angular.module('directivesModule').directive('isolatedScopeWithController', function () {
    return {
        restrict: 'EA',
        scope: {
            datasource: '=',
            add: '&',
        },
        controller: function ($scope) {

            ...            
            
            $scope.addCustomer = function () {
                //Call external scope's function
                var name = 'New Customer Added by Directive';
                $scope.add();

                //Add new customer to directive scope
                $scope.customers.push({
                    name: name                
                });
            };
        },
        template: '<button ng-click="addCustomer()">Change Data</button><ul>
                   <li ng-repeat="cust in customers">{{ cust.name }}</li></ul>'
    };
});
指令的使用者可以通過下邊方式傳遞一個外部函數給指令:

<div isolated-scope-with-controller datasource="customers" add="addCustomer()"></div>

當用戶點擊指令內部創建的按鈕時,addCustomer()函數將會被執行。由於沒有參數傳遞,這將是一個相對明確的操作。

你怎樣傳遞參數給addCustomer()函數或其它函數?例如:假設下邊控制器中的addCustomer()函數在調用時,需要傳入一個名爲name的參數:

var app = angular.module('directivesModule', []);

app.controller('CustomersController', ['$scope', function ($scope) {
    var counter = 0;
    $scope.customer = {
        name: 'David',
        street: '1234 Anywhere St.'
    };
    
    $scope.customers = [];

    $scope.addCustomer = function (name) {
        counter++;
        $scope.customers.push({
            name: (name) ? name : 'New Customer' + counter,
            street: counter + ' Cedar Point St.'
        });
    };

    $scope.changeData = function () {
        counter++;
        $scope.customer = {
            name: 'James',
            street: counter + ' Cedar Point St.'
        };
    };
}]);
Passing a parameter out of the directive to the external function is quite straightforward once you know the trick but requires a bit of upfront knowledge to get it working properly. Here’s what most devs try to do initially:

當你掌握了訣竅時,給傳入指令的外部函數添加參數將變得很簡單,但是想要讓它很好的工作還是需要一點必備的知識。大部分指令初學者做法如下:

angular.module('directivesModule').directive('isolatedScopeWithController', function () {
    return {
        restrict: 'EA',
        scope: {
            datasource: '=',
            add: '&',
        },
        controller: function ($scope) {
            ...


            $scope.addCustomer = function () {
                //Call external scope's function
                var name = 'New Customer Added by Directive';
                $scope.add(name);

                //Add new customer to directive scope
                $scope.customers.push({
                    name: name                });
            };
        },
        template: '<button ng-click="addCustomer()">Change Data</button><ul>' +
                  '<li ng-repeat="cust in customers">{{ cust.name }}</li></ul>'
    };
});

可以看到,指令的控制器中通過$scope.add(name)語句來調用外部函數,並且傳入一個參數值。這樣會成功嗎?事實證明,外部函數中的參數值是undefined,這將使你撓頭問爲什麼。那麼,我們將會怎樣做呢?


方法1:使用一個對象類型的參數


在這種情況下,一種可行的方案是傳遞一個對象參數。下邊是一個怎樣從指令中傳遞name值給外部函數的示例:

angular.module('directivesModule').directive('isolatedScopeWithController', function () {
    return {
        restrict: 'EA',
        scope: {
            datasource: '=',
            add: '&',
        },
        controller: function ($scope) {
            ...

           
            $scope.addCustomer = function () {
                //Call external scope's function
                var name = 'New Customer Added by Directive';
                $scope.add({ name: name });

                //Add new customer to directive scope
                $scope.customers.push({
                    name: name,
                    street: counter + ' Main St.'
                });
            };
        },
        template: '<button ng-click="addCustomer()">Change Data</button>' +
                  '<ul><li ng-repeat="cust in customers">{{ cust.name }}</li></ul>'
    };
});

可以看到,指令內部語句$scope.add({ name: name })傳遞一個對象類型的參數,對象的屬性name就是我們想要傳遞的參數。不幸的是,這個函數仍然不能被執行!爲什麼呢?這個name參數必須寫在外部函數被關聯到指令屬性的地方。具體做法如下邊例子:

<div isolated-scope-with-controller datasource="customers" add="addCustomer(name)"></div>
需要特別注意的一點是:這裏的傳遞的是一個參數,而到外部函數執行時,參數是一個字符串。並且,指令內部$scope.add({ name: "..."})name屬性名必須和上邊代碼中div標籤屬性中add="addCustomer(name)"的參數名name完全一樣。

可以看出,外部函數的參數名在指令內部調用函數時對象類型參數的屬性中匹配,匹配到相同名字的屬性時,將其值賦給外部參數,這樣參數傳遞成功。雖讓不是很直觀,但是確實是一種有效的方法。


方法2:存儲一個函數引用並調用它


使用方法1的要點是保證指令使用時參數名已經被定義,並且,它會匹配指令傳出對象參數中的屬性名,如果沒有任何一個屬性名匹配上,這種方法就行不通。即使這種方法可以達到目的,但有許多複雜的東西,我稱之爲"侷限性知識",而且,如果指令說明文檔不是很詳細時,將無法在不看指令源碼情況下給外部函數參數命名。

另外一個方法是在html標籤屬性中定義函數時,不添加圓括號。如下所示:

<div isolated-scope-with-controller-passing-parameter2 datasource="customers" add="addCustomer"></div>
給外部函數addCustomer傳遞參數,你可以像下邊這樣。將$scope.add()(name)放到指令代碼中,看看addCustomer函數是怎樣在傳遞一個name參數的情況下被調用的。

angular.module('directivesModule').directive('isolatedScopeWithControllerPassingParameter2', function () {
    return {
        restrict: 'EA',
        scope: {
            datasource: '=',
            add: '&',
        },
        controller: function ($scope) {
            
            ...

            $scope.addCustomer = function () {
                //Call external scope's function
                var name = 'New Customer Added by Directive';

                $scope.add()(name);

                ...          
            };
        },
        template: '<button ng-click="addCustomer()">Change Data</button><ul>' +
                  '<li ng-repeat="cust in customers">{{ cust.name }}</li></ul>'
    };
});
這種方法的原理是什麼?它爲什麼就可以呢?在$scope.add()(name)代碼處打斷點調試,在瀏覽器控制檯中執行$scope.add()代碼,輸出結果如下。可以看出$scope.add()得到函數的定義。這是因爲當這個函數被綁定給add內部屬性時,沒有加圓括號導致的,如上邊所示。

第二個圓括號($scope.add()(name))的作用是調用這個函數,並傳遞name參數的值。這當然要求指令的使用者移除圓括號(在你的項目組中,這或許是或者不是'正常的'),但是相比方法1,在我看來,這種方法更加乾淨利索。


&類型的內部屬性-透過現象看本質


如果你對一些導致結果的內部過程感興趣,當一個&類型的內部屬性在一個指令中被調用時(例如:上邊的add內部屬性),下邊的代碼就是被調用時執行的代碼:

case '&':
    parentGet = $parse(attrs[attrName]);
    isolateScope[scopeName] = function(locals) {
        return parentGet(scope, locals);
    };
break;
attrName代表上邊例子中指令內部屬性addparentGet函數是$parse函數執行的結果,其內容如下所示:

function (scope, locals) {
      var args = [];
      var context = contextGetter ? contextGetter(scope, locals) : scope;

      for (var i = 0; i < argsFn.length; i++) {
        args.push(argsFn[i](scope, locals));
      }
      var fnPtr = fn(scope, locals, context) || noop;

      ensureSafeObject(context, parser.text);
      ensureSafeObject(fnPtr, parser.text);

      // IE stupidity! (IE doesn't have apply for some native functions)
      var v = fnPtr.apply
            ? fnPtr.apply(context, args)
            : fnPtr(args[0], args[1], args[2], args[3], args[4]);

      return ensureSafeObject(v, parser.text);
}

這段代碼處理對象屬性和外部函數參數的映射,並調用這個函數。當你使用&類型的內部屬性時,你並不需要知道這些,但可以瞭解到現象背後的原理,是一件有意思的事。



總結


總之,在指令中傳遞參數有些複雜和曲折。但是,如果你知道了這個過程是怎樣工作的,使用它並不困難。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章