標籤:
對於一個在前端屬於純新手的我來說,Javascript都還是一知半解,要想直接上手angular JS,遇到的阻力還真是不少。不過我相信,只要下功夫,即使是反人類的設計也不是什麼大的問題。
Okay,廢話不多說。為了弄明白angular JS為何物,我先是從Scope開始。那麼什麼是Scope呢?
類比到其他的程式設計語言上,感覺Scope就像是Data Model的範圍一樣,為Expressions的執行提供上下文,暫且先這麼理解吧。
Scope的特性
接下來,看看Scope有哪些特性呢?
Scope提供$watch方法監視Model的變化。
Scope提供$apply方法傳播Model的變化。
Scope可以繼承,用來隔離不同的application components和屬性存取權限。
Scope為Expressions的計算提供上下文。
對於這四點特性,因為我之前學習過ActionScript、C++、Java,所以第一、三、四點不難理解,唯獨第二點感覺有點雲裡霧裡。本著打破沙鍋問到底的原則,我還是通過Google搜到了一些東西。對於有經驗的老手,板磚請輕拍!
源起Javascript
首先,乍一看,scope.apply()似乎就是一個使得bindings得到更新的普普通通的方法。但稍微多想一點,為什麼我們需要它?一般在 什麼時候用它呢?用弄明白這兩個問題,還得從Javascript說起。在Javascript代碼裡,都是按照一定順序來執行的,當輪到一個程式碼片段執 行的時候,瀏覽器就只會去執行當前的片段,不會做任何其他的事情。所以有時候一些做得不是很好的網頁,當點擊了某個東西之後會卡住,Javascript 的工作方式就是會導致這一現象原因之一!下面我們有一段代碼來感受一下:
var button = document.getElementById(‘clickMe‘);function buttonClicked () { alert(‘the button was clicked‘);}button.addEventListener(‘click‘, buttonClicked);function timerComplete () { alert(‘timer complete‘);}setTimeout(timerComplete, 5000);
當載入Javascript代碼時,先先找一個一個id叫“clickMe”的按鈕,然後添加一個監聽器,然後設定逾時。等待5秒,會彈出一個對話方塊。如 果重新整理頁面並立即點擊clickMe按鈕,會彈出一個對話方塊,如果你不點擊OK,timerComplete函數永遠沒有機會執行。
如何更新bindings
好了,扯了一些看似不相關的東西之後,我們迴歸正題。angular JS是怎麼知道什麼時候資料的變化和頁面需要更新的呢?代碼需要知道什麼時候資料被修改了,但是現在又沒有一種方法直接去通知說某個對象上的資料變了(盡 管ECMAScript 5正在嘗試解決這一問題,但也還是處於實驗階段)。而目前比較主流的策略有以下有兩種解決方案。一種是需要用特殊的對象,讓所有的資料都只能通過調用對象 的方法設定,而不是直接通過property指定。這樣的話,所有的修改就可以被記錄下來了,就知道什麼時候頁面需要更新了。這樣做的弊端就是我們必須去 繼承一個特殊的對象。對於賦值也只能通過object.set(‘key‘, ‘value‘)而不是object.key=value的方式。在架構中,像EmberJS和KnockoutJS就是這麼乾的(雖然我都沒接觸過—— 囧)。另一種就是angular JS採用的方式,在每一次Javascript代碼執行序列執行結束後都去檢查是否有資料的改變。這看起來似乎並不高效,甚至嚴重影響效能。但是 angular JS採用了一些比較巧妙的手段解決了這個問題(還沒研究過,目前尚不明確)。這麼做的好處就是,我們可以隨便使用任意對象,對於賦值方式也沒有限制,而且 對於資料的改變也能覺察到。
對於angular JS採取的這種解決方案,我們關心的是什麼時候資料發生了變化,而這也正是scope.apply()派上用場的地方。對於檢查繫結資料到底有沒有發生變化,實際上是由scope.digest()完成的,但是我們幾乎從來就沒有直接調用過這個方法,而是調用scope.apply()方法,是因為在scope.apply()方法裡面,它會去調用scope.digest()方法。scope.apply()方法帶一個函數或者一個運算式,然後執行它,最後調用scope.digest()方法去更新bindings或者watchers。
什麼時候用$apply()
還是那個問題,那我們到底什麼時候需要去調用apply()方法呢?情況非常少,實際上幾乎我們所有的代碼都包在scope.apply()裡面,像ng−click,controller的初始化,http的回呼函數等。在這些情況下,我們不需要自己調用,實際上我們也不能自己調用,否則在apply()方法裡面再調用apply()方法會拋出錯誤。如果我們需要在一個新的執行序列中運行代碼時才真正需要用到它,而且若且唯若這個新的執行序列不是被angular JS的庫的方法建立的,這個時候我們需要將代碼用scope.apply()包起來。下面用一個例子解釋:
<div ng:app ng-controller="Ctrl">{{message}}</div>
functionCtrl($scope) { $scope.message ="Waiting 2000ms for update"; setTimeout(function () { $scope.message ="Timeout called!"; // AngularJS unaware of update to $scope }, 2000);}
上面的代碼執行後頁面上會顯示:Waiting 2000ms for update。顯然資料的更新沒有被angular JS覺察到。
接下來,我們將Javascript的代碼稍作修改,用scope.apply()包起來。
functionCtrl($scope) { $scope.message ="Waiting 2000ms for update"; setTimeout(function () { $scope.$apply(function () { $scope.message ="Timeout called!"; }); }, 2000);}
這次與之前不同的是,頁面上先會顯示:Waiting 2000ms for update,等待2秒後內容會被更改為:Timeout called! 。顯然資料的更新被angular JS覺察到了。
NOTE:我們不應該這樣做,而是用angular JS提供的timeout方法,這樣它就會被自動用apply方法包起來了。
科學是把雙刃劍
最後,我們再來瞅一眼scope.apply()和scope.apply(function)方法吧!雖然angular JS為我們做了很多事情,但是我們也因此丟失了一些機會。從下面的虛擬碼一看便知:
function$apply(expr) { try { return$eval(expr); } catch(e) { $exceptionHandler(e); } finally { $root.$digest(); }}
它會捕獲所有的異常並且不會再拋出來,最後都會調用$digest()方法。
總結一下
$apply()方法可以在angular架構之外執行angular JS的運算式,例如:DOM事件、setTimeout、XHR或其他第三方的庫。這僅僅是個開始,水還有很深,歡迎大家一起來deep dive!
$scope.$apply