为什么不建议在函数中使用$rootScope?

13

我在查看Angularjs的常见问题时看到以下文章:

$rootScope存在,但可能会被滥用

在Angular中,作用域形成层次结构,从树顶的根作用域原型继承。通常可以忽略这一点,因为大多数视图都有自己的控制器,因此也有自己的作用域。

偶尔会有一些数据需要对整个应用程序进行全局共享。对于这些数据,可以注入$rootScope并像任何其他作用域一样设置值。由于作用域是从根作用域继承的,因此这些值将像本地$scope上的值一样可用于与指令(如ng-show)相关联的表达式中。

当然,全局状态很糟糕,你应该像在任何语言中使用全局变量一样谨慎地使用$rootScope。特别是不要将其用于代码,只用于数据。如果你想在$rootScope上放置一个函数,几乎总是更好将其放在一个服务中,在需要时可以注入它,并更容易进行测试。

相反,不要创建一个唯一目的是存储和返回数据片段的服务。

— AngularJS常见问题解答 - $rootScope存在,但可能会被滥用

那么,为什么不建议将$rootScope用作全局函数?是否存在性能问题?


1
只是污染一个对象,以便复制该对象以创建新的作用域,这些作用域又被复制到可以嵌套多个级别的子作用域中。 - charlietfl
2
这是不正确的。由于Angular中的作用域继承是实现为普通的JavaScript原型继承,因此永远不会复制任何内容。如果在当前作用域中找不到成员$scope.foo,解释器将继续在$scope._proto_指向的对象中搜索该成员(它恰好是父作用域,或者如果没有父作用域,则是rootScope),然后是该对象的_proto_,等等。 由于这在JavaScript中非常常见,所有引擎都会对其进行大量优化,即使是长的原型链也不会显着减慢您的代码。 - Kevin Dreßler
3个回答

11

我之前回答过这个问题,但你问这些问题很好。

$rootScope 存在,但它可能会被用于恶意目的。在 Angular 中,作用域形成一个层次结构,从树顶的根作用域原型继承。通常情况下,可以忽略这一点,因为大多数视图都有控制器和自己的作用域。

非隔离作用域是分层的,但大多数开发人员应该使用具有隔离作用域的指令。AngularJS 的作用域非常分层,这是许多 Angular 应用程序中导致错误的源头。这是一个我称之为“作用域泄漏”的问题,其中一个作用域属性在 DOM 树的某个地方神奇地修改了,而你却不知道为什么。

Angular 的默认行为是继承作用域,这使得一个控制器更新另一个控制器管理的内容变得诱人,以此类推。这就是创建源代码之间“意大利面条”连接的方式。这样非常难以维护那段代码。

偶尔会有一些数据片段,您希望将其全局适用于整个应用程序。对于这些数据,可以注入 $rootScope 并像其他作用域一样设置值。

不正确。AngularJS 允许您定义诸如常量、值和服务之类的东西。这些是可以注入到路由、控制器和指令中的东西。这就是您使应用程序中的内容全局可访问的方式,如果您想使控制器或指令可测试,那么您应该这样做。单元测试编写人员不知道指令或控制器依赖的 $rootScope 中应该有哪些属性。他们必须假定 $rootScope 没有变异来提供服务或数据。

当然,全局状态很糟糕,你应该像在任何语言中使用全局变量一样谨慎地使用 $rootScope。

问题不在于$rootScope本身,而是人们对它的使用。许多应用程序将当前用户、身份验证令牌和会话数据添加到rootScope中。这在模板中得到了广泛使用(如果用户已登录,则显示X,否则显示Y)。问题在于HTML无法传达作用域层次结构。因此,当您看到{{user.firstname + ' '+ user.lastname}}时,您不知道变量user来自哪里。第二个问题是子作用域可以遮盖根作用域属性。就像前面的例子一样,如果一个指令执行scope.user = 'bla bla bla'。它没有替换rootScope上的值。它隐藏了它。现在你会在模板中得到一些奇怪的意外结果,你不知道为什么变量user会改变。

相反,不要创建一个其唯一目的是存储和返回数据位的服务。

Angular的$cacheFactory$templateCache是存储数据的服务的示例。我认为作者是试图鼓励在Angular模块中使用常量和值,但这并不是一个好的描述。

所以我想知道为什么不建议将函数作为全局函数放在$rootScope中?有性能问题吗?

$rootScope是唯一可用的作用域,可在angular.config(..)期间进行修改。如果这是您唯一可以执行此操作的时间,请在此期间进行操作。例如,您可能需要在应用程序启动之前注入API密钥或Google分析变量。

一般来说,任何作用域上的函数都是不好的想法。主要是因为模板表达式中的所有内容都要在作用域内消化。函数倾向于隐藏复杂操作。当模板调用函数时,通过阅读 HTML 无法确定模板的复杂程度。我曾经见过像 getHeight() 这样的作用域函数,其中函数本身执行了三层嵌套循环。每次 Angular 执行观察程序并检查它是否更改时,必须调用该函数。您应该尽可能使您的模板保持干燥。


我认为我知道作者想通过仅存储和返回数据位的服务所达到的目的。最近,我建议一个团队摆脱他们在$rootScope上的变量。有更好的方法来实现他们想要的功能。他们通过将它们移动到具有getter和setter的服务中解决了这个问题。显然,他们只是将全局状态问题转移到了其他地方。 - ricksmt
“$rootScope在angular.config()期间是唯一可用的作用域。” 如果我没错的话,我无法在配置阶段访问$rootScope,我相信此配置块仅适用于提供程序。你是指.run()块吗?如果我说错了,请纠正我。 - napstercake
@razorblade 是的,你说得对。有一个根作用域提供程序,但那只是让你调整摘要限制。 - Reactgular

4

全局变量被滥用

$rootScope是一个全局变量,虽然有其适用的场景,但大多数使用它的人都在滥用。以下是全局变量不应该使用的原因:

非本地性 -- 当源代码中各个元素的作用域受到限制时,最容易理解。全局变量可以被程序的任何部分读取或修改,使得难以记住或推理出每种可能的用法。

无访问控制或约束检查 -- 任何程序的一部分都可以获取或设置全局变量,并且可以轻松地打破或忘记其使用规则。(换句话说,get/set访问器通常优于直接数据访问,对于全局数据来说更是如此。)由此可见,缺乏访问控制会极大地阻碍在需要运行不受信任代码(例如与第三方插件一起工作)的情况下实现安全。

隐式耦合 -- 具有许多全局变量的程序通常存在某些变量之间的紧密耦合,以及变量和函数之间的耦合。将耦合项分组成为凝聚单元通常会导致更好的程序。

并发问题 -- 如果多个执行线程可以访问全局变量,则需要同步(这往往被忽略)。当动态链接模块与全局变量一起使用时,即使在数十种不同上下文中测试了两个独立模块也是安全的,组成的系统可能也不是线程安全的。

命名空间污染 -- 全局名称在任何地方都可用。您可能会无意中使用全局变量,而认为自己使用的是本地变量(由于拼写错误或忘记声明本地变量),反之亦然。此外,如果您必须链接具有相同全局变量名称的模块,如果运气好,您将获得链接错误。如果运气不好,链接器将仅将所有使用相同名称的用法视为同一对象。

内存分配问题 -- 一些环境具有使全局变量分配变得棘手的内存分配方案。在语言中特别是真正的"构造函数"具有除分配之外的副作用的情况下(因为在这种情况下,您可以表达两个全局变量相互依赖的不安全情况)。此外,在动态链接模块时,可能不清楚不同库是否具有自己的全局变量实例,还是共享全局变量。

测试和限制 - 使用全局变量的源代码相对较难测试,因为无法在运行之间轻松设置"干净"的环境。更一般地,使用任何未明确提供给该源的全局服务(例如读写文件或数据库)的源代码,由于同样的原因也难以进行测试。对于通信系统,测试系统不变性的能力可能需要同时运行多个系统"副本",这受到任何共享服务(包括全局内存)的使用的严重阻碍 - 这些服务未被提供为测试的一部分。

在Angular中共享数据

当需要在Angular的控制器之间共享数据时,应该使用一个服务。通过自定义的服务,您可以创建一个 getter 和一个 setter 方法。然后将它注入到您需要使用的控制器中,这样您就可以在应用程序中使用它了。

来源http://c2.com/cgi/wiki?GlobalVariablesAreBad


2
如果您引用了整个部分,应考虑引用原始内容。http://c2.com/cgi/wiki?GlobalVariablesAreBad - Kevin Dreßler
抱歉,我忘记了,我现在会添加它。 - Joe Lloyd

1
不存在性能问题。实际上,它会通过缩短依赖注入的服务数量来提高您的性能。一小部分时间
但这是一个设计上的大问题。考虑一个大型应用程序,有数十个视图、复杂的组件,并与许多知名API(例如Twitter、Flickr、Facebook、OAuth等)相关联。
您不会独自开发此应用程序。以下问题将会出现:

命名空间

您正在使用Facebook API,其他人正在使用Twitter API。您们都认为在函数中使用$rootScope是个好主意,并且都编写了$rootScope.login函数。当执行git merge时,你如何解决这个问题?您需要命名空间,因此,您需要开发两个服务myFacebookAPImyTwitterAPI,然后可以为登录实现相同的接口。(login(user,pw))。请注意,在控制器中,这使您能够抽象出您正在处理的实际社交网络,例如:
$scope.callAction = function (action) {
var service;
    if ($scope.serviceSelected === 'fb') {
         service = myFacebookAPI;
    } else {
         service = myTwitterAPI;
    }
    service[action]();
};

测试

在专业开发中,你需要编写测试。Angular 为你提供了自动化测试服务等工具,但你无法像舒适的方式一样测试你分配给 $rootScope 的内容。

还会出现其他问题,但我认为这应该足以让你自己思考。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接