1. 程式人生 > >angular原理及模組簡介

angular原理及模組簡介

本人前端小白,奈何在公司在做一個PC端的程式,用angular寫,不得不自學了一下angular框架。雖然在工作過程中勉強勉強夠了,但是覺得既然用了就稍微瞭解得全面一點,所以花了幾個晚上看了一下angular的developer guide,大概知道了一點點angular這個東東到底是幹啥,後來又花了幾個晚上做點小記錄,防止以後忘了。

Angular簡介(大神可略過)

Angular是一個強大的前端框架,其強大之處主要是可以把靜態頁面與動態資料繫結起來。平時我們看到的網頁介面上面的資料都是固定,但如果我們要變化這些資料,例如我在一個文字框輸入,要實時改動一個文字,腫麼破。這時候有兩種方法(我只想到兩種,求大神告知更多):

1.改變一下,就請求一下後端,例如php,然後後端重新返回一個更新好的頁面,當然這種方法很傻,改變一點小資料就請求後端,的確太傻(由於前端小白,我之前就用這種方式做了一個小網站,後來接觸到angular才發現自己太傻);

2.通過js改寫DOM(for 小白:這裡的DOM可以理解為html,但官方稱呼叫document object model,小白我以前總是不知道這是啥),js最初的document類就可以幹這事兒,後來出現了一個jquery(js的一個強大的庫),也可以方便的改寫DOM,對於這些前端就可以知道的資料就不用請求後端了。

jquery也只是一個庫,提供了一些簡單改變DOM的方法,對於簡單的小工程來說也夠了。但是對於比較大的工程,考慮的不僅是功能的實現,還包括可維護可擴充套件,這就需要MVC模式了(for小白:至於啥是MVC,可以看我介紹spring框架那篇部落格裡面)。如果只用jquery,view的邏輯會和c,m的邏輯混在一起,不便於維護,例如你在文字框裡資料了一個東西,你得用寫程式碼去獲取這個值,然後做處理,或者你的某個值改變了,你還得寫程式碼去更新一下view,而angular就是提供這樣一個解決方案的框架(後面還會有介紹感覺angular的強大)。

Angular裡面的html檔案就是view,叫模板(template),當你的資料變化需要改變模板的時候,不用再js程式碼裡面去改變,你可以什麼都不做,因為angular神奇的地方就是把模板與資料繫結(data binding),當資料改變的時候模板自動就變了,你的view變了(在文字框輸入東西了)也會自動反應到你的資料上面,這就是雙向繫結。在angular的理念裡面,模板就是一副素描畫,資料就是顏色,你想做完這幅畫,只需要向模板填充你想要顏色就行了(也就是填充你的資料),例如下面這個例子,你的輸入自動顯示到介面上

你只需要專注你的資料和模板就夠了,他們之間怎麼填充,angular把這些做好了,也就是剝離了view層對contorller,mdoel層的影響,下面就是angular官方給出的區別

一般處理資料:


angular:


簡單來說就是你用angular了資料和view自動雙向繫結,不用你再程式碼中去更新,不用angular你還要自己寫程式碼在view變了時候去更新資料,在資料變了的時候去更新view。

angular原理

angular是基於js的一個框架,首先需要了解一下js的工作原理。

JS原理

瀏覽器裡面有一個事件佇列(event queue),使用者觸發啥事兒,或者網路請求,延時操作(例如定時器之類),都是一個event,瀏覽器會輪訓這些事件,然後呼叫這些回撥(這裡的回撥簡單來說可以理解為觸發一個函式),然後就進入JavaScript的環境中執行(JavaScript context),在這裡面可以改變資料,操作DOM(也就是html結構),然後再退出JavaScript環境,又進入瀏覽器環境,然後瀏覽器根據之前的改動重新繪製介面,這就是個一個流程。

angular原理

           angular的執行就是在JavaScript context裡面自己實現了一套環境,叫做angular環境(angular context),非angular那部分環境叫經典環境(classic context),

在angular context裡面也有一個佇列,這個佇列裡面是watch列表,列表裡面裝的就是那些被監聽的變數,包括那些進行資料繫結的變數(也就是和view進行繫結的那些)。如果使用者改變了一個綁定了資料的view,這時候會觸發一個angular函式$apply(也就是把這個event放入了event queue,然後輪訓到這個的時候就觸發了),然後把這個改變的值更新進繫結的那個變數,再開始呼叫一個digest的函式,digest就是用來輪訓這個watch列表,看這個列表中的指是否變動,如果有變動就變動改寫相應的DOM(不用angular就要自己寫這部分程式碼,如果你有100個變數,你就要寫100個這種改動,而且如果以後有啥變動,還得自己去重構)。


關於angular原理機制的一些參考:

這裡還有兩點注意,

1.angular會至少輪訓兩遍watch列表,為啥?因為第一次輪訓可能在改寫DOM的時候可能會觸發其他watch列表裡面的變數變化,這時候還會再輪訓,直到連續兩次輪訓的變數不再變化。所以如果你有兩個變數的變化是相互影響的,就是A變了觸發B變,B變了觸發A變,這樣會引起死迴圈,angular好像是在輪訓5次(或者是10次,具體我忘了),如果還發現值沒有穩定,就會報錯(我曾經就幹過,介面突然卡死,整個瀏覽器都卡死了,好不容易開啟控制檯看,全部是angular輪訓報的錯,angular的輪訓直接卡死了整個瀏覽器)。

2.另外還有一點,關於效率問題,有人提出來angular這樣無差別輪訓可能會影響效能,但是angular的創始人給瞭解釋,人能在一個頁面上最多就能看200個元素,在一個web頁面上面不會有這麼多的元素繫結資料,如果繫結這麼多元素需要實時更新,那屬於網頁設計的問題(引自stackoverflow,具體網址找不到),所以並不用擔心輪訓的效率問題,如果真的有效率問題,說明網頁本身可能存在問題(在豆瓣上看到一篇帖子,一個人用angular測了500個ngModel繫結的頁面,很卡,所以對於不必要的繫結,最好不要綁)

angular元件

Controller

           Controller是angular一個重要的元件,基本用angular一定會用到controller。Controller顧名思義,用來控制的,是MVC中的C,邏輯控制。在angular裡面,controller是一個JavaScript的建構函式,這個函式有兩個作用,初始化scope,還有就是增加方法(add behavior)。


這裡稍微簡單解釋一下scope(後面會說一下),scope是一個物件(object)可以理解為是連線view和controller的一個橋樑,scope的屬性中有一些值,有一些方法(behavior),在html中可以直接訪問到,如下圖,ng-click裡面的那些方法都是scope的一個屬性,還有顯示出來的那些值。在scope裡面初始化之後,在view(也就是html中,angular官方叫做template)裡面就就可以訪問到這些值,也可以觸發這些方法,這也就是angular資料雙向繫結的具體使用(很簡單吧,不用寫一堆jquery了)。

           所以controller的作用就在於上面說的,初始化scope和為scope增加方法,同時angular官方也給出了一些不建議使用的方式(如下圖),因為這樣操作基本上都有更好的方式

Tips:

1.        angular在1.2版本後多了一個controlleras的語法,這個語法允許為controller起個別名(有木有感覺像sql裡面的as),如下圖


如果不用這個語法,需要在controller這個函式裡面依賴注入(Dependency Injection,後面也會介紹到,如上面那個例子所示,在函式的引數裡面寫個$scope)。如果用這個語法就不用了,兩者的不同就在於不用controller as這個語法,html通過訪問scope的屬性來訪問資料,所以要把給html訪問的資料寫進scope的屬性,如果用controller as,整個controller這個例項(例子中的demo)會作為scope的一個屬性,例如html要訪問一個data屬性的值

Controller as 訪問的是scope.demo.data

Controller 訪問的是scope.data

2.        controller繼承,每個controller繼承其實是scope的繼承,可以簡單理解為JavaScript的繼承,具體可以看我另一篇部落格(如果有時間發的話T^T),或者是angular官方文件上面說的。在這裡就是如果子controller裡面沒有指定這個資料,就會用父類的,如果指定了就用子類的,但是要注意如果是改變model裡面的值,有可能改父類的值(下面還會介紹這個概念)

Service

Service可以理解為MVC結構中的M層,來處理具體的業務邏輯,最理想的程式碼就是在view裡面觸發了controller中的函式,然後controller來呼叫model裡面具體的處理,然後model返回給controller改scope的資料,反應在view上面。Service就是這個作用,在angular裡面,service有兩個特點

1.        懶載入(lazy loading):只有在需要用的時候(也就是在其他service,filter,directive或者controller裡面依賴注入的時候才會生成這個service例項)

2.        單例模式(singleton):service在angular裡面是單例(singleton),只在第一次被注入的時候建立例項,然後存在cache裡面,等需要的時候(也就是另外的依賴注入的時候),從cache裡面取出。所以service的生命週期只要建立之後,除非app退出,否則一直都有這個例項。不能銷燬(我還沒找到一種手動銷燬的辦法,事實上在網上查了一些需要銷燬的例子,其實都可以用其他方法來做,不一定非要銷燬這個service例項)

PS:如果在使用過程中需要多例的樣子,可以自己稍微改動一下,把service當做一個factory模式,返回各種需要的例項。可以參考下面的連線(兩個例子其實一樣,只是在service的寫法上有點不同而已,另外提示連結裡面的網頁在embed標籤欄裡面可以看到樣式)

  • 註冊service

            在官方給出的developerguide裡面主要介紹了兩種方式,factory模式和provider模式,但其實還有一種service模式,所以總共有三種:

1.        factory:angular裡面比較常用的一種方式,註冊一個function,這個function在生成例項的時候會被呼叫到,這個函式返回一個service例項(要自己寫return的),所以只要把自己需要的service寫成一個obj,在這個obj裡面定義要的方法和值,然後再最後return一下這個obj就可以了,如下圖

2.        service:service註冊就更簡單了,相當於對factory做了一層封裝。只要在service裡面寫需要的方法和值就行了,不需要return,如下圖

3.        provider:provider是angular裡面註冊service最底層的方式,無論是service方式註冊還是factory註冊,其實底層都是用的provider這種方式。在provider裡面有一個$get的屬性,這個屬性是一個函式,這個函式就是factory裡面我們寫的那個函式,用service那種寫法在這裡就是new 一個service的那個function賦給它(js裡面function也可以看做一個物件的,所以等於直接new了一個物件給$get),angular就是通過在依賴注入(後面會介紹)的時候,呼叫這個函式獲得一個例項。

如果一個service只有在例項化它之前才知道一個配置,例如讀檔案或者網路返回的一些動態配置,這種就不能在程式碼裡面寫死,需要傳引數給service初始化(有點類似於Java,C++裡面的帶引數的建構函式),這時候就需要在provider裡面配置。所以provider就是在service初始化前對service做一些配置的元件(所以叫provider嘛),在provider裡面留一個介面(也就是留一個函式),等獲取到需要的配置的時候調provider的這個介面,就可以設定service的引數。注意:這裡的provider只是提供了這樣一種方法,可以把它理解為一種工具,config才是呼叫provider的東東。至於為啥不直接用provider呼叫,而還要加個config,我一開始想不通,後來問了我boss,他認為這只是為了好理解,讓人一看就知道是配置。後來我也想了一下,應該也是為了方便統一配置。如果直接調provider裡面建,要麼分開寫,要麼還要自己寫一個function在裡面配置,angular大概應該是為了提供一個統一併且方便理解的介面吧(個人理解)。具體例子如下(注意:在provider裡面寫的名字,在用的時候會在名字後面加一個provider,例如例子中寫的User,但實際呼叫的那個provider是UserProvider)

 

在使用以上三種哪種方式,官方文件似乎沒有給出啥太明顯的建議,個人是這樣認為的,service更傾向於是一種服務,factory傾向於一個工具,provider是需要對這個service做一些配置。下面的部落格寫得很好,裡面也有介紹啥時候用哪種模式,附上一些參考。

scope

           scope是連線controller和view的橋樑,angular官方是這樣描述的:Scopeis the glue between application controller and the view(如上面的例子可以看出)

  • scope對外api:

scope主要提供三個對外API(官方文件中developer guide裡面只提及了兩個)

1.watch:

監聽model是否發生了變化,注意這裡的watch提供三種api監聽

(1)(scope.$watch(watchExpression,listener)只監聽對應的值或者reference是否變化,如果變化就觸發註冊的回撥函式(也就是那個listener)

(2)(scope.$watchCollection(watchExpression,listener):監聽對應的值或者reference以及集合裡是否發生變化(例如集合增加或者減少,但是不包括集合裡面的值變化)

(3)(scope.$watch(watchExpression,listener,true)):監聽對應的值或者reference以及集合裡是否發生變化並且還包括裡面的值是否發生變化,下圖可以比較清晰的看出其中的區別

2.apply:

在angular context外發布model變化的訊息(PS:如果在angular context外變化angular是不會更新介面的,例如用setTimeOut這種方式來更新model,因為setTimeOut只是把一個event放入了佇列裡面,不會馬上執行,等到執行註冊timeout的這個function的時候,如果是完全和angular無關的,也就是沒有用到angular的一些內建命令,這是不會觸發進入angular context的,所以這時候的執行完全就是在angular context外,所以即使更新model的資料,也不會在view上面顯示出來,所以要在外面更新一般要自己呼叫一下apply,具體例子可以參考下面給的那個部落格。一般在ng開頭的命令中和angular自帶的一些service裡面都會自動呼叫apply,所以我們不需要去呼叫)

3.digest:

這個在angular官方文件中沒有列出來,但其實也是可以直接呼叫的,官方應該是不推薦這樣做。呼叫apply就會呼叫digest,digest會輪訓那些watches(註冊了監聽的那些值的列表),如果發現值變化了會呼叫watch註冊的那個function來進行一些處理,可以理解為apply->digest->watch

  •  scope種類

           scope分為兩種,一種是child scope,一種是isolatescope,前者是按照類似DOM結構的繼承關係,後者是完全獨立的(一般用在directive中,因為directive一般是脫離上下文,能夠單獨使用的,例如要做一個通用的列表,在用的時候只需要傳個列表值進來就可以了,這種和上下文無關,所以一般是獨立的,就類似於Android裡面的adapter一樣)

  • scope繼承

           對於child scope的繼承,就和JavaScript的繼承差不多,簡單來說就是如果子scope中沒有的屬性,會去父scope去找,一層一層去找.


           如果這時候想賦值,不會改到父scope中的屬性,例如parentScope.a= 1,如果childScope中沒有指定a那麼childScope.a也是1,但是如果這時候賦值childScope.a = 2,這時候parentScope.a還是1,為啥,因為那個賦值語句對於JavaScript不是一個改變值的語句,是為childScope建立了一個a的屬性值等於2,所以父parentScop不受影響。但是如果屬性是model(也就是物件)就不一樣了,以為訪問model的時候傳的reference(這裡和C++是不一樣的,Java和JavaScript裡面都是把類作為reference傳,C++是通過拷貝建構函式拷貝一份,除非修改拷貝建構函式,否則預設是傳值),例如:parentScope.a.value = 1,如果childScope沒有指定,那麼childScope.a.value也是1,這時候賦值childScope.a.value = 2,那麼這時候父parentScope.a.value也是2。為啥,因為childScope.a是訪問parentScope的屬性(如果childScope裡面沒有指定,注意這個前提),由於a是個model(物件),所以訪問的是地址(reference),這時候a.value=2就是對這個地址的值進行了改寫,所以parentScope也會被改變。當然如果先把childScope.a=newA,這樣childScope指向的就不是parentScope的了,這時候再改a的值就不會影響到parentScope了。建議可以在自己在console裡面試試(JavaScript中function其實就是類,我的另外一篇部落格也介紹了關於JavaScript和angular的繼承關係)

參考自:

  • 追蹤scope

           這是angular官方給出的怎麼在view中除錯scope,也就是看scope當前的一些值

           其實還有另一個方法,也就是我們在專案中用到的一個方法,設一個全域性變數,然後再每個controller裡面都把scope賦值給這個全域性變數,這樣可以在console裡面從這個全域性變數裡面看到想追蹤的scope的值了。但是注意:如果這個全域性變數只是為了除錯,不要在程式碼中使用這個全域性變數,也就是不要讀取,因為這個存在只是作為除錯使用的,是隨時會去掉的一個東西,如果有程式碼邏輯依賴這個全域性變數的值,在去掉之後會導致錯誤的。所以不要讓程式碼依賴一個隨時會去掉的變數。

  • Scope事件分發

           這個在專案中我們基本沒用過,但angular提供了這個機制,emit和broadcast(做Android的同學應該對這個單詞比較有感覺吧,但其實這類似於Android裡面的事件傳遞機制event dispatch,例如touchEvent和clickEvent這類,但angular這個似乎不存在消費,因為專案沒用這個,所以也沒仔細考證,求大神告知)

emit:

釋放事件,當前scope和父scope都可以收到這個事件,如果在對應的scope裡面有註冊這個scope的回撥,就會呼叫這個回撥函式。

Broadcast:

釋出事件,當前scope和子scope都會收到這個事件,如果在對應的scope裡面有註冊這個scope的回撥,就會呼叫這個回撥函式。

具體例子可參考

DependencyInjection(依賴注入)

           依賴注入是在很多程式語言和框架中都會提及的一個東西。其實也很好理解,首先什麼是依賴,A模組(例如類,方法),需要用到B模組中的東西,這時候就說A對B有依賴,例如A類裡面有個add的方法,在B類裡面需要用到這個這個add的方法,就是B對A有依賴。這時候就需要注入(其實注入也可以理解為初始化這種意思),在Java裡面可能就需要new 一個A,在angular裡面,就直接用函式引數這種形式來寫,但是要先在其他地方定義這個依賴的類,就是要定義一個service(如上面提到的service那部分),然後再把這個service作為一個函式引數傳進來。這樣就相當於new了這樣一個物件。具體可以參考上面service部分的例子或者angular 官方的例子

依賴注入寫法在angular中有三種:

1.Inline ArrayAnnotation:

           在中括號裡面用單引號寫上,並且在function的引數裡面寫上,而且要注意順序一致

2.$inject PropertyAnnotation

           用$inject來寫,同時也要注意引數順序一致

3.ImplicitAnnotation

           只在function的引數裡面寫,最簡單的一種寫法,但是也是angular官方不推薦的。因為這種寫法在程式碼混淆中會出問題,當然也有一個工具解決這個問題,這裡就不提及了,詳見angular官方文件https://docs.angularjs.org/guide/di

另外angular還提供一種嚴苛模式(Android裡面其實也有一種嚴苛模式,但是和angular這個不同,Android的同學不要弄混了),不允許Implicit Annotation,一旦用了Implicit Annotation就會報錯,這裡也就不介紹了,詳見angular官方文件同上。另外關於解決依賴的問題,其實有三種方式(如下圖),但angular認為前兩種方式不好,因為前兩種要自己編碼,比較麻煩,特別是在單元測試的時候,所以才用第三種(spring框架也是用的這種方式),angular裡面主要就是靠$injector來建立和追蹤依賴,所以減輕了開發者的負擔。更多詳情參考angular官方文件,這裡就不提及了。

template

           angular裡面的template其實就是html,在angular中,可以用下面四種方式來控制模板(html)的顯示(如圖),都比較簡單,看看例子就知道了,這裡就不提及了。angular建議如果是簡單的app,可以把所有html寫在同一個html檔案中,然後用directive這些來控制(一般就是index.html),如果比較複雜一點的app,可以把不同的view(也就是html)放在同一個page裡面,但是把各自view定義在不同的html檔案中,然後通過引用的方式在載入在這個page中(我們的專案就是通過這種方式來做的,其實也可以認為是一個index.html,但是裡面的很多view都來自於其他html檔案)


參考自:

後記:本來想仔細研究完angular官方文件再發,但後來發現已經寫了六千多字了,放在一篇裡面發不太好,剛好這裡面介紹的都是angular一些基本的元件,算是基礎篇吧。其次就是剛好趕上公司業務調整,現在還需要接手其他業務線的工作,後面可能也不太有時間寫多少js了,所以先發出來,也算是對過去一段時間寫angular的一個小總結,希望以後有空再寫個這篇部落格的續集吧。

因為前人,才能更高

2. angular官方文件developer guide:https://docs.angularjs.org/guide/concepts(可以從concepts這個模組往後看,本文大部分是基於angular developer guide,主要來自同一個網站,文章中已經指明,在此就不一一列出各模組的引用了)

5.stackoverflow關於如何銷燬service:http://stackoverflow.com/questions/32781488/how-to-destroy-an-angular-factory-instance(最後回答者建議不要銷燬,其實可以用其他方式來實現業務邏輯,這裡主要是想說明其實很多時候不用非要銷燬service,可以用其他的方式來做)

6.多型service的寫法:http://jsfiddle.net/rbdmjLok/3/ (例子為一個計數service,在兩個地方分開計數的例子,一個service多處獨立使用)