breakinferno / breakinferno.github.io

0 stars 0 forks source link

angular.js 1.x $scope #11

Open breakinferno opened 6 years ago

breakinferno commented 6 years ago

$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

源码在这里

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类似

指令

其中的关系如图:

directive

directive2

参考

源码看scope 作用域与事件

breakinferno commented 6 years ago

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;
        });