AngularJS內幕詳解之 Scope

在AngularJS的代碼庫中呈現出了大量有趣的設計,最有趣的兩個例子是scope的工作方式和directives(指令)的表現。

有的人第一次接觸AngularJS時就被告知directives是和DOM交互,或供你隨意操作DOM,就像jQuery. 這立馬變得非常複雜,試想,scopes, directives 和controllers相互作用.

複雜的設置之後,你開始學習它先進的思想:the digest cycle,獨立的scope、內嵌以及指令中不同的鏈接函數。這些已經非常負責,這篇文章不會涉及指令,但會在後續的文章中寫到。

這篇文字將帶你跳過AngularJS 的scopes和AngularJS應用生命週期的各種坑,同時提供有趣信息的深度閱讀。

(門檻是高的,但scope是很難解釋的。如果在此慘遭失敗,至少我會拋出幾個重要的點。)

如果這個圖表看起來非常的費解,那麼這篇文章很適合你。

01-mindbender

(Image credit) (View large version)

(免責聲明:這篇文字是基於 AngularJS version 1.3.0

AngularJS 用scopes分離指令和DOM的通信。scopes也存在於controller層。scopes 是普通的JavaScript對象,AngularJS沒有過多的操作。只是添加了“一串”帶有一個或兩個$符號前綴的內部屬性。其中以$$開頭的常常不是必須的,經常用它們作代碼氣味,可以避免更深入的理解digest循環。

在AngualrJS 俚語中,scope”不是你平時思考Javascript代碼或編程的scope。scope通常適用於維持一段代碼塊的上下文、變量等。通常情況下,scope用來包含一段代碼中的上下文、變量等。

(在大多數語言中,變量維持在一個用花括號({})或代碼塊定義的虛擬袋中,這就是大家所說的塊級作用域.相比之下,Javascript 使用的是詞法作用域,大致意思是使用函數或者全局對象定義虛擬袋而不是代碼塊。虛擬袋可以嵌套更多小的袋子。....)

下面用一個簡單的例子來解釋函數

function eat (thing) {
   console.log('Eating a ' + thing);
}

function nuts (peanut) {
   var hazelnut = 'hazelnut';

   function seeds () {
      var almond = 'almond';
      eat(hazelnut); // I can reach into the nuts bag!
   }

   // Almonds are inaccessible here.
   // Almonds are not nuts.
}

我不在贅述這個問題,因爲這不是人們談論AngularJS時的scope,如果你喜歡瞭解更多關於JavaScript語言上下文的scope請參考“Where Does this Keyword Come From?

在Angular的條款中,scope也是結合上下文的。AngularJS中,一個scope跟一個元素關聯(以及所有它的子元素),而一個元素不是必須直接跟一個scope關聯。元素通過以下三中方式被分配一個scope:

1、scope通過controller或者directive創建在一個element上(指令不總是引入新的scope)

<nav ng-controller='menuCtrl'>

2、如果一個scope不存在於元素上,那麼它將繼承它的父級scope

<nav ng-controller='menuCtrl'>
   <a ng-click='navigate()'>Click Me!</a> <!-- also <nav>'s scope -->
</nav>

3、如果一個元素不是某個ng-app的一部分,那麼它不屬於任何scope。

<head>
   <h1>Pony Deli App</h1>
</head>
<main ng-app='PonyDeli'>
   <nav ng-controller='menuCtrl'>
      <a ng-click='navigate()'>Click Me!</a>
   </nav>
</main>

爲了弄清楚元素的scope,試着由內到外遞歸我剛剛列出的3條元素規則??? 它創建了一個新的scope?那是它的scope,它有父級嗎?檢查它的父級是某個ng-app的一部分嗎?。遺憾的是---沒有scope

你可以用充滿魔力的開發者工具輕鬆的找出元素的scope。

在介紹digest如何工作以及內部如何表現前,我會剖析scope的一些屬性來介紹某些概念。我也會讓你知道我如何獲取這些屬性。首先,打開 Chrome 並導航到我正在使用的一個 angular應用程序。然後,我將審查一個元素並打開開發者工具。

(你知道$0能讓你獲得最後一個選中的元素嗎?$1讓你能訪問前一個被選中的元素等。我想你會經常用到$0,尤其是使用 AngularJS工作時。)

對於每一個DOM元素, 用 angular.element 包裝跟使用jQuery 或 jqLite, jQuery 的 迷你版是一樣的。 一旦被包裝, 你通過 scope() 函數得到的結果 —— 你猜對了!—— 就是跟元素關聯的 AngularJS scope。結合$0,我發現自己經常使用下面的命令。

angular.element($0).scope()

(當然,如果你使用jQuery,那麼 $($0).scope() 也會達到一樣的效果。不管 jQuery 是否可用, angular.element 一直可用。)

然後我能審查該 scope, 確定該 scope 是我預期的, 確定屬性的值是否跟我預期的匹配。讓我們來看看一個典型的scope中可訪問的特殊屬性, 它們以一個或多個 $ 符號開頭。

for(o in $($0).scope())o[0]=='$'&&console.log(o)

這就足夠了,我將遍歷所有的屬性,

下面,我列出了由該命令產生的屬性,按功能分組。讓我們從基本的開始,它僅僅提供scope導航。

  • $id scope 的唯一標識

  • $root 根scope

  • $parent 父級scope, 如果 scope == scope.$root 則爲 null

  • $$childHead 第一個子 scope, 如果沒有則爲 null

  • $$childTail 最後一個子scope, 如果沒有則爲 null

  • $$prevSibling 前一個相鄰節點 scope, 如果沒有則爲 null

  • $$nextSibling 下一個相鄰節點 scope, 如果沒有則爲 null

這沒什麼驚喜, 瀏覽 scope 沒什麼意思。有時候訪問 $parent 看起來是得當的, 但是有更好的、低耦合的方式來處理父級通信而不是緊緊的與人爲的scope綁定在一起。 比如說 使用事件監聽器,下面我們將講到。

下面介紹的屬性允許我們發佈事件和訂閱事件。這個模式叫發佈/訂閱

  • $$listeners 在scope上註冊事件監聽器。

  • $on(evt, fn) 註冊一個名爲evt,監聽器爲fn的事件。

  • $emit(evt, args) 發送事件 evt, 在scope 鏈上冒泡,在當前scope 以及所有的 $parents 上觸發,包括 $rootScope。

  • $broadcast(evt, args) 發送事件 evt, 在當前scope 以及它 所有的 children 上觸發。

當事件觸發的時候, 事件監聽器傳遞 event 對象和其它參數到 $emit 或者 $broadcast 函數。有很多方式爲 scope 上的事件傳遞值。

指令可以通過事件來告知一些重要的事情發生了。查看下面的指令示例,一個按鈕被點擊後,告知你喜歡吃什麼類型的食物。

angular.module('PonyDeli').directive('food', function () {
   return {
      scope: { // I'll come back to directive scopes later
         type: '=type'
      },
      template: 'I want to eat some {{type}}!',
      link: function (scope, element, attrs) {
         scope.eat = function () {
            letThemHaveIt();
            scope.$emit('food.order, scope.type, element);
         };

         function letThemHaveIt () {
            // Do some fancy UI things
         }
      }
   };
});

我爲事件添加了命名空間,你也應該這麼做。它可以防止命名衝突,你可以清楚的知道事件的來源或者你正在訂閱的是什麼事件。如果你對分析數據感興趣或者想追蹤 food 的元素點擊,可以用 Mixpanel。這確實很有意義,並且沒有理由污染你的指令或者控制器。你可以用一個指令來處理 food 點擊的分析和追蹤,這是一個很好的自足的方式。

angular.module('PonyDeli').directive('foodTracker', function (mixpanelService) {
   return {
      link: function (scope, element, attrs) {
         scope.$on('food.order, function (e, type) {
            mixpanelService.track('food-eater', type);
         });
      }
   };
});

這個服務的實現是不貼切的,因爲它僅僅是對Mixpanel的客戶端API的封裝。 它的HTML看起來應該像下面的樣子, 我用了一個 controller 來維護所有的實物類型。ng-app指令很好的幫助 AngularJS 自動啓動我的程序。爲了完善這個例子,我添加了一個 ng-repeat指令來渲染我所有的 food 而不是重複自身。這只是通過 foodTypes 循環,在 foodCtrl 的scope 中是可以訪問的。

<ul ng-app='PonyDeli' ng-controller='foodCtrl' food-tracker>
   <li food type='type' ng-repeat='type in foodTypes'></li>
</ul>

angular.module('PonyDeli').controller('foodCtrl', function ($scope) {
   $scope.foodTypes = ['onion', 'cucumber', 'hazelnut'];
});

完整的項目例子託管在 CodePen

表面上看是一個很好的例子,但你應該思考是否需要一個事件給其它人訂閱。也許一個服務就行。這種情況。你可以用另一種方式。 你會問自己你需要事件因爲你不知道誰會訂閱food.order,這意味着使用事件更加面向未來。你也會說 food 追蹤指令沒有理由存在,因爲它沒有跟 DOM ,甚至是 scope 交互, 只是監聽了一個事件,你可以使用 service 替換它。

在剛剛的情況下,這兩種說法都是對的。當更多的組件需要 food.order-aware 時,纔會感覺事件的做法更清晰。事實上,事件很有用,當你真的需要橋接兩個 scope 的間隙時,其它的因素就不那麼重要了。

正如我們所見,在本文即將到來的第二部分更加仔細的審查指令,在 scope 間通信時事件甚至不是必要的。一個子 scope 可以它的父scope讀取,通過父scope綁定它,它也可以更新這些值。

(很少情況下通過事件來幫助子 scope 與 父 scope 更好的通信。)

同級之前往往很難相互通信, 經常通過它們共同的父級scope 來通信。通常從 $rootScope 開始廣播,然後在你希望的相鄰節點監聽,如下:

<body ng-app='PonyDeli'>
   <div ng-controller='foodCtrl'>
      <ul food-tracker>
         <li food type='type' ng-repeat='type in foodTypes'></li>
      </ul>
      <button ng-click='deliver()'>I want to eat that!</button>
   </div>
   <div ng-controller='deliveryCtrl'>
      <span ng-show='received'>
         A monkey has been dispatched. You shall eat soon.
      </span>
   </div>
</body>

angular.module('PonyDeli').controller('foodCtrl', function ($rootScope) {
   $scope.foodTypes = ['onion', 'cucumber', 'hazelnut'];
   $scope.deliver = function (req) {
      $rootScope.$broadcast('delivery.request', req);
   };
});

angular.module('PonyDeli').controller('deliveryCtrl', function ($scope) {
   $scope.$on('delivery.request', function (e, req) {
      $scope.received = true; // deal with the request
   });
});

這個例子也放在 CodePen

漸漸的,你將對事件和服務越來越熟悉。我想告訴你,當你期望視圖模塊改變來響應事件,你應該使用 event, 當你不期望視圖模塊改變,你應該使用 service。有時候響應是這兩種的混合:一個動作觸發了一個事件,事件調用了一個 service, 或者 service 從 $rootScope廣播了一個事件。這視情況而定,並且你應該這樣分析,而不是隨意使用一個方法。

如果你有兩個組件通過 $rootScope 通信,你可能更喜歡使用 $rootScope.$emit (而不是 $broadcast)和 $rootScope.$on。這種方式下, 事件只會在 $rootScope.$$listeners 之間傳播, 那些你知道沒有該事件的監聽器的後代的 $rootScope上,不會循環浪費時間。

angular.module('PonyDeli').factory("notificationService", function ($rootScope) {
   function notify (data) {
      $rootScope.$emit("notificationService.update", data);
   }

   function listen (fn) {
      $rootScope.$on("notificationService.update", function (e, data) {
         fn(data);
      });
   }

   // Anything that might have a reason
   // to emit events at later points in time
   function load () {
      setInterval(notify.bind(null, 'Something happened!'), 1000);
   }

   return {
      subscribe: listen,
      load: load
   };
});

CodePen

事件跟服務已經差不多了,讓我們移步到一些其它的屬性。

瞭解這個恐怖的過程是認識 AngularJS的關鍵。

AngularJS 基於它的數據綁定的特性,通過循環髒檢測來追蹤變化並且在變化時觸發事件。這比聽起來簡單。事實上, 它就是這樣簡單。讓我們快速的瀏覽一下 $digest 循環的核心組件。 首先,有一個 scope.$digest 方法,通過遞歸檢測scope 和它的後代們的變化。

  1. $digest()
    執行 $digest 循環髒檢測

  2. $$phase digest 循環的當前階段, [null, '$apply', '$digest'] 中的一個。

你需要小心觸發 digest,因爲當你已經在一個 digest 階段而嘗試這麼做, 會因爲一些無法解釋的現象導致 AngularJS 出錯。

讓我們看看 文檔裏關於 $digest怎麼說

Processes all of the watchers of the current scope and its children. Because a watcher’s listener can change the model, the $digest() keeps calling the watchers until no more listeners are firing. This means that it is possible to get into an infinite loop. This function will throw 'Maximum iteration limit exceeded.' if the number of iterations exceeds 10.

Usually, you don’t call $digest() directly in controllers or in directives. Instead, you should call $apply() (typically from within a directives), which will force a $digest().


它處理當前scope 及其 後代們的所有的 watchers。因爲 watcher 的監聽器可以改變 model,$digest() 持續調用 watcher 直到沒有監聽器被觸發。這意味着可能進入死循環。如果迭代超過10次,這個函數會拋出異常 'Maximum iteration limit exceeded'。

通常,我們在控制器或指令中不直接調用 $digest。相反,你應該調用$apply() (通常在一個指令裏) 用來強制執行一個 $digest()。

所以,一個 $digest 處理所有的 watcher,處理這些 watcher時,這些watcher觸發,直到沒有別的觸發 watcher。爲了我們理解這個循環,仍然有兩個問題需要解答。

  • "watcher" 是什麼鬼?!
  • 什麼觸發 $digest?!

這兩個問題的答案因複雜度差異很大,但是我會盡量爲你解釋清楚。我將介紹 watcher,讓你有個自己的看法。

如果你讀過這一步,你或許已經知道什麼事 watcher了。你或許使用過 scope.$watch,甚至用過scope.$watchCollection。$$watchers 屬性有用 scope 上所有的 watcher。

  • $watch(watchExp, listener, objectEquality) 爲scope添加一個 watch 監聽器

  • $watchCollection watch 數組元素或對象屬性

  • $$watchers 保持所有的 watch 與 scope 的關聯

    watcher 是AngularJS的數據綁定功能的很重要的一面, 但是爲了觸發這些 watcher,AngularJS 需要我們的幫助。否則,它不能有效的更新數據綁定的變量爲正確值。思考下面的例子:

<body ng-app='PonyDeli'>
   <ul ng-controller='foodCtrl'>
      <li ng-bind='prop'></li>
      <li ng-bind='dependency'></li>
   </ul>
</body>

angular.module('PonyDeli').controller('foodCtrl', function ($scope) {
   $scope.prop = 'initial value';
   $scope.dependency = 'nothing yet!';

   $scope.$watch('prop', function (value) {
      $scope.dependency = 'prop is "' + value + '"! such amaze';
   });

   setTimeout(function () {
      $scope.prop = 'something else';
   }, 1000);
});

我們有初始值 'initial value',我們期望第二行 HTML 變爲 'prop is "something else"! 驚喜發生在1秒後,對嗎? 甚至更有趣的是,你至少期待第一行變爲'something else'!爲什麼不是呢?這是個 watcher 嗎?

實際上,你在 HTML 標籤上的一系列操作都是以創建 watcher 結束。這種情況下,每個 ng-bind 指令創建一個 watcher,當 prop、 dependency 變化時它會更新 HTML <li>。

這樣,你現在可以把你的代碼想象成3個 watcher,每個 ng-bind 指令的watcher, 還有一個在控制器中的。AngularJS如何知道 timeout 執行後的屬性變化?你應該想起 AngularJS 更新屬性時,可以在 timeout 回調時添加一個手動的 digest。

setTimeout(function () {
   $scope.prop = 'something else';
   $scope.$digest();
}, 1000);

我放置了一個沒有 $digest 的例子在 CodePen,以及 timeout有 $digest的。你可以用$timeout service 替換 setTimeout,它提供了一些錯誤處理,並且會執行 $apply()。

$timeout(function () {
   $scope.prop = 'something else';
}, 1000);
  • $apply(expr) 解析和計算一個表達式,然後在 $rootScope 上執行 $digest 循環

爲了在每個 scope 上執行 digest, $apply 提供了很好的錯誤處理功能。如果你嘗試調優性能, 使用 $digest 或許能夠保證, 但是在我瞭解 AngularJS 內部工作原理而感覺良好之前, 我會遠離它。實際上很少時候需要手動調用 $digest();$apply 總是更好的選擇。

現在我們回到第二個問題上來。

  • 什麼觸發了 $digest?!

Digest的內部觸發在AngularJS代碼庫中具有重要地位。它們的要麼直接被觸發,要麼是調用 $apply() 觸發,就像我們在 $timeout 服務裏看到的。不管是AngularJS中的核心還是邊緣的指令都會觸發digest。 digest 觸發你的 watcher, watcher更新你的 UI。這是基本的思路。

你可以從 AngularJS wiki裏面找到關於好的實踐資源,鏈接在文章底部。

我已經解釋了 watcher 和 $digest 循環如何相互交互。下面,我將列出一些與 $digest 循環相關的屬性,它們可以在 scope 上找到。 這些可以幫助你在 AngularJS 編譯時解析文本表達式, 或者在 digest 循環的不同階段執行一小段代碼。

在最後的部分,來聊聊 scope 的性能。儘管有時候你可能要用 $new 聲明自己的scope, 但它們使用內部手段處理 scope 的生命週期。

  • $$isolateBindings 獨立 scope 綁定(例如:{ options: '@megaOptions' })

  • $new(isolate) 創建一個子 scope 或者一個獨立的 scope, 它不繼承自它們的父級。

  • $destroy 從 scope 鏈裏移除該 scope; scope 和後代們不會收到事件, watcher 也不再被觸發。

  • $$destroyed scope 是否被銷燬。



源引:http://www.w3ctech.com/topic/1611

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