您好,欢迎来到保捱科技网。
搜索
您的当前位置:首页AngularJS内幕详解之 Scope

AngularJS内幕详解之 Scope

来源:保捱科技网

  • 原文作者: 
  • 原文:

在AngularJS的代码库中呈现出了大量有趣的设计,最有趣的两个例子是scope的工作方式和directives(指令)的表现。

有的人第一次接触AngularJS时就被告知directives是和DOM交互,或供你随意操作DOM,就像jQuery. 这立马变得非常复杂,试想,scopes, directives 和controllers相互作用.

复杂的设置之后,你开始学习它先进的思想:the digest cycle,的scope、内嵌以及指令中不同的链接函数。这些已经非常负责,这篇文章不会涉及指令,但会在后续的文章中写到。

这篇文字将带你跳过AngularJS 的scopes和AngularJS应用生命周期的各种坑,同时提供有趣信息的深度阅读。

(门槛是高的,但scope是很难解释的。如果在此惨遭失败,至少我会抛出几个重要的点。)

如果这个图表看起来非常的费解,那么这篇文章很适合你。

() ()

(免责声明:这篇文字是基于 )

AngularJS 用scopes分离指令和DOM的通信。scopes也存在于controller层。scopes 是普通的JavaScript对象,AngularJS没有过多的操作。只是添加了“一串”带有一个或两个$符号前缀的内部属性。其中以$$开头的常常不是必须的,经常用它们作代码气味,可以避免更深入的理解digest循环。

哪种scope是我们要讨论的?

在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请参考“。

AngularJS 中scope 的继承 

在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。

拨开 AngularJS scope 内部属性 

在介绍digest如何工作以及内部如何表现前,我会剖析scope的一些属性来介绍某些概念。我也会让你知道我如何获取这些属性。首先,打开 Chrome 并导航到我正在使用的一个 angular应用程序。然后,我将审查一个元素并打开开发者工具。

(你知道吗?$1让你能访问前一个被选中的元素等。我想你会经常用到$0,尤其是使用 AngularJS工作时。)

对于每一个DOM元素, 用 angular.element 包装跟使用 或 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)

这就足够了,我将遍历所有的属性,

深入 AngularJS Scope 的内部学习 

下面,我列出了由该命令产生的属性,按功能分组。让我们从基本的开始,它仅仅提供scope导航。

  •  scope 的唯一标识

  •  根scope

  •  父级scope, 如果 scope == scope.$root 则为 null

  •  第一个子 scope, 如果没有则为 null

  •  最后一个子scope, 如果没有则为 null

  •  前一个相邻节点 scope, 如果没有则为 null

  •  下一个相邻节点 scope, 如果没有则为 null

这没什么惊喜, 浏览 scope 没什么意思。有时候访问 $parent 看起来是得当的, 但是有更好的、低耦合的方式来处理父级通信而不是紧紧的与人为的scope绑定在一起。 比如说 使用事件,下面我们将讲到。

AngularJS Scope 的事件模型 

下面介绍的属性允许我们发布事件和订阅事件。这个模式叫。

  •  在scope上注册事件。

  •  注册一个名为evt,为fn的事件。

  •  发送事件 evt, 在scope 链上冒泡,在当前scope 以及所有的 $parents 上触发,包括 $rootScope。

  •  发送事件 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 的元素点击,可以用 。这确实很有意义,并且没有理由污染你的指令或者控制器。你可以用一个指令来处理 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 来维护所有的实物类型。指令很好的帮助 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'];
});

完整的项目例子

表面上看是一个很好的例子,但你应该思考是否需要一个事件给其它人订阅。也许一个服务就行。这种情况。你可以用另一种方式。 你会问自己你需要事件因为你不知道谁会订阅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
   });
});

这个例子也

渐渐的,你将对事件和服务越来越熟悉。我想告诉你,当你期望视图模块改变来响应事件,你应该使用 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
   };
});

事件跟服务已经差不多了,让我们移步到一些其它的属性。

Digest 变化 

了解这个恐怖的过程是认识 AngularJS的关键。

AngularJS 基于它的数据绑定的特性,通过循环脏检测来追踪变化并且在变化时触发事件。这比听起来简单。事实上, 它就是这样简单。让我们快速的浏览一下 $digest 循环的核心组件。 首先,有一个 scope.$digest 方法,通过递归检测scope 和它的后代们的变化。

你需要小心触发 digest,因为当你已经在一个 digest 阶段而尝试这么做, 会因为一些无法解释的现象导致 AngularJS 出错。

让我们看看 里关于 $digest怎么说


它处理当前scope 及其 后代们的所有的 。因为 watcher 的可以改变 model, 持续调用 watcher 直到没有被触发。这意味着可能进入死循环。如果迭代超过10次,这个函数会抛出异常 'Maximum iteration limit exceeded'。

通常,我们在控制器或指令中不直接调用 $digest。相反,你应该调用 (通常在一个指令里) 用来强制执行一个 $digest()。

所以,一个 $digest 处理所有的 watcher,处理这些 watcher时,这些watcher触发,直到没有别的触发 watcher。为了我们理解这个循环,仍然有两个问题需要解答。

  • "watcher" 是什么鬼?!
  • 什么触发 $digest?!

这两个问题的答案因复杂度差异很大,但是我会尽量为你解释清楚。我将介绍 watcher,让你有个自己的看法。

如果你读过这一步,你或许已经知道什么事 watcher了。你或许使用过 ,甚至用过。$$watchers 属性有用 scope 上所有的 watcher。

  •  为scope添加一个 watch

  •  watch 数组元素或对象属性

  •  保持所有的 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 结束。这种情况下,每个 ,当 prop、 dependency 变化时它会更新 HTML <li>。

这样,你现在可以把你的代码想象成3个 watcher,每个 ng-bind 指令的watcher, 还有一个在控制器中的。AngularJS如何知道 timeout 执行后的属性变化?你应该想起 AngularJS 更新属性时,可以在 timeout 回调时添加一个手动的 digest。

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

我放置了一个在 CodePen,以及 timeout的。你可以用 替换 setTimeout,它提供了一些错误处理,并且会执行 $apply()。

$timeout(function () {
   $scope.prop = 'something else';
}, 1000);
  •  解析和计算一个表达式,然后在 $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 表达式。

  •  在稍后的时间里解析和计算一个表达式。

  •  同步处理队列,会消耗每个 digest

  •  在下一个 digest 周期后执行 fn

  •  用 $$postDigest(fn) 注册方法

scope已死!scope万岁!(The Scope Is Dead! Long Live the Scope!) 

在最后的部分,来聊聊 scope 的性能。尽管有时候你可能要用 $new 声明自己的scope, 但它们使用内部手段处理 scope 的生命周期。

  •   scope 绑定(例如:{ options: '@megaOptions' })

  •  创建一个子 scope 或者一个的 scope, 它不继承自它们的父级。

  •  从 scope 链里移除该 scope; scope 和后代们不会收到事件, watcher 也不再被触发。

  •  scope 是否被销毁。

的scope?这是什么鬼?这个系列的第二部分将讨论指令,它包括的scope,嵌入,link 函数,编译器,指令控制器等。Wait for it!

扩展阅读

Here are some additional resources you can read to extend your comprehension of AngularJS.

  • “,” Nicolas Bevacqua
  • “,” AngularJS, GitHub
  • “,” AngularJS, GitHub
  • , John Lindquist
  • “,” StackOverflow

转载于:https://my.oschina.net/lenolong/blog/688875

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- baoaiwan.cn 版权所有 赣ICP备2024042794号-3

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务