文章目錄
前言本文整理截至目前 AngularJS 1.5 為止, 個人以為的最佳實務做法。 為什麼現在?基於以下三點理由:
『你可以這麼做,不代表你必須這麼做』,基於『JavaScript: The Good Parts』的哲理,本文適合具有 AngularJS 1.x 實務開發經驗,然而卻隨著 AngularJS 的發展,逐漸對於網路上充斥各種主觀、矛盾的說法、用法感到困惑的開發者。 雖然遵循本文提出的原則,可以以趨近於元件化的風格開發 AngularJS 1.5 應用程式,但本文不涉及 AngularJS 1.x 移轉到 AngularJS 2.0 議題。 在閱讀本文之前,建議讀者可以先閱讀 Angular 1 Style Guide 這篇文章,本文的程式碼基本上遵循該文倡導的風格,除了少數與下一篇參考文章的建議牴觸之外。 本文主要內容則是基於 Sane, scalable Angular apps are tricky, but not impossible. Lessons learned from PayPal Checkout. 這篇文章,並補充個人觀點。 最佳實務不要使用
|
1 | <div ng-controller=“myController”> |
這有什麼問題呢?
ng-controller
違反元件封裝的原則:template 未能與 controller 程式碼封裝在一起,而獨自暴露在 html 中。出處:Don’t specify controllers in your routes
不論是 AngularJS 內建的 router 或是一般常用的 ui-router,典型的寫法,都是同時指定 template
/ templateUrl
及 controller
:
1 | $routeProvider. |
這有什麼問題呢?
其實跟『不要使用 ng-controller
』的狀況類似,都是與封裝有關:
template
來指定,那麼當要重複使用 controller 時,就必須複製該 template,templateUrl
來指定,那麼就有可能不小心指定了錯誤的 template 檔案,造成與 controller 不一致的狀況。ng-controller
)出處:
基於上面兩點,應該將原本可能使用到 ng-controller
的部份,一律改用 component 來寫。
雖然還是可以使用 $compileProvider.directive()
來定義 directive,在 AngularJS 1.5 下,則提供了新的 component()
函數。這是為了讓 AngularJS 1.X 程式移植到 AngularJS 2.0 比較容易,而特地提供,讓開發者以比較接近 AngularJS 2.0 component 寫法的方式來定義元件:
myComponent.html
1 | <strong>{{vm.foo}}</strong> |
myComponent.js
1 | var myTemplate = require(‘./myComponent.html’); |
注意,使用 component()
函數來建立元件時:
restrict: 'E'
的形式建立,也就是只能建立 element 形式的元件,scope: {}
的形式建立,也就是建立 “isolated scope”,不會繼承 parent scope。因此,若需要外部 scope 的資料,必須明確在 bindings
中定義屬性,並在使用 element tag 時傳入。component()
函數中,已使用 bindings
取代 scope
及在 1.3 ~ 1.4 之間引入的 bindToController
屬性。所以在 component()
函數中,scope
及 bindToController
屬性無效,也不應再使用。controllerAs
,則會自動幫你指定為 $ctrl
,也就是說,強迫必須使用 controllerAs
語法 (參考後面『永遠使用 “controllerAs” 語法』)。另外,
component()
函數只能用來建立 element 元件,如果元件必須同時或者只支援其他類型,譬如 attribute 形式,則必須使用 directive()
函數定義。require()
引入 template 檔案的作法,是採用 Webpack 的作法。實務上可以直接使用 templateUrl
指定外部檔案, 或直接以內聯的形式定義 template
內容。myComponent
的設定獨立定義,甚至獨立為一個檔案,這樣將來要移轉為 AngularJS 2.0 元件會比較方便。最後,再提醒一下,定義元件時,使用 myComponent
名稱,這名稱就稱為該元件的 “directive name”。首字小寫,這是 AngularJS 定義元件的推薦慣例。(當作是定義 instance 變數,所以小寫開頭。)
而在 html 中使用時,則必須使用 my-component
,這稱為該元件的 “tag name”。全部小寫,以 -
分隔單字,雖然 HTML 不區分大小寫,但是使用全小寫是推薦的慣例。
而 controller 的名稱為 MyComponent
,因為它是一個 constructor。首字大寫,表示這是一個 class,這是 JavaScript 的慣例。
一旦定義好 component,就可以如下使用:
在 markup 中使用
1 | <body ng-app=“myApp”> |
在路由中使用
1 | $routeProvider. |
好處如下:
bindings
中以 eventName: '&'
的方式定義,同樣由外部以偏好的方式指定,元件不需要依賴於外部 scope 的特定方法名稱,Refactoring Angular Apps to Component Style 這篇文章詳細介紹各種將 controller 改用 component 的形式撰寫的步驟,相當值得一讀,強烈推薦。
component()
定義 element 元件;使用 directive()
定義 attribute directive建議只要是 element 形式的 directive,都使用 component()
函數來建立。其它情況則使用 directive()
函數。這裡一併列出建議的 element 元件 及 attribute directive 的參考寫法,做為對照。
定義 component / element directive:
1 | var myTemplate = ‘<div></div>’; |
定義 attribute directive:
1 | var myDirective = { |
出處:Always use an isolated scope
首先,你只有在定義 directive 時,才能指定 scope 種類 (註)。而 scope 有三種:
isolated scope: scope: {}
建立一個獨立的 scope。要交換的資料必須明確定義。
new scope: scope: true
建立新的 child scope,並且繼承 parent scope。
no scope: scope: false
不建立 scope,直接使用 parent scope。
這有什麼問題呢?
既然 directive 就是 element 和 attribute (還有 class 和 comment 但是官方不建議使用),讓它們可以直接存取父元件的資料不是很奇怪嗎?標準 html 的標籤不都是要自行指定屬性?
繼承了 parent scope,也就意謂著依賴於 parent scope,因此元件的可重用性就大幅降低。
因為 scope 是以類似 prototpye 的方式繼承,親子 scope 之間會發生屬性遮蔽問題 (請參考下一段說明)。
因此,強烈建議一律使用 “isolated scope”,不要使用另外兩種。當需要 parent scope 資料時,請明確定義屬性,並由 parent scope 指定傳入。
註:即使在 ng-controller
中使用 “controllerAs” 語法,也只是幫你以指定的名稱建立一個包裹物件而已,scope 類型仍然是 “new scope”,並無法指定 scope 類型。這也是一個不應該使用 ng-controller
的好理由。
出處:
只要遵循上面『盡可能元件化 (使用 component 取代 ng-controller
)』的原則,即自動 (強制) 獲得使用 “controllerAs” 及 “isolated scope” 的好處。
如果你不需要知道 AngularJS 過去最被詬病的 scope 黑暗歷史,或者,你已經了解什麼是 “controllerAs”,可以放心跳過這一段。
像這樣的寫法:
1 | <input type=“text” ng-model=“username” /> |
其中 username
將直接繫結到上層 scope 的 username
屬性。這裡的關鍵字是『上層 scope』。
這有什麼問題呢?
由於某些 AngularJS 的 directive,譬如 ngIf
,會建立新的 scope,也就是前面的提到的 “new scope”,注意 “new scope” 會繼承到 parent scope。意思是說,你的 binding 將會根據你的 element 所在的位置,因而繼承了你沒有預料到的『上層 scope』,因此而可能有不同的行為。
其次,由於屬性是以 prototype 的方式繼承,一旦 child scope 建立了同名的屬性,就同時遮蔽了 parent scope 的同名屬性。使得原本希望直接更新 parent scope 屬性的意圖落空。
注意,以下範例為了清楚起見,直接使用 ng-controller
建立新的 scope,實務上仍然建議『盡可能元件化 (使用 component 取代 ng-controller
)』。
注意上面 Form 2,當你輸入資料時,ParentController 的 username
並不會同步更新。
為了避開巢狀 scope 的 prototype properties 繼承問題,可以在 parent scope 中以物件的形式將值包裝起來。
這樣,在 child scope 中,只要永遠不直接對 scope 設定屬性,就不用擔心 scope 屬性遮蔽問題。而透過該物件存取其屬性,就能自由與 parent scope 交換存取資料。
注意 vm
在這裡是可以任意自訂的名稱,取名 vm
是 “View Model” 的意思。
由於上面的作法,可以優雅地解決 scope 遮蔽問題,AngularJS 1.2 針對這個作法,新增了 “controllerAs” 語法。
首先,不推薦,但你可以使用 ng-controller="MyController as $ctrl"
的方式啟用這個功能;
再者,你可以在 directive()
及 component()
函數,使用 controller: 'MyController as $ctrl'
的方式啟用,或者以 controllerAs: '$ctrl'
的方式另外指定。
“ControllerAs” 語法,只是上面介紹的作法的語法蜜糖,當你寫 controller as $ctrl
時,AngularJS 在背後所做的事,基本上就跟上面一樣。
細節是這樣的,首先,記住 controller 函數其實是一個 constructor,AngularJS 會以 new
操作子呼叫你的 controller 函數,以建立一個新的物件,然後,AngularJS 會自動幫你在 $scope
中,以 $ctrl
為屬性名稱,將該物件指定給 $scope
。直接以程式展示的話,大概是像這樣:
1 | function MyController() { |
所以,從另一個角度看,在上面的 controller 中的 this
,只是一個普通的 JavaScript 物件,當然也就沒有 $scope 的功能。這就解釋了為什麼使用了 “controllerAs” 語法後,就無法在 controller 中使用 $scope 的功能。因此,如果在 controller 中需要 $scope 功能的話,就必須再另外透過 DI 注入 $scope 物件,就像這樣:
1 | MyController.$inject = [‘$scope’]; |
這一篇解釋得很詳細:
AngularJS: “Controller as” or “$scope”?
這一篇說明為何 1.3 需要引進 bindToController
,以及後來在 1.4 的演進:
Exploring Angular 1.3: Binding to Directive Controllers。
John Papa 是一直大力推廣 “controllerAs” 語法的人:
Do You Like Your Angular Controllers with or without Sugar?
AngularJS’s Controller As and the vm Variable
然後,AngularJS 1.5 引進了上面介紹的 component()
函數,同時,似乎是為了有所區別,AngularJS team 決定 (沒有找到相關資料,請看下面的推論) 引入新的 bindings
屬性,用來完全取代 scope
及 bindToController
。我想這應該是為了避免使用者誤用,而造成模稜兩可的狀況:
退回到 1.4,強制定義 scope: {}
,而使用 bindToController
定義屬性
因為在 1.4 時,bindToController
屬性就可以直接定義屬性,其意義與 bindings
完全相同,只不過並未強制 scope: {}
的定義。若這麼做的話,等於是期望使用者這樣定義:
1 | { |
但是若使用者卻使用 1.3 的語法:
1 | { |
那麼,哪個屬性有效呢?記住,我們已經強制定義 scope: {}
,所以這裡的 scope
定義無效;而我們期望定義為 hash object 的 bindToController
,在這裡當然也找不到正確的屬性定義。
退回到 1.3,仍然使用 scope
定義屬性,強制 bindToController: true
屬性。
也就是說,期望使用者這樣定義:
1 | { |
但是若使用者卻使用 1.4 的功能:
1 | { |
那麼,又是哪個屬性有效呢?同樣地,我們已經強制定義 bindToController: true
,所以這裡的 bindToController
定義無效;而我們期望定義為 hash object 的 scope
,在這裡當然也找不到正確的屬性定義。
所以,由此推論,AngularJS team 為了避免上述兩種模稜兩可的狀況,而決定乾脆同時忽略 scope
和 bindToController
的使用者定義,分別強制其值為 {}
及 true
,而另外使用 bindings
來定義屬性。
雖然不確定理由是否如上述推論,但同時有三個屬性 (scope
, bindToController
, bindings
),提供一樣的功能,顯然又是一個為了向後相容,而疊床架屋的例子,雖然解決了問題,但同時也造成了更多混淆。
$rootScope
出處:Limit your use of $rootScope
應該不須贅言,$rootScope
基本上就等於 global。
出處:Keep your state as close as possible to the components which need it.
大部分情況下,你的元件應該能自給自足,或者,頂多需要由父元件提供資料。有些時候,你可能需要在多個元件之間共用資料。除非你是在開發通用的元件,譬如 Tab / TabPanel,這時你可能要考慮使用 require
屬性。否則,開發一般應用程式時,應該盡量在最靠近它們的共同父元件上提供共用的資料。
index.html
1 | <body ng-app=“app”> |
parent.js
1 | var template = [ |
child-one.js
1 | app.component(‘childOne’, { |
child-two.js
1 | app.component(‘childTwo’, { |
factory()
函數 (忘記 service()
和 provider()
吧)出處:Forget about services and providers
直接先講結論:
在 AngularJS 中,能夠透過 DI 注入使用的,都是服務。
一般常在爭論的,所謂 factory, service 及 provider,其實根本都不是真正的主體,它們都只是用來建立服務的方法。
正確的說,我們透過 factory()
, service()
, provider()
,甚至 constant()
, value()
這些方法,建立服務。而不論是用哪一個方法建立的服務,它們本質上完全相同,唯一的不同就只有建立的形式不同。
爭論什麼是 factory, service 及 provider,只是讓問題失焦。甚至於,我主張,不應該說『一個 factory』,而應該這麼說:『一個使用 factory()
函數建立的服務』。
如果你了解它們背後的實作方式,全部都是基於同一個其界面具有 $get()
函數的物件,你就可以根據需要,自在地採用適合你的情境的『形式』來建立服務;如果還是不了解,就根據本項建議,全部採用 factory()
來建立服務。
許多人都建議只使用 factory,主要理由如下:
我認為以上的說法還不夠精準,最好改為:
以下透過模擬它們的實作方式,稍微說明一下它們的差異。簡單地說,它們的差別可以用下面這段虛擬碼表達,為了簡單起見,dependency injection 處理的部份完全不列入。
首先,我們建立的 module,具有 (自動注入) 下列 $provide
提供的 factory()
, service()
及 provider()
等函數 (其它略):
1 | var $provide = { |
其中 provider()
函數,做了這些事:
new
運算子呼叫我們提供的 ProviderConstructor()
建構子,建立一個物件,這個物件也就是所謂的 Provider 服務,serviceName + 'Provider'
的名稱,註冊為一個服務,這個服務將只能透過 config()
函數注入使用,$get()
方法,此方法回傳的物件才是真正的服務物件,serviceName
為名稱,註冊為一個服務,這個服務將能夠在 run()
, factory()
, service()
, provider()
, controller()
, directive()
… 等函數,以及任意建構子中注入使用。而 factory()
函數,則是:
ProviderConstructor()
建構子,以該建構子做為參數呼叫上面的 provider()
函數,provider()
函數以 new
運算子呼叫該 ProviderConstructor()
建構子時,將建立一個具有 $get()
方法的物件,該 $get()
方法直接被設定為我們傳入的 factoryFunction()
函數,因此呼叫 $get()
方法,即等於呼叫 factoryFunction()
函數,provider()
函數的說明, ProviderConstructor()
建構子建立的物件,將被以 serviceName + 'Provider'
的名稱,註冊為一個所謂的 Provider 服務,provider()
函數的說明,$get()
方法被呼叫,於是 factoryFunction()
函數回傳的任意值,將以我們提供的 serviceName
為名稱,註冊為一個服務,而 service()
函數,則是:
factoryFunction()
函數,以該函數做為參數呼叫上面的 factory()
函數,factoryFunction()
函數中,以 new
運算子呼叫傳入的 ServiceConstructor
建構子,建立服務物件,並將其返回,factory()
函數的說明,factory()
函數又會轉呼叫 provider()
函數,factoryFunction()
函數返回的物件,也就是 ServiceConstructor
建構子建立的物件,最終被以 serviceName
為名稱,註冊為服務,$get()
方法的物件,被以 serviceName + 'Provider'
的名稱,註冊為所謂的 Provider 服務。解釋起來似乎很麻煩,但是直接看程式碼,其實真的沒有什麼驚奇的地方。
重點有兩個:
provider()
函數來建立,該 Provider 服務才有機會附加其他額外方法,而 factory()
和 service()
函數建立的 Provider 服務,都只會有 $get()
方法,我過去一直以為 factory()
函數與 service()
函數的差異是,它每次被要求注入的時候都會執行一次,畢竟它的名稱叫做 “factory”,而且又只是個普通函數,所以我一直以為它就是 “factory method”,應該每次都回傳新的物件。但事實上它卻不是這樣被設計的,它跟 使用 service()
函數來建立服務完全一樣,我們傳入的 factory function 總是只被呼叫一次。
我從來沒有在任何一篇文章中看過有人強調這一點,最多只說它們全部都是 singleton。甚至大部分的文章在描述的時候,反而讓人誤以為每次都會呼叫 $get()
函數,譬如,在 Service vs Factory - Once and for all 這篇文章中這樣描述:
大概的意思就是說,每次我們向 injector 要求時,就會呼叫 provider 的 $get()
函數。
錯錯錯! $get()
函數永遠只會被呼叫一次!
正是因為只會被呼叫一次,並且其結果被儲存下來,所以不論是透過 factory()
, service()
或 provider()
函數,實際上建立的服務都是 singleton,建立之後的行為完全一樣,它們之間真正的差別就是建立的形式不同。
AngularJS team 真的是把一件簡單的事情極度複雜化了,而且明明同樣都是服務,卻用了三種不同的方式來建立,又偏偏取了很不好的一組名稱,才會導致連他們自己人,都會在所謂的解惑文中,用不正確的方式描述其行為。
由下面的範例就可以看出來 (請打開 console 看 log):
personFactory
,但是卻同時存在有 personFactoryProvider
,personFactory.rtti
的值,在兩個 controller 中相同,表示 personFactory
的確是同一個物件,PersonController1
透過 personFactory.defaults()
函數設定的值,在 PersonController2
可以看得到。
了解背後的運作原理之後,再來看看實際上要建立服務時,我們如何透過 factory()
, service()
或 provider()
函數來建立。
基本定義方式如下:
1 | angular.module(‘app’, []) |
雖然透過 provider()
函數建立服務的彈性最大,也能提供設定功能,但是如同前面的範例的 defaults()
函數,設定功能有很多方式可以達成,因此並沒有非使用 provider()
函數不可的理由。而 service()
函數由於強制必須提供 constructor,所以服務本身只能是物件類型,彈性上多少受到一些限制。基於這些理由,多數的人才會建議:一律使用 factory()
函數來建立服務。
factory()
函數來建立服務的常見用法回傳常數
1 | .factory(‘pi’, function () { |
這個作法足以取代 constant()
, .value()
。
回傳服務物件
1 | app.factory(‘personFactory’, personFactory); |
可以看到:
create()
函數。最重要的是,它能夠接受參數。setDefaults()
函數,足以取代 provider。回傳建構子
老實說,我比較不建議這個作法。使用 DI,就是不希望依賴於實作,不希望由 client 自行建構依賴物件。使用 DI 應該直接取得立即可用的物件或服務,應該依賴於其界面,而不是其實作。
1 | .factory(‘Person’, function () { |
大多數時候,module.run()
函數即可勝任模組的初始化工作,沒有必要非在 module.config()
中進行初始化。許多時候,基於封裝原則,甚至於應該在元件中進行初始化。
唯一的例外,就是必須使用到 AngularJS 或第三方程式庫提供的 Provider 的時候,譬如 $routeProvider
。這是因為 AngularJS 做了一個很不好的決定:強制 Provider 只能在 module.config()
中使用。你在開發自己的應用程式時,實在沒有必要跟著這麼做。
出處:Be careful with event publishing and listening
透過 $rootScope
或 $scope
發佈事件,應該是針對不特定受眾所為,通常是設計 API 時才需要考慮。多數時候,你只是在開發應用程式,所有的元件都在你的掌握之中,你可以透過元件之間的屬性,或 callback 函數來傳遞資料。如果是服務,則應該考慮使用 Promise。如果隨意濫用 $rootScope
或 $scope
發佈事件,小心導致程式難以除錯的風險。
$exceptionHandler
出處:Take advantage of $exceptionHandler
你的程式中未捕獲的 exception 都會丟給 $exceptionHandler
這個服務處理,這個服務預設的行為是輸出 log。你可以定義自己的 $exceptionHandler
服務,這將覆蓋原本的服務,讓你有機會自行處理錯誤:也許是上傳錯誤到後端伺服器進行記錄等等。
1 | angular.module(‘exceptionOverride’, []).factory(‘$exceptionHandler’, function () { |
出處:Find a good way to publish logs to the server side
SPA 應用程式是在使用者的瀏覽器中執行,如果沒有適當地記錄 log 訊息,根本無從得知在使用者的瀏覽器中到底發生了什麼事。
最底限至少應該在應用程式發生錯誤時,輸出錯誤訊息,請求使用者幫忙回報。若經過使用者同意,也可以考慮記錄整個執行過程,排除敏感資料,上傳到後端伺服器加以記錄分析。
上傳的時候應該盡力減少發送次數及流量,並且務必做好加密的動作。
另外,可以考慮使用商業化的產品,譬如 Loggy、Paper Trail 等。
如果你的服務 API 遵循 RESTful 原則,你可能會有許多 resources 具有 sub-resources,而 RESTful 要求每個 resource 應該要有唯一的 URL。然而 AngularJS 內建的 ngRoute
不支援巢狀路由,因此並不算 RESTful 友好。
稍具規模,支援 RESTful 的 AngularJS 1.X 應用程式,都應該考慮使用 ui-router,甚至可以考慮搭配 ui-router-extras,以提供更多進階功能。
為什麼不使用新的 AngularJS 2.0 的 ngComponentRouter?
目前不推薦 ngComponentRouter 的主要原因,是因為:
尚未正式發佈
ngComponentRouter 原本計畫要隨同在 AngularJS 1.4 發佈,但是現在已 1.5 卻還是沒有正式發佈,想必 AngularJS team 也還有其他考量。或許只是因為 AngularJS 2.0 尚未正式發佈,而針對 AngularJS 1.x 的規格也仍然還在變動。
目前的參考文件比較缺乏。
最新的文件可以參考 Component Router。其它目前在網路上可以找到的針對 AngularJS 1.x 的文章幾乎全部都已經過時,都是還是使用 controller 的方式。建議可以參考 StackOverflow 上 Angular 1.5 and new Component Router 的回答。或直接參考 ngComponentRouter 的 examples。另外,我也寫了一個 sample 可以參考:
出處:Be careful with promises and error handling
首先,不論是透過 AngularJS 自己的 $q
提供的 Promise,或是標準的 Promise/A+,如果你沒有使用 catch()
捕捉錯誤,任何錯誤,包括 throw 的錯誤以及沒有 fulfilled 的錯誤,都將無聲無息地被吃掉。
再來,除非你是透過 return $q.reject(err)
回傳錯誤,否則任何其它被 throw 出來的錯誤,即使你已經使用 catch()
捕捉了,它們都還是會被丟到前面提到的 $exceptionHandler
服務。這就面臨了兩難的困境。意謂著,如果你使用 catch()
捕捉錯誤,同時也使用 $exceptionHandler
服務處理未捕捉的錯誤,結果是同一個錯誤你將處理兩次。
為了避免這個問題,可以借用 Java 的 checked exception 的概念:屬於你的設計中,明確會丟出來的錯誤,把它們當作是 checked exception,一律使用 $q.reject(err)
來處理,這樣它們就不會進入到 $exceptionHandler
服務;至於哪些你沒有預料到,或者是程式設計錯誤造成的錯誤,則把它們當作是 unchecked exception,讓它們由 $exceptionHandler
服務來把捕捉。
AngularJS 1.x 的模組,只能在啟動階段定義,使用任何的 hack,即使是透過路由來做延遲載入,都無可避免的有其缺陷,更不用說這會無謂地增加程式的複雜度。
出處:Be careful with the digest cycle
在 AngularJS 1.x 中,所謂的雙向資料繫結 (two way data binding),是靠著不斷地檢查 scope 中的變數是否有變動,而在變動時做出反應:更新畫面或根據使用者輸入更新 scope 資料。由於資料之間可能有關聯性,所以某些資料更新後,可能會導致其它資料也需要更新,也就是發生所謂的漣漪效應。因此,AngularJS 會持續地進行檢查、更新,直到資料不再發生變動為止。如果資料持續不斷發生變動,則 10 次 $digest loop 之後,AngularJS 就會丟出 exception。
AngularJS 如何檢查資料是否發生異動?它並不是靠傳說中的 Object.observe()
(註),而是藉由儲存舊值,透過比對舊值與現值,來判斷是否發生異動。然而,這裡存在著一個陷阱:由於資料可以經由函數回傳,如果函數將資料包裹在物件或陣列中回傳,雖然就『值』本身來看,並沒有發生異動,實際的包裹物件已經不是同一個了。
1 | $scope.foo = function () { |
JavaScript 並不像 Java 一樣,有所謂 equals()
和 hashCode()
方法,用來快速檢查兩個物件的『值』是否相等,也就是所謂的『等價』。因此,為了效率考量,AngularJS 並不會進行所謂 deepEqual()
檢查,而只是檢查物件是不是同一個 (位址相同)。由於每次要比對現值時,就會呼叫一次函數,而函數每次都會回傳一個新的包裹物件,因此每次都判定為資料發生異動,於是超過 10 次之後,就丟出錯誤了。
因此,上面的例子比較好的作法,應該像這樣:
1 | var foo = { bar: ‘baz’ }; |
這樣每次被呼叫時,回傳的都是同一個物件,AngularJS 就不會誤判了。
註:Object.observe()
提案已經於 2015 年 11 月撤銷,因此未來也不可能使用 Object.observe()
來檢查資料異動。
AngularJS 可以算是最早引入自訂 html 元件概念的 framework,只是早期的推廣,強調 ng-controller
的使用方便性,導致許許多多程式將業務邏輯與畫面呈現全部混在 controller 中。雖然 AngularJS 2.0 發佈在即,也更趨近於 Web Component 標準,舊有使用 AngularJS 1.x 的應用程式,只要嚴守元件化的原則,仍然可以寫出相當容易維護的程式,甚至於更有利於將來移植到 AngularJS 2.0。
以上整理,已盡力將參考資料完整列出,惟個人學識有限,如有謬誤或不足之處,歡迎補正,謝謝。
|
来自: 菩提与蜗牛 > 《AngularJS》