Pull to refresh

Понимание областей видимости или Scope в AngularJS

Reading time 10 min
Views 78K
Original author: Mark Rajcok
В Ангуляре, дочерняя область видимости обычно прототипически наследуется от родительской. Единственным исключением является директива, в которой используется scope: { ... }, создающая «изолированную» область видимости, не наследуемую прототипически. Такая конструкция часто используется при создании директив для компонентов «многоразового использования»

Наследование областей, как правило, прямое, и часто даже не нужно знать, как оно делается… пока не столкнешься с двухсторонней привязкой данных (т. е. элементами формы, ng-model) к примитивам (напр., числу, строке, логическому типу), определенными в родительской области видимости из дочерней. Она работает не так, как этого ожидают большинство людей. Происходит так, что потомок создает собственную область видимости, которая перекрывает родительское свойство с одноименным названием. Это не особенность Ангуляра, так работает прототипное наследование в Яваскрипте. Новые разработчики, работающие с Ангуляром, часто не понимают, что ng-repeat, ng-switch, ng-view и ng-include создают новые дочерние области, так что проблема появляется при использовании этих директив.

Этой проблемы легко избежать, следуя «лучшим практикам», где говорится, что выражение в ng-model должно всегда содержать точку.

Точка «.» в модели гарантирует, что прототипное наследование работает как надо. Поэтому
лучше чем
.

Если действительно хотите/нужно использовать примитивы, есть два пути решения проблемы:

1. Используйте $parent.parentScopeProperty
в дочерней области видимости. Это запретит дочерней области создавать собственное свойство.
2. Определите функцию в родительской области видимости, и обратитесь к ней из потомка, передавая элементарное значение в родителя (не всегда возможно)

Прототипное наследование в Яваскрипте


В первую очередь, важно иметь четкое представление о прототипном наследовании в Яваскрипте, особенно если пришли из серверного программирования и больше знакомы с классическим наследованием.

Предположим parentScope имеет свойства aString, aNumber, anArray, anObject, и aFunction. Если childScope прототипически наследуется от parentScope, имеем:

image

(Обратите внимание, что для экономии места, объект anArray показан как одиночный объект с тремя значениями, а не объект с тремя строками.)

Если попытаться получить доступ к свойствам по умолчанию, определенным в parentScope, из дочерней области видимости, Яваскрипт сначала ищет в дочерней области и, не найдя там, затем смотрит в родительской области, где уже находит свойство. (Если бы не нашел в parentScope, то продолжил бы поиск по цепочке прототипов... вплоть до корневой области видимости). Итак, все утверждения ниже верны:

  childScope.aString === 'parent string'
  childScope.anArray[1] === 20
  childScope.anObject.property1 === 'parent prop1'
  childScope.aFunction() === 'parent output'

Предположим, что после этого сделали так:

  childScope.aString = 'child string'

Цепочка прототипов не расматривается, а новое свойство aString добавляется в childScope. Новое свойство перекрывает свойство из parentScope с таким же названием. Это будет очень важным, при обсуждении ng-repeat и ng-include ниже.

image

Предположим, что после этого сделали так:

  childScope.anArray[1] = '22'
  childScope.anObject.property1 = 'child prop1'

Цепочка прототипов рассматривается, так как объекты (anArray и anObject) не найдены в childScope. Объекты находятся в parentScope, а значения свойств обновляются в исходных объектах. Новые свойства не добавляются в childScope; новые объекты не создаются. (Отметим, что в Яваскрипте массивы и функции также являются объектами.)

image

Предположим, что после этого сделали так:

  childScope.anArray = [100, 555]
  childScope.anObject = { name: 'Mark', country: 'USA' }

Цепочка прототипов опять не рассматривается, и дочерняя область видимости получает два новых свойства объекта, которые перекрывают свойства объекта из parentScope с теми же именами.

image

Подытожим:

  • Если читаем childScope.propertyX и childScope содержит propertyX, то цепочка прототипов не рассматривается.
  • Если устанавливаем childScope.propertyX, цепочка прототипов рассматривается.

И последний сценарий:

  delete childScope.anArray
  childScope.anArray[1] === 22  // true

Сперва удалим свойство из childScope, а затем, когда попытаемся получить доступ к свойству снова, состоится переход по цепочке прототипов.

image

Наследование областей видимости в Ангуляре


Соучастники:

  • Создают новые области видимости, которые прототипически наследуются: ng-repeat, ng-include, ng-switch, ng-view, ng-controller, директивы с scope: true, директивы с transclude: true.
  • Создает новую область, которая не наследуется прототипически: директивы с scope: { ... }. Вместо этого они создают «изолированную» область видимости.

Обратите внимание, что по умолчанию, директивы не создают новые области, т.е. по умолчанию scope: false.

ng-include

Предположим, что имеем в контроллере:

  $scope.myPrimitive = 50;
  $scope.myObject    = {aNumber: 11};

И в HTML:

  <script type="text/ng-template" id="/tpl1.html">
    <input ng-model="myPrimitive">
  </script>
  <div ng-include src="'/tpl1.html'"></div>

  <script type="text/ng-template" id="/tpl2.html">
    <input ng-model="myObject.aNumber">
  </script>
  <div ng-include src="'/tpl2.html'"></div>

Каждая ng-include создает новую дочернюю область видимости, прототипически унаследованную от родительской области.

image

Ввод (скажем, «77») в первом текстовом поле приведет к тому, что дочерняя область видимости получит новое свойство myPrimitive из области видимости, которое перекроет свойство родительской области с одноименным названием.

Вероятно, это не то, чего вы хотите или ожидаете.

image

Ввод (скажем, «99») во втором текстовом поле не приводит к созданию новой дочерней области видимости. Потому что tpl2.html связывает модель со свойством объекта, прототипное наследование заканчивается, когда ngModel ищет объект MyObject — находит его в родительской области.

image

Примечание: на изображении выше ошибка, число «99» должно заменить 11, а не 50.

Можно переписать первый шаблон с использованием $parent, если не хотим менять модель из примитива на объект:

  <input ng-model="$parent.myPrimitive">

Ввод (скажем, «22») в этом текстовом поле не имеет последствий для нового дочернего свойства. Модель теперь связана со свойством родительской области (потому что $parent является свойством дочерней области видимости, ссылающимся
на родительскую область).

image

Для всех областей видимости (прототипных или нет), Angular всегда отслеживает отношения родитель-ребенок (т. е. иерархию), через свойства $parent, $$childHead и $$childTail в области видимости. Эти свойства обычно не показываются на диаграммах.

В случаях, когда элементы формы не задействуются, другим решением будет определение функции в родительской области видимости для изменения примитива. Только убедитесь, что потомок всегда вызывает эту функцию, которая будет доступна в дочерней области видимости в соответствии с прототипными наследованием. Например,

  // в родительской области видимости
  $scope.setMyPrimitive = function(value) {
    $scope.myPrimitive = value;
  }

Здесь простой пример, который использует подход с «родительской функцией». (Являлся частью поста на Stack Overflow.)

См. так же stackoverflow.com/a/13782671/215945 и github.com/angular/angular.js/issues/1267.

ng-switch

Наследование область видимости в ng-switch работает так жде как в ng-include. Так что если нуждаетесь в двухсторонней привязке данных к примитиву в родительской области видимости, используйте $parent или измените модель на объект, а затем привяжите к свойству этого объекта. Это позволит избежать перекрытия свойст родительской области свойствами дочерней.

См. так же AngularJS, bind scope of a switch-case?

ng-repeat

ng-repeat работает немного по-другому. Предположим, что в контроллере содержится следующее:

  $scope.myArrayOfPrimitives = [ 11, 22 ];
  $scope.myArrayOfObjects    = [{num: 101}, {num: 202}]

И в HTML:

  <ul>
    <li ng-repeat="num in myArrayOfPrimitives">
      <input ng-model="num">
    </li>
  <ul>
  <ul>
    <li ng-repeat="obj in myArrayOfObjects">
      <input ng-model="obj.num">
    </li>
  <ul>

Для каждого элемента/итерации, ng-repeat создает новую область видимости, которая прототипически наследуется от родительской, но он также присваивает значение элемента новым свойствам в новой дочерней области. (Названием нового свойства является имя переменной из цикла.) Ниже показан фактический исходный код для ng-repeat:

  childScope = scope.$new(); // дочерняя область прототипически наследуется от родительской ...     
  childScope[valueIdent] = value; // создает новое свойство в childScope

Если элемент является примитивом (как в myArrayOfPrimitives), по сути, копии значения присваивается новое свойство дочерней области видимости. Изменение значения свойства в дочерней области (т. е., свойства num, унаследованного из ng-model) не изменяет массив ссылок в родительской области. Таким образом, в первом ng-repeat выше, каждая дочерняя область видимости получает свойство num, которое не зависит от массива myArrayOfPrimitives:

image

ng-repeat не будет работать (как хотите или ожидаем). В Angular 1.0.2 или более раннем, ввод в поля ввода, изменяет значения в серых блоках, которые видны только в дочерних областях. В Angular 1.0.3 +, ввод в текстовые поля никак на них не влияет. (См. пояснения Артема, почему это так.) Мы же хотим, чтобы ввод влияюл на массив myArrayOfPrimitives, а не на свойство дочерней области видимости. Чтобы достичь этого, необходимо заменить модель на массив объектов.

Если элемент является объектом, то по ссылке на исходный объект (не копии) присваивается новое свойство дочерней области видимости. Изменение значения свойства из дочерней области (т. е., свойства num, унаследованного из ng-model) изменяет объект по ссылке из родительской области. Так во втором ng-repeat выше, мы имеем:

image

(серая линия показывает куда он переходит.)

Это работает, как ожидалось. Ввод в поля ввода, изменяет значения в серой области, которые видны и в дочерней и в родительской областях видимости.

См. так же Сложности с ng-model, ng-repeat, и inputs и ng-repeat and databinding

ng-view

Уточняется, но думаю, что действует так же, как ng-include.

ng-controller

Вложенные контроллеры (с использованием ng-controller) подчиняются нормальному прототипному наследованию, как ng-include и ng-switch, так что к ним применимы те же методы. Тем не менее «считается дурным тоном обмениваться информацией между двумя контроллерами через наследование $scope» — onehungrymind.com/angularjs-sticky-notes-pt-1-architecture. Вместо этого необходимо использовать сервис.

(Если действительно необходимо поделиться данными через наследование области видимости контроллеров, то делать ничего не надо. Дочерняя область видимости будет иметь доступ ко всем свойствам в родительской области. См. также Controller load order differs when loading or navigating)

Диретивы

  1. по умолчанию (scope: false) — директива не создать новую область видимости, так что никакого наследования не происходит. Это легко, но опасно, потому что, например, директива может подумать, что создает новое свойство в области видимости, когда фактически она затрет существующее свойство. Не лучший выбор при написании директив, предназначенных для компонентов многоразового использования.

  2. scope: true — директива создает новую дочернюю область видимости, прототипически унаследованную от родительской области. Если более чем одна директива (на одном и том же DOM элементе) запрашивают новую область, все равно будет создана только одна дочерняя область. Т. к. у нас «нормальное» прототипное наследование, как в ng-include и ng-switch, то будьте осторожны с двухсторонней привязкой данных к примитивам родительской области видимости и перекрытием свойств родительской области свойствами дочерней.

  3. scope: { ... } — директива создает новые изолированные области видимости. Они не наследуются прототипически. Как правило, это лучший вариант для создания компонентов многоразового использования, т. к. директива не сможет случайно прочитать или изменить данные из родительской области. тем не менее, таким директивам часто необходим доступ к некоторым свойствам из родительской области.

    Хэш-объект используется для создания двухсторонней привязки (с помощью «=») или односторонней (с помощью «@») между родительской областью и изолированной. Есть так же «&» для привязки к выражениям в родительской области. Таким образом, все они создают локальные свойства области видимости, получаемые из родительской области. Обратите внимание, что атрибуты используются чтобы помочь установить
    привязки — невозможно сослаться на имена свойств из родительской области в хэш-объекте, без использования этих атрибутов. Например, привязка к родительскому свойству parentProp в изолированной области: и {scope: localProp: '@parentProp' } не будет работать. Атрибут должен использоваться для указания на каждое родительское свойство к которому нужно привязаться в директиве: и scope: { localProp: '@theParentProp' }.

    __proto__ изолированной области ссылается на объект Scope (рисунок ниже необходимо обновить, чтобы показать оранжевый объект «Scope» вместо «Object»). $parent изолированной области ссылается на родительскую область видимости, так что, хотя он изолированный и не наследуется прототипически от родительской области, он всё же является потомком.

    Для рисунка ниже имеем

    <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2"> и

    scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }

    Предположим так же, что директива делает это в связующей функции: scope.someIsolateProp = "I'm isolated"

    image

    Последнее замечание: используйте attrs.$observe('attr_name', function(value) { ... } в связующей функции для получения интерполированного значения свойства из изолированной области видимости, записанного с помощью «@». Например, если такое содержится в связующей функции — attrs.$observe('interpolated', function(value) { ... }value будет установлено в 11. (scope.interpolatedProp не определено в связующей функции. В противоположность этому, scope.twowayBindingProp определено в связующей функции, т. к. задано с помощью «=».)

    Подробнее об изолированных областях видимости читайте в onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope


    transclude: true — директива создает новую «включенную» дочернюю область видимости, которая прототипически наследуется от родительской области. Поэтому, если включенный контент (т. е., материал, который будет заменен ng-transclude) требует двухстороннюю привязку данных к примитиву из родительской области видимости, используйте $parent или превратите модель в объект и затем привяжитесь к его свойству, что позволит избежать перекрытия свойства родительской области свойством дочерней

    Включенная и изолированная области видимости (если таковые имеются) являются потомками одно уровня — свойство $parent каждой из этих областей ссылается на одного и того же родителя. Если существуют и включенная и изолированная области, свойство $$nextSibling изолированной области будет ссылаться на включенную область.

    По включенным областям видимости см. так же AngularJS two way binding not working in directive with transcluded scope

    Для рисунка ниже имеем, такую же директиву как указано выше с этим дополнением: transclude: true

    image


    Этот пример содержит функцию showScope() которую можно использовать для изучения изолированной области видимости и ассоциированной с ней включенной области. Смотрите инструкции в комментариях к примеру.

    Заключение

    Существует четыре типа областей видимости:

    1. прототипически наследуемая область — ng-include, ng-switch, ng-controller, директивы с scope: true
    2. прототипически наследуемая область с несколькими копиями — ng-repeat. Каждая итерация ng-repeat создает новую дочернюю область видимости, и эта новая облать всегда получает новые свойства.
    3. изолированная область — директивы с scope: {...}. Не наследуется прототипически, но «=», «@», и «&» обеспечивают механизм для доступа к свойствам родительской области через атрибуты.
    4. включаемая область — директивы с transclude: true. Так же прототипически наследуется, но так же соседствует с какой-то изолированной областью.

    Для всех областей (наследуемых или нет), Ангуляр всегда отслеживает отношения родитель-ребенок (т. е. иерархию), через свойства $parent, $$childHead и $$childTail.
Tags:
Hubs:
+30
Comments 6
Comments Comments 6

Articles