沒錯,我就是要吹爆Angular

杜帥在掘金發表於2018-06-27

沒錯,我就是要吹爆Angular

距離新版Angular釋出已經過去了超過20個月,社群已經有了相當的規模。前端專案複雜性的加深以及對工程化的推進也讓大家越來越重視起這一“框架”。

但是畢竟正如查理芒格所說:

拿著錘子的人,看啥都像釘子

對與其它前端庫的使用者來說,接受起Angular非常困難,而且即便學習了Angular,也往往不得要領。

科學家發現,在進行程式設計時,大腦主要活躍的區域是語言相關的區域。因此Angular的推廣不能直接靠宣傳Angular,而是應該從其它框架出發,引申出Angular解決了什麼問題,這樣才能事半功倍(你不能為了讓俄羅斯人學會中文就一直對他說中文吧)。

因此我希望藉由其它框架,比如React,Vue出發,結合自己的使用經驗,告訴大家Angular的強大之處,以及選擇Angular帶來的裨益。


沒錯,我就是要吹爆Angular

React曾經席捲前端 React曾經席捲整個前端界,甚至是當時MVVM的唯一選擇,Angular也借鑑了很多React的實現方式。

但是在使用React的時候,試著思考幾個問題:

1.為什麼一定要用setState更新狀態呢

合併多次更新可以避免資源的浪費,又或是避免檢視更新的副作用?

大神Morgan對此有十分詳盡的描述:setState:這個API設計到底怎麼樣

更深入思考一下,可以避免這種額外的語法開銷麼?為什麼不能只關注實現呢

React是採用將一段JSX模板語法編譯成js物件的方式來實現資料對映的,因此必須觸發render函式的重複執行才能更新模板,setState很有效地避免的多次重複執行該函式。

細心的小朋友會發現,如果我將每一次的render拆分得足夠細,比如細到每一個具體的tag,那麼當我更新其資料的時候,是不是可以不用setState了呢(直接修改tag的屬性或者是文字)?

另外一個問題就是,我怎麼在不主動呼叫setState的情況下知道哪個tag的哪個屬性變更了呢?當然,即便是setState也需要知道狀態是否改變,不過只需要找出是否改變(防止重複渲染),而另一種需要定位改變項的位置。

第一個問題,Angular採用的**模板解析HTML+**的方式,將元素節點直接解析為elementRef,每一個elementRef都有updateRenderer,在有變更的時候呼叫render2函式,而這一函式可以由不同平臺定義(框架設計之初就想到了跨平臺)

而第二個問題,Angular借鑑了前輩AngularJS的方法——髒檢查

什麼是髒檢查?就是遍歷整個元件找到變化的節點。但是問題依舊存在,我怎麼知道當前元件存在變更呢?AngularJs採用的方式是在setController和繫結ng-事件的時候促發髒檢查,必要的時候手動促發髒檢查。

這樣會出現迴圈髒檢查的情況,而Angular借鑑了React單項資料流的概念,使得變更檢測只能自頂向下執行一次,避免手動促發髒檢查。

髒檢查機制
髒檢查機制

那麼問題來了,如何保證所有變更項都會被檢測到呢?

大殺器Zone

新的角度思考,能夠引發Dom變更的情況有哪些?不外乎就是Dom事件,ajax,setTimeout等,Angular借鑑Linux的執行緒本地儲存機制,暴力代理所有可能引發變更的操作angular/zone.js,一旦變更發生,即執行髒檢查。

當然,為了提高髒檢查效能,Angular還能調整檢查策略:

Default模式下的髒檢查
Default模式下的髒檢查

onPush模式下的髒檢查
onPush模式下的髒檢查

onPush模式下一旦某個節點沒有變更,則不檢查其子節點。

好了,一切水到渠成——Zone代理所有可能引發變更的操作,引發髒檢查,定位到了變更之後使用render2更新模板。

setState消失了,你在進行程式設計的時候就只用關注當前元件的資料,至於模板展示,則完全不在你的考慮範圍內。

優雅麼?等等,迴圈的複雜度是很難控制的,一旦使用了髒檢查,會不會出現AngularJs的卡頓情況呢?

JS中的函式呼叫會消耗效能,尤其是在迴圈次數非常多的時候。很多現代瀏覽器能夠智慧感知函式內聯,將函式中的運算內聯進其呼叫棧中執行。但是隻有當呼叫次數可預期的時候,JS引擎才會進行這樣的過程。

首先考慮使用場景,大部分節點的屬性繫結都不會超過10個(畢竟你只會操作class,style,節點屬性和text),那麼當節點屬性少於10個的時候,使得JS引擎進行內聯操作:

github.com/angular/ang…

export function checkAndUpdateElementInline(
    view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any,
    v7: any, v8: any, v9: any): boolean {
  const bindLen = def.bindings.length;
  let changed = false;
  if (bindLen > 0 && checkAndUpdateElementValue(view, def, 0, v0)) changed = true;
  if (bindLen > 1 && checkAndUpdateElementValue(view, def, 1, v1)) changed = true;
  if (bindLen > 2 && checkAndUpdateElementValue(view, def, 2, v2)) changed = true;
  if (bindLen > 3 && checkAndUpdateElementValue(view, def, 3, v3)) changed = true;
  if (bindLen > 4 && checkAndUpdateElementValue(view, def, 4, v4)) changed = true;
  if (bindLen > 5 && checkAndUpdateElementValue(view, def, 5, v5)) changed = true;
  if (bindLen > 6 && checkAndUpdateElementValue(view, def, 6, v6)) changed = true;
  if (bindLen > 7 && checkAndUpdateElementValue(view, def, 7, v7)) changed = true;
  if (bindLen > 8 && checkAndUpdateElementValue(view, def, 8, v8)) changed = true;
  if (bindLen > 9 && checkAndUpdateElementValue(view, def, 9, v9)) changed = true;
  return changed;
}
複製程式碼

而在多於10個的情況下采用迴圈呼叫:

function checkNoChangesNodeDynamic(view: ViewData, nodeDef: NodeDef, values: any[]): void {
  for (let i = 0; i < values.length; i++) {
    checkBindingNoChanges(view, nodeDef, i, values[i]);
  }
}
複製程式碼

另外一個優化方向,便是webworker!

於是,你便得到了效能差不多的(要是遇上不可控的菜鳥,React就會出現效能災難),設計上更為優雅的Angular。

Vue便採用了類似Angular中Dom渲染中解析的方式,但是尤大神認為髒檢查會增大效能開銷,因此採用set,get的proxy模式手動促發變更(既模板變數必須儲存在特定物件中)。

但是雖然髒檢查對效能有所消耗,但是類似React的diff演算法還是需要進行變更檢查,而且還是在渲染過程中動態進行,所以理論上相比Angular的髒檢查會消耗更多的效能

但是React的渲染過程是手動觸發的,配合fibber可以對渲染的次數進行控制,但是毫無疑問在變更檢測方面的效能潛力,髒檢查更甚。

可以說Zone的存在讓髒檢查煥發了青春。

你是採用React的完全手動處理變更?還是採用Vue的手動觸發變更?抑或是Angular的完全不用考慮變更呢?

當你專案複雜到一定程度時,你就知道哪一種更好了~

沒錯,我就是要吹爆Angular


2.狀態管理這麼更新會抓狂的?

我們都知道React在進行跨元件傳遞資料的時候,會採用狀態管理機(例如redux),Vue也使用了Vuex的狀態管理機制,結合上一節中的Vue響應式模型,也能很優雅地管理狀態。

但是問題來了,首先,由於React沒有響應式機制,導致一次狀態管理的變更簡直碎片化地令人抓狂:示例:Todo List · GitBook。這些難道不讓人感到痛苦麼?

Vue有相應的響應式機制,但是真正在寫的時候,一旦專案規模變大,相信很多人都寫出過this.$http://store.xxx.xxx.xxx.xxx的程式碼,你在每一個”.“的位置都會翻來覆去地檢視定義。

而Angular呢?Rxjs和DI才是最終解決方案。

舉例說明,假設我有一個需求:需要在使用者輸入的時候動態搜尋,並將搜尋結果顯示在搜尋框下方。使用Vue的時候我們這麼做:

// computed中處理變更
computed{
    test(){
        return this.$store.search.result
    }
}

// 輸入框觸發變更
onChange(value){
    this.$store.dispatch('searchFromRemote',value)
} 

// 在action中定義變更
actions:{
    async searchFromRemote(ctx,value){
        const result = await axios.get('xxxxxx',{value:value})
        ctx.commit('changeSearchResult',result)
    }
}

// commit中修改值...
複製程式碼

這還是用了async await的情況,還沒有考慮catch,並且由於debounce的移除,導致使用者每敲一次鍵盤,就需要向後端請求一次,還必須配合lodash等函式庫才能實現延時請求的功能。

在這種情況下,還需要你在超過3處區域切換程式設計上下文。

接下來,見證響應式程式設計的威力:

// 直接定義
searchFromRemote(value){
    this.searchResult$ = this.input$.pipe(
      debounce(100),
      switchMap(res=>
        this.http.get('xxxxx',{value:res.value})
      ),
      catch(err=>{
        this.handleError(err)
      })
  )
}
複製程式碼

直接模板處

<test>result {{searchResult$ | async}}</test>
複製程式碼

整合錯誤處理,整合瀏覽器併發,一個函式搞定

接下來我們再修改一下需求,延時處理請求,並且先請求伺服器A,如果伺服器A沒有結果,再請求伺服器B,並且在使用者按下ctrl+z組合鍵時請求伺服器C。

還是一個函式:


searchFromRemote(value){
    this.searchResult$ = this.input$.pipe(
      combineLatest(this.inputKey$.pipe(pairwise()),(inputRes,inputKeyRes)=>{
        if(inputKeyRes[1][0]===17 && inputKeyRes[1][1]===90){
          return {input:inputRes,type:'c'}
        }else{
          return {input:inputRes,type:'a'}
        }
      }),
      debounce(100),
      switchMap(res=>
        if(res.type==='c'){
          return this.http.get('server-c',{value:res.input.value})
        }else{
          return this.http.get('server-a',{value:res.input.value}).pipe(switchMap(res=>{
            if(res.data===undefined){
              return this.http.get('server-b',{value:res.input.value})
            }else{
              return Observable.of(res)
            }
          }))
        }
      ),
      catchError(err=>{
        this.handleError(err)
      })
  )
}
複製程式碼

而如果還採用之前的方式,怕是要停下來罵產品經理了。

嚴格來說狀態管理這個說法並不適合Angular,只有當你操作的時靜態的資料時,狀態才需要被管理。但是Angular操作的全是動態的資料,我只用定義我的資料從生成到顯示會做何種變換,為什麼要在意他被儲存在哪裡?

適應Rxjs的思維非常高效,比如我要處理使用者的輸入,我只需要思考:輸入流——>何種方式變換(與其它流互動或是自己改變)

而採用flux或者redux模式,我們需要定義有哪些資料,哪些操作會引起怎樣的改變,還需要兼顧純函式等語法細節,程式設計實現不應該只關注資料麼?

相信接觸過HDFS管理的同學很容易接受這種流式的資料處理,高度抽象往往會帶來更高的程式設計效率和更易維護的程式碼。

並且當你搭配使用Redux和mobx的時候,得到的不就是一個只有少數幾個運算子的低配版Rx麼?為什麼不一步到位呢


3.你真的需要TypeScript

動態型別一時爽,程式碼重構火葬場的觀念我就不多說了。Js的函式式特性的確強大,但是你的工作是繁複的前端程式設計,不是民兵導彈的制導系統。你的工作還需要面臨CodeReview,需要面臨人事的調動,需要進行分工合作,甚至需要構造可重用的工程化元件。

你不能一天花10個小時時間用以閱讀他人或自己過去的JS程式碼,然後每天工作14個小時,程式設計師不是應該只想每天工作4小時然後年薪百萬麼?

現在就使用TypeScript,告別噁心的程式碼重構,讓自己的程式設計真正能夠物件導向吧。

有了TypeScript,即便是新手,程式碼也是這樣的:

沒錯,我就是要吹爆Angular

當然,高手的程式碼會是這樣:

沒錯,我就是要吹爆Angular

但是不使用TypeScript,管你是誰,程式碼看起來只能像這樣:

沒錯,我就是要吹爆Angular

當然你說你是ramda高手,寫出來的程式碼沒有一個大括號,當我沒說。


4.約定既是框架

一千個人有一千種React程式碼風格,但是Angular的程式碼風格只有一種。你會發現Angular的每一處都是最佳實踐,設計模式的運用是基於Google多年的Java程式設計經驗的,響應式的應用也是基於微軟對於作業系統中非同步處理的經驗總結。

無數的程式設計概念都有其歷史厚重感,而Angular將他們匯聚到了一起。windows中的linq‘時間上的陣列’,spring中的依賴注入,處理HDFS的MR,到linux執行緒本地儲存,再到前端界的MVVM,MVC。

當你站在巨人的肩膀上,完全適應了Angular的程式設計正規化,你才會養成對於優秀實現的不懈追求,並且這些習慣都是有益的。

比如我現在藉助TypeScript和Rxjs在開發cocos creator專案的時候速度很快,從來沒有想到過遊戲開發的體驗能夠如此愉悅。

你能通過學習Angular一覽所有前端程式設計主題,而不用糾結於一些基礎概念,記太多名詞也是負擔,不是麼?

並且,當你能熟練使用Angular的時候,React的靈活性,Vue的小而美才能真正被你所利用。

不瞭解外語的人也不會理解自己的母語——歌德 Angular相對於React和Vue來說是新事物,對於新事物我們要保持開放的心態,積極去嘗試使用Angular,發現他的閃光點,而不是一味地保守,在沒有使用過他的情況下就盲目否定。

不過意識到Angular的強大也不代表你可以否定React的一切,就如上文所說,Angular是一個框架而React和Vue不是

但是Angular能從大局觀上給你帶來很徹底的改變。你只有徹底搞懂了Angular,才能明白React setState的設計思路。當業務複雜化時你能毫不猶豫地選擇Rxjs。你才能將React或者Vue和相應的庫結合起來,組成自己的"Angular"。

相關文章