理解Angular Nested Scope 的關鍵:Prototype Chain

/* 原文發表在自己的博客上 歡迎踩踩 */

一個人人都要踩的坑

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 咯~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容