/* 原文發(fā)表在自己的博客上 歡迎踩踩 */
一個(gè)人人都要踩的坑
Angular容易上手的一個(gè)重要原因就是data binding非常簡(jiǎn)單朴则,當(dāng)你在controller里面給scope綁定上一個(gè)object,立刻就能在view中show出來情屹,而且也能夠非常輕松地實(shí)現(xiàn)two way binding。生活十分愉快概龄。
但突然有一天昌腰,不知道從加了哪一行代碼開始,two way binding不工作了坏挠。你翻箱倒柜把書從頭翻到尾,到SO上求爺爺告奶奶邪乍,最終你發(fā)現(xiàn)降狠,你遇到了一個(gè)名叫nested scope
的問題。
這個(gè)坑長(zhǎng)什么樣
我們來舉一個(gè)不能再簡(jiǎn)單的栗子庇楞,童鞋們可以到這里看demo榜配。首先我們有個(gè)html頁(yè)面充當(dāng)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;
});
很容易看出,我們這個(gè) angular app吕晌,其實(shí)就是把 $scope.name
綁定到 p
和兩個(gè) input
element 上蛋褥。初始化后,頁(yè)面是這樣的睛驳,初始值都是 world
然后我們?cè)诘谝粋€(gè) input box 里面將文字改成 kitty
接下來屬于高危動(dòng)作乏沸,睜大你的雙眼:修改第二個(gè) input box 里的 text淫茵,把它改成 peng 。驚人的是屎蜓,Title 和 第一個(gè) input 里的 kitty 并未隨之改變痘昌。
最后就是見證奇跡的時(shí)刻钥勋,修改第一個(gè) input box 的值炬转,改回成 world,Title 立刻隨著一起改變算灸,但第二個(gè) input box 像是與這個(gè)世界失去了聯(lián)系扼劈。
在分析上面的 case 中 到底發(fā)生了什么之前,我們一起回顧一下 JavaScript 的基礎(chǔ)知識(shí) Inheritance and the prototype chain菲驴。JS 高玩自行跳過這個(gè)章節(jié) :)
什么是prototype
我們知道在 C++/Java/C# 這樣的面向?qū)ο缶幊陶Z(yǔ)言中荐吵,我們可以使用繼承(inheritance)來實(shí)現(xiàn)屬性和方法的共享,減少冗余代碼的書寫。
JavaScript 也支持繼承先煎,但是它并沒有類的概念贼涩,而是使用 prototype 來實(shí)現(xiàn)這一目標(biāo)。JavaScript 中的每個(gè)對(duì)象都有一個(gè)內(nèi)部私有的鏈接指向另一個(gè)對(duì)象薯蝎,這個(gè)對(duì)象就是原對(duì)象的原型(prototype)遥倦。這個(gè)原型對(duì)象也有自己的原型,直到對(duì)象的原型為 null 為止(也就是沒有原型)占锯。這種一級(jí)一級(jí)的鏈結(jié)構(gòu)就稱為原型鏈袒哥。
擁有了繼承之后,JavaScript 的 Object 就擁有了兩種屬性消略,一種是對(duì)象自身的屬性堡称,另外一種是繼承于原型鏈上的屬性。當(dāng)我們?nèi)プx取 Object 的某個(gè)屬性時(shí)艺演,首先查看當(dāng)前 Object 是否擁有該屬性却紧,有的話返回值,如果沒有的話钞艇,找到它的 prototype啄寡,看看這個(gè)對(duì)象上是否有有該屬性。JavaScript 會(huì)順著 prototype chain 一路上去哩照,直到找到這個(gè)屬性活著 prototype chain 到頭為止挺物。
關(guān)于 prototype 更加詳細(xì)和通透的解釋,大家可以參考 這篇文章 和 ruanyf 老師的大作飘弧,我高中語(yǔ)文老是不及格识藤,就不給大家添麻煩了。我就帶大家來看個(gè)小小的栗子次伶。第一步痴昧,我們創(chuàng)建一個(gè) object,就叫它爹吧冠王。
> parent = { "first_name": "Peng"}
< Object {first_name: "Peng"}
爹有一個(gè)屬性叫做 first_name赶撰,值為 Peng。接著我們生一個(gè)兒子柱彻,
> child = Object.create(parent)
Object.create() 這個(gè)函數(shù)會(huì)創(chuàng)建一個(gè)新的 Object 并將新Object 的 prototype 指定為 傳入的參數(shù)豪娜。比如這里,我們傳入的參數(shù)是 parent哟楷,那么 child 的prototype 就是 parent瘤载,child 會(huì)從 parent 這里繼承屬性。比如:
> child.first_name
< "Peng"
child 上本身并沒有 first_name 這個(gè)屬性卖擅,但是他爹有鸣奔,于是依然得到了 Peng 這個(gè)值墨技。到這里為止,我們展示了如何從 prototype 上繼承一個(gè) primitive value 挎狸。繼承 object property 也是一樣的扣汪。
> parent = { "name" : { "first": "peng", "last": "lv"}}
> child = Object.create(parent)
> child.name.first
< "peng"
童鞋們可以在瀏覽器的 console 里面玩一下
到這里,即使是從沒聽說過 prototype 的朋友肯定也明白了锨匆,這不就是老鼠的兒子會(huì)打洞么私痹。但是關(guān)于 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紊遵。當(dāng)我們?nèi)?get property 的時(shí)候,會(huì)順著 prototype chain 一直往上找侥蒙,但是 set property 并不會(huì)這樣暗膜,而是為當(dāng)前對(duì)象生成一個(gè)新的 property 。比如這樣:
自此 child 和 parent 就失聯(lián)了。下面我們可以來看看 Angular 的 nested scope 是怎么一回事论衍。
Angular 如何創(chuàng)建child scope
在使用 Angular 的一些 built-in directive 時(shí)瑞佩,比如 ng-if/ng-include/ng-repeat/ng-switch/etc 時(shí),需要注意的一點(diǎn)是坯台,Angular 會(huì)為其生成一個(gè)新的 scope炬丸,這個(gè) 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
上有一個(gè) scope 作為膠水來粘合 controller 和 view稠炬,而ng-if
又會(huì)生成一個(gè) scope,這個(gè) scope 向上繼承 controller 的 scope咪啡。這個(gè)繼承 Angular 是如何實(shí)現(xiàn)的呢首启,我們來看源碼
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;
}
我們看到,創(chuàng)建 child scope 的時(shí)候撤摸,會(huì)把 child scope 的 prototype (原型) 設(shè)置為 parent 毅桃。根據(jù)我們上面剛剛溫習(xí)的 prototype 繼承機(jī)制,當(dāng)在第二個(gè) input box 里訪問 ng-model="name"
時(shí)准夷,會(huì)先到 ng-if 上的 child scope 尋找 name 這個(gè)屬性钥飞,如果沒有,沿著 prototype 找到 parent scope冕象,最終找到 name
這個(gè)屬性代承。
而當(dāng)我們往第二個(gè) input box 里面輸入新的值(見 第三張圖片)汁蝶,則觸發(fā)了 prototype 的另一個(gè)規(guī)則 Setting a property to an object creates an own property , ng-if 上的 child scope 增加了一個(gè)新的屬性 name 渐扮,parent scope 上的 name 和 child scope 的 name 從此再無瓜葛论悴。當(dāng)我們?cè)俅涡薷?第一個(gè) input box 里的值時(shí),實(shí)際上我們修改的 parent scope 上的 name 墓律,對(duì)于 第二個(gè) input box 來說膀估,并沒有什么卵用。
問題到這里已經(jīng)清楚了耻讽,Angular 的 nested scope 使用了 prototype 這個(gè)機(jī)制來實(shí)現(xiàn) child scope 對(duì) parent scope 的繼承察纯,當(dāng)我們修改 child scope 上的屬性時(shí),會(huì)導(dǎo)致無法更新 parent scope 的屬性针肥。那么該如何解救它們呢饼记?
有兩招
Dot Notation 和 $parent
第一招,江湖人稱 Dot Notation
換做人能夠聽懂的語(yǔ)言就是慰枕,避免給 child scope 上的屬性賦值具则。還記得上文我們講解 prototype chain 的時(shí)候說過,屬性是 object 也可以繼承
> parent = { "name" : { "first": "peng", "last": "lv"}}
> child = Object.create(parent)
> child.name.first = "hulk"
< "hulk"
> parent.name
< Object {first: "hulk", last: "lv"}
parent 有個(gè)屬性叫 name具帮,name 有個(gè)屬性叫 first 博肋。如果我們修改 child.name.first ,第一步是查找 child 上的 name 屬性蜂厅,沒有找到匪凡,根據(jù) prototype chain 找到了 parent 上的 name 屬性,然后修改了它的 property first
掘猿。整個(gè)過程并沒有給 child 的 property name
賦值病游。
當(dāng)然,如果你直接修改 child.name
稠通,name 的繼承就消失了礁遵。
> child.name = {"first": "captain", last: "america"}
< Object {first: "captain", last: "america"}
> parent.name
< Object {first: "hulk", last: "lv"}
Dot Notation,這個(gè)名字真的是傳神啊采记,修改和訪問 $scope上屬性的屬性($scope.name.first)佣耐,而不是直接操作 $scope的屬性($scope.name),多一個(gè) Dot 唧龄,就解決了 two way binding 的坑兼砖。
第二招,見招拆招既棺,使用$parent讽挟。
不是擔(dān)心修改 child scope 上的屬性么,直接訪問 parent scope 上的屬性不就行了么丸冕。我們?cè)賮砜?Angular 的代碼
child.$parent = parent;
child scope 直接有個(gè) $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>
這個(gè)方法過于暴力,博主不推薦使用胖烛,被同事爆的風(fēng)險(xiǎn)太高了眼姐。
每篇文章的最后總該總結(jié)點(diǎn)什么诅迷,不能虎頭蛇尾....
想到了!以上問題只在 Angular 1.x 出現(xiàn)众旗,因?yàn)?.0開始就沒有 scope 咯~