Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

angular.js 1.x $scope #11

Open
breakinferno opened this issue Oct 10, 2018 · 1 comment
Open

angular.js 1.x $scope #11

breakinferno opened this issue Oct 10, 2018 · 1 comment

Comments

@breakinferno
Copy link
Owner

breakinferno commented Oct 10, 2018

$scope

$scope,作用域,即控制范围,表示某个范围数据和视图都由它管。比如

<div id="ctrl" ng-controller="exampleCtrl">
    这个div下所有范围都是exampleCtrl所有的$scope控制,这个地盘已经被这个$scope占了。
    <h1>{{title}}</h1>
</div>

exampleCtrl = ($scope) => {
    $scope.title = 'mdzz'
}

表示id为ctrl的div被exampleCtrl控制,这个范围下的数据都在其scope控制下。�

作用

作用域基本功能

  • 提供观察者以监视数据模型的变化
  • 可以将数据模型的变化通知给整个应用,甚至是系统外的组件
  • 可以进行嵌套,隔离业务功能和数据
  • 给表达式提供运算时所需的执行环境

作用域包含了渲染视图时所需的功能和数据,它是所有视图的唯一源头。可以将作用域理解成视图模型(view model)。

生命周期

**创建:**在创建控制器或指令时,AngularJS会用$injector创建一个新的作用域,并在这个新建的控制器或指令运行时将作用域传递进去。

**链接:**当Angular开始运行时,所有的$scope对象都会附加或者链接到视图中。所有创建$scope对象的函数也会将自身附加到视图中。这些作用域将会注册当Angular应用上下文中发生变化时需要运行的函数。

这些函数被称为$watch函数,Angular通过这些函数获知何时启动事件循环。

**更新:**当事件循环运行时,它通常执行在顶层$scope对象上(被称作$rootScope),每个子作用域都执行自己的脏值检测。每个监控函数都会检查变化。如果检测到任意变化,$scope对象就会触发指定的回调函数。

**销毁:**当一个$scope在视图中不再需要时,这个作用域将会清理和销毁自己。

尽管永远不会需要清理作用域(因为Angular会为你处理),但是知道是谁创建了这个作用域还是有用的,因为你可以使用这个$scope上叫做$destory()的方法来清理这个作用域。

scope的继承类似于js的原型继承.即先在自己的scope中查找属性,如果没找到则到父级scope中查找,直到rootScope。没有就报错

下面几种情况会产生scope:

  • ng-repeat

  • ng-switch

  • ng-view

  • ng-controller

  • 带有 scope: true 的指令

  • 带有 transclude: true 的指令
    以下指令创建新的scope, 且在原型上 不继承 父scope:

  • 带有 scope: { ... } 的指令, 这会创建一个 独立的scope (isolate scope)
    注意: 默认指令并不会创建 scope, 默认是 scope: false, 通常称之为 共享scope.

ng-repeat

源码在这里

JS:

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

HTML:

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

这个指令有点特殊,每次repeat都会新建一个�新的scope,每个scope都会在原型上继承父级的scope.所以如果迭代对象是一个primitive,则每个值会复制到每个scope属性上。如果�迭代的对象为object,则其引用会被赋值给scope属性,具体如图:

primitive:

primitive

object:

object

ng-include、ng-switch、ng-view 、ng-controller类似

指令

  • scope: false(默认的), 指令不会创建新的 scope, 没有继承关系. 与 $parent 共享 $scope.
  • scope: true, 指令会创建一个 子scope, 并在原型上继承 $parent. 如果在一个 DOM 上有多个指令想要创建新的 - scope, 会报错.
  • scope: { ... }, 指令会创建一个 孤立的scope. 这在创建可重用的组件时是最好的选择. 但是, 即使如此, 指令还是希望读取 $parent 的数据. 这个时候可以使用如下符号获得:
    • scope: { **: "="} 与 $parent 建立双向绑定.
    • scope: { **: "@"} 与 $parent 建立单向绑定.
    • scope: { **: "&"} 绑定 $parent 的表达式.
      想要获得相应的属性, 必须通过指令上的属性获得
    • HTML:
    • JS: scope: { localProp: '@theParentProp' }
  • HTML:
  • JS: scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
  • 指令在 link 期间: scope.someIsolateProp = "I'm isolated"

其中的关系如图:

directive

  • transclude: true, 指令创建了一个 “transcluded” 的子scope, 在原型上继承其 父scope. 如果上述例子同时具有transclude: true. 那么这个 “transcluded” scope, 和 “islolated” scope 是姊妹关系. 他们的 $parent 指向同一个 scope. 且 isolate scope 的 $$nextSibling 就是这个 “transcluded scope”. 下图反应了他们之间的关系:

directive2

参考

源码看scope
作用域与事件

@breakinferno
Copy link
Owner Author

ngRepeat解读

整体

var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
  var NG_REMOVED = '$$NG_REMOVED';
  var ngRepeatMinErr = minErr('ngRepeat');

  return {
    restrict: 'A',
    multiElement: true,
    transclude: 'element',
    priority: 1000,
    terminal: true,
    $$tlb: true,
    compile: function ngRepeatCompile($element, $attr) {
    ......
    return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) {
    ......
    }
    }
  };

  var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) {
    scope[valueIdentifier] = value;
    if (keyIdentifier) scope[keyIdentifier] = key;
    scope.$index = index;
    scope.$first = (index === 0);
    scope.$last = (index === (arrayLength - 1));
    scope.$middle = !(scope.$first || scope.$last);
    scope.$odd = !(scope.$even = (index & 1) === 0);
  };

  var getBlockStart = function(block) {
    return block.clone[0];
  };

  var getBlockEnd = function(block) {
    return block.clone[block.clone.length - 1];
  };];

主要来看compile和link中的内容:

compile中在postLink之前:

      // 取ngRepeat表达式
      var expression = $attr.ngRepeat;
      var ngRepeatEndComment = $compile.$$createComment('end ngRepeat', expression);
      // 匹配该表达式  注意 in as track by
      var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);

      if (!match) {
        throw ngRepeatMinErr('iexp', 'Expected expression in form of \'_item_ in _collection_[ track by _id_]\' but got \'{0}\'.',
            expression);
      }

      var lhs = match[1]; // 存储临时变量即in前面的括号部分,例如`item` in items 或者`(item, index)` in items
      var rhs = match[2]; // 存储被循环的collection名,即上面的items
      var aliasAs = match[3];// 存储别名
      var trackByExp = match[4]; // 存储可选的track by字符串
      // 具体的临时变量部分解析
      match = lhs.match(/^(?:(\s*[$\w]+)|\(\s*([$\w]+)\s*,\s*([$\w]+)\s*\))$/);

      if (!match) {
        throw ngRepeatMinErr('iidexp', '\'_item_\' in \'_item_ in _collection_\' should be an identifier or \'(_key_, _value_)\' expression, but got \'{0}\'.',
            lhs);
      }
      var valueIdentifier = match[3] || match[1];  // value
      var keyIdentifier = match[2];  // key

      if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) ||
          /^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(aliasAs))) {
        throw ngRepeatMinErr('badident', 'alias \'{0}\' is invalid --- must be a valid JS identifier which is not a reserved name.',
          aliasAs);
      }

      var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn;
      var hashFnLocals = {$id: hashKey};
      // 有track by语句则处理,否则如果colletion为对象则使用key作为id,如果数组使用hashKey
      if (trackByExp) {
        trackByExpGetter = $parse(trackByExp);
      } else {
        trackByIdArrayFn = function(key, value) {
          return hashKey(value);
        };
        trackByIdObjFn = function(key) {
          return key;
        };
      }

match = lhs.match(/^(?:(\s*[$\w]+)|\(\s*([$\w]+)\s*,\s*([$\w]+)\s*\))$/); 中正则这样划分(\s*[$\w]+)\(\s*([$\w]+)\s*,\s*([$\w]+)\s*\)两部分,分别对应item in items(key, value) in items的情形。其他见中文注释

下面是link函数的内容

   // 如果有track by语句则使用track by语句
   if (trackByExpGetter) {
          trackByIdExpFn = function(key, value, index) {
            // assign key, value, and $index to the locals so that they can be used in hash functions
            if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
            hashFnLocals[valueIdentifier] = value;
            hashFnLocals.$index = index;
            return trackByExpGetter($scope, hashFnLocals);
          };
        }

        //lastBlockMap是一个用来保存最近一次view更新完后各个循环item状态的hash。该hash的键是各个被循环的item,值则是一个保存了该item相应属性的对象:
        //scope 与该item绑定的scope
        //element view中在该元素之前的元素
        //index item对应的元素在页面中的出现顺序,也即最后一次循环时,该item被处理的先后顺序。
        var lastBlockMap = createMap();

        //核心部分,监听collection
        $scope.$watchCollection(rhs, function ngRepeatAction(collection) {
          var index, length,
              previousNode = $element[0],     // node that cloned nodes should be inserted after
                                              // initialized to the comment node anchor
              nextNode,
              // Same as lastBlockMap but it has the current state. It will become the
              // lastBlockMap on the next iteration.
              nextBlockMap = createMap(),
              collectionLength,
              key, value, // key/value of iteration
              trackById,
              trackByIdFn,
              collectionKeys,
              block,       // last object information {scope, element, id}
              nextBlockOrder,
              elementsToRemove;

          if (aliasAs) {
            $scope[aliasAs] = collection;
          }
          // 类数组collection直接赋值,对象则按枚举顺序提取key加入collectionKeys
          // 结果是如果collection为[1,2,3],collectionKeys为[1,2,3]
          // 如果collection为{a: 1, b: 2, c: 3} ,collectionKeys为['a', 'b', 'c']
          if (isArrayLike(collection)) {
            collectionKeys = collection;
            trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
          } else {
            trackByIdFn = trackByIdExpFn || trackByIdObjFn;
            // if object, extract keys, in enumeration order, unsorted
            collectionKeys = [];
            for (var itemKey in collection) {
              if (hasOwnProperty.call(collection, itemKey) && itemKey.charAt(0) !== '$') {
                collectionKeys.push(itemKey);
              }
            }
          }

          collectionLength = collectionKeys.length;
          nextBlockOrder = new Array(collectionLength);

          // locate existing items
          for (index = 0; index < collectionLength; index++) {
            // 获取key ,value
            key = (collection === collectionKeys) ? index : collectionKeys[index];
            value = collection[key];
            //按照collectionKeys中保存的key依次取出要被循环处理的value。trackById是使用trackByIdFn计算出来的每个item唯一的标识,用来建立item与页面中元素间的关联。
           //如果lastBlockMap中有trackById这个属性,则说明该item在上次循环中已经存在,则将相应的属性/值设置到nextBlockMap对象中,同时在nextBlockOrder数组中保存顺序。
           //如果在lastBlockMap中找不到trackById但在nextBlockMap中找到了,则说明在collection中有两个item的trackById是相同的,这时会抛出异常,因为不可能两个item对应页面中的同一个element。
           //如果在两个map对象中都没有找到,则说明这个item是首次出现,那么则在nextBlockMap中将对应的值设置为false,表明没有scope与之对应。
            trackById = trackByIdFn(key, value, index);
            if (lastBlockMap[trackById]) {
              // found previously seen block
              block = lastBlockMap[trackById];
              delete lastBlockMap[trackById];
              nextBlockMap[trackById] = block;
              nextBlockOrder[index] = block;
            } else if (nextBlockMap[trackById]) {
              // if collision detected. restore lastBlockMap and throw an error
              forEach(nextBlockOrder, function(block) {
                if (block && block.scope) lastBlockMap[block.id] = block;
              });
              throw ngRepeatMinErr('dupes',
                  'Duplicates in a repeater are not allowed. Use \'track by\' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}',
                  expression, trackById, value);
            } else {
              // new never before seen block
              nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined};
              nextBlockMap[trackById] = true;
            }
          }

          // 在经历前面一次检查后,现在还留在lastBlockMap中的item就是被从collection中移除的item。要做的就是将对应的element从DOM中移除并销毁item对应的scope。
          for (var blockKey in lastBlockMap) {
            block = lastBlockMap[blockKey];
            elementsToRemove = getBlockNodes(block.clone);
            $animate.leave(elementsToRemove);
            if (elementsToRemove[0].parentNode) {
              // if the element was not removed yet because of pending animation, mark it as deleted
              // so that we can ignore it later
              for (index = 0, length = elementsToRemove.length; index < length; index++) {
                elementsToRemove[index][NG_REMOVED] = true;
              }
            }
            block.scope.$destroy();
          }

          // 这个循环用来处理已有item的DOM移动以及新item对应的DOM插入。在这个循环中previousNode代表了上一次循环item元素在DOM中的位置,angular会顺次将各个block插入到前一个block的后面(对于已经存在的元素则是移动)。
          for (index = 0; index < collectionLength; index++) {
            key = (collection === collectionKeys) ? index : collectionKeys[index];
            value = collection[key];
            block = nextBlockOrder[index];
            // 已有的block移动位置
            if (block.scope) {
              // if we have already seen this object, then we need to reuse the
              // associated scope/element

              nextNode = previousNode;

              // skip nodes that are already pending removal via leave animation
              do {
                nextNode = nextNode.nextSibling;
              } while (nextNode && nextNode[NG_REMOVED]);

              if (getBlockStart(block) !== nextNode) {
                // existing item which got moved
                $animate.move(getBlockNodes(block.clone), null, previousNode);
              }
              previousNode = getBlockEnd(block);
              // 跟新scope
              updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
            } else {
              // 新的block则有新的scope,插入dom
              $transclude(function ngRepeatTransclude(clone, scope) {
                block.scope = scope;
                var endNode = ngRepeatEndComment.cloneNode(false);
                clone[clone.length++] = endNode;

                $animate.enter(clone, null, previousNode);
                previousNode = endNode;
                block.clone = clone;
                nextBlockMap[block.id] = block;
                updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
              });
            }
          }
          // 为下一次做准备
          lastBlockMap = nextBlockMap;
        });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant