美文网首页Tech Daily Life程序员我爱编程
理解Angular Nested Scope 的关键:Proto

理解Angular Nested Scope 的关键:Proto

作者: 甩甩鸟 | 来源:发表于2015-07-03 21:18 被阅读375次

    /* 原文发表在自己的博客上 欢迎踩踩 */

    一个人人都要踩的坑

    Angular容易上手的一个重要原因就是data binding非常简单,当你在controller里面给scope绑定上一个object,立刻就能在view中show出来,而且也能够非常轻松地实现two way binding。生活十分愉快。

    但突然有一天,不知道从加了哪一行代码开始,two way binding不工作了。你翻箱倒柜把书从头翻到尾,到SO上求爷爷告奶奶,最终你发现,你遇到了一个名叫nested scope的问题。

    这个坑长什么样

    我们来举一个不能再简单的栗子,童鞋们可以到这里看demo。首先我们有个html页面充当view

    <body ng-controller="MainCtrl">
      <p>Hello {{name}}!</p>
      <input ng-model="name">
      <div ng-if="includeForm">
        <input ng-model="name">
      </div>
    </body>
    

    接着咱们有段javascript

    var app = angular.module('plunker', []);
    app.controller('MainCtrl', function($scope) {
      $scope.name = 'World';
      $scope.includeForm = true;
    });
    

    很容易看出,我们这个 angular app,其实就是把 $scope.name 绑定到 p 和两个 input element 上。初始化后,页面是这样的,初始值都是 world

    一切都在掌控之中一切都在掌控之中

    然后我们在第一个 input box 里面将文字改成 kitty


    完美!完美!

    接下来属于高危动作,睁大你的双眼:修改第二个 input box 里的 text,把它改成 peng 。惊人的是,Title 和 第一个 input 里的 kitty 并未随之改变。


    发生了什么...发生了什么...

    最后就是见证奇迹的时刻,修改第一个 input box 的值,改回成 world,Title 立刻随着一起改变,但第二个 input box 像是与这个世界失去了联系。

    已经彻底被玩坏已经彻底被玩坏

    在分析上面的 case 中 到底发生了什么之前,我们一起回顾一下 JavaScript 的基础知识 Inheritance and the prototype chain。JS 高玩自行跳过这个章节 :)

    什么是prototype

    我们知道在 C++/Java/C# 这样的面向对象编程语言中,我们可以使用继承(inheritance)来实现属性和方法的共享,减少冗余代码的书写。

    JavaScript 也支持继承,但是它并没有类的概念,而是使用 prototype 来实现这一目标。JavaScript 中的每个对象都有一个内部私有的链接指向另一个对象,这个对象就是原对象的原型(prototype)。这个原型对象也有自己的原型,直到对象的原型为 null 为止(也就是没有原型)。这种一级一级的链结构就称为原型链。

    拥有了继承之后,JavaScript 的 Object 就拥有了两种属性,一种是对象自身的属性,另外一种是继承于原型链上的属性。当我们去读取 Object 的某个属性时,首先查看当前 Object 是否拥有该属性,有的话返回值,如果没有的话,找到它的 prototype,看看这个对象上是否有有该属性。JavaScript 会顺着 prototype chain 一路上去,直到找到这个属性活着 prototype chain 到头为止。

    关于 prototype 更加详细和通透的解释,大家可以参考 这篇文章 和 ruanyf 老师的大作,我高中语文老是不及格,就不给大家添麻烦了。我就带大家来看个小小的栗子。第一步,我们创建一个 object,就叫它爹吧。

    > parent = { "first_name": "Peng"}
    < Object {first_name: "Peng"}
    

    爹有一个属性叫做 first_name,值为 Peng。接着我们生一个儿子,

    > child = Object.create(parent)
    

    Object.create() 这个函数会创建一个新的 Object 并将新Object 的 prototype 指定为 传入的参数。比如这里,我们传入的参数是 parent,那么 child 的prototype 就是 parent,child 会从 parent 这里继承属性。比如:

    > child.first_name
    < "Peng"
    

    child 上本身并没有 first_name 这个属性,但是他爹有,于是依然得到了 Peng 这个值。到这里为止,我们展示了如何从 prototype 上继承一个 primitive value 。继承 object property 也是一样的。

    > parent = { "name" : { "first": "peng", "last": "lv"}}
    > child = Object.create(parent)
    > child.name.first
    < "peng"
    

    童鞋们可以在浏览器的 console 里面玩一下


    没图我说个球啊没图我说个球啊

    到这里,即使是从没听说过 prototype 的朋友肯定也明白了,这不就是老鼠的儿子会打洞么。但是关于 prototype 的继承,我想把 MDN 文档里的一句话高亮出来

    Setting a property to an object creates an own property. The only exception to the getting and setting behavior rules is when there is an inherited property with a getter or a setter.

    最重要的就是第一句了,Setting a property to an object creates an own property。当我们去 get property 的时候,会顺着 prototype chain 一直往上找,但是 set property 并不会这样,而是为当前对象生成一个新的 property 。比如这样:

    右手 左手 不是慢动作重播!右手 左手 不是慢动作重播!

    自此 child 和 parent 就失联了。下面我们可以来看看 Angular 的 nested scope 是怎么一回事。

    Angular 如何创建child scope

    在使用 Angular 的一些 built-in directive 时,比如 ng-if/ng-include/ng-repeat/ng-switch/etc 时,需要注意的一点是,Angular 会为其生成一个新的 scope,这个 scope 继承自 外层的 scope。回到我们最上面提到的 demo

    <body ng-controller="MainCtrl">
      <p>Hello {{name}}!</p>
      <input ng-model="name">
      <div ng-if="includeForm">
        <input ng-model="name">
      </div>
    </body>
    

    MainCtrl 上有一个 scope 作为胶水来粘合 controller 和 view,而ng-if 又会生成一个 scope,这个 scope 向上继承 controller 的 scope。这个继承 Angular 是如何实现的呢,我们来看源码

    function createChildScopeClass(parent) {
      function ChildScope() {
        this.$$watchers = this.$$nextSibling =
            this.$$childHead = this.$$childTail = null;
        this.$$listeners = {};
        this.$$listenerCount = {};
        this.$$watchersCount = 0;
        this.$id = nextUid();
        this.$$ChildScope = null;
      }
      ChildScope.prototype = parent;
      return ChildScope;
    }
    

    我们看到,创建 child scope 的时候,会把 child scope 的 prototype (原型) 设置为 parent 。根据我们上面刚刚温习的 prototype 继承机制,当在第二个 input box 里访问 ng-model="name" 时,会先到 ng-if 上的 child scope 寻找 name 这个属性,如果没有,沿着 prototype 找到 parent scope,最终找到 name 这个属性。

    而当我们往第二个 input box 里面输入新的值(见 第三张图片),则触发了 prototype 的另一个规则 Setting a property to an object creates an own property , ng-if 上的 child scope 增加了一个新的属性 name ,parent scope 上的 name 和 child scope 的 name 从此再无瓜葛。当我们再次修改 第一个 input box 里的值时,实际上我们修改的 parent scope 上的 name ,对于 第二个 input box 来说,并没有什么卵用。

    鸟都不鸟你啊鸟都不鸟你啊

    问题到这里已经清楚了,Angular 的 nested scope 使用了 prototype 这个机制来实现 child scope 对 parent scope 的继承,当我们修改 child scope 上的属性时,会导致无法更新 parent scope 的属性。那么该如何解救它们呢?

    有两招

    Dot Notation 和 $parent

    第一招,江湖人称 Dot Notation

    换做人能够听懂的语言就是,避免给 child scope 上的属性赋值。还记得上文我们讲解 prototype chain 的时候说过,属性是 object 也可以继承

    > parent = { "name" : { "first": "peng", "last": "lv"}}
    > child = Object.create(parent)
    > child.name.first = "hulk"
    < "hulk"
    > parent.name
    < Object {first: "hulk", last: "lv"}
    

    parent 有个属性叫 name,name 有个属性叫 first 。如果我们修改 child.name.first ,第一步是查找 child 上的 name 属性,没有找到,根据 prototype chain 找到了 parent 上的 name 属性,然后修改了它的 property first 。整个过程并没有给 child 的 property name 赋值。

    当然,如果你直接修改 child.name,name 的继承就消失了。

    > child.name = {"first": "captain", last: "america"}
    < Object {first: "captain", last: "america"}
    > parent.name
    < Object {first: "hulk", last: "lv"}
    

    Dot Notation,这个名字真的是传神啊,修改和访问 $scope上属性的属性($scope.name.first),而不是直接操作 $scope的属性($scope.name),多一个 Dot ,就解决了 two way binding 的坑。

    第二招,见招拆招,使用$parent。

    不是担心修改 child scope 上的属性么,直接访问 parent scope 上的属性不就行了么。我们再来看 Angular 的代码

    child.$parent = parent;
    

    child scope 直接有个 $parent 属性来 reference parent scope。

    <body ng-controller="MainCtrl">
      <p>Hello {{name}}!</p>
      <input ng-model="name">
      <div ng-if="includeForm">
        <input ng-model="$parent.name">
      </div>
    </body>
    

    这个方法过于暴力,博主不推荐使用,被同事爆的风险太高了。


    每篇文章的最后总该总结点什么,不能虎头蛇尾....

    想到了!以上问题只在 Angular 1.x 出现,因为2.0开始就没有 scope 咯~

    相关文章

      网友评论

      本文标题:理解Angular Nested Scope 的关键:Proto

      本文链接:https://www.haomeiwen.com/subject/ffhyqttx.html