如何閱讀大型前端開源項目的源碼
作者: Daniel
github.com/ProtoTeam/blog/blob/master/201805/3.md
1月底的時候,Angular 官方博客發布了一則消息:
AngularJS is planning one more significant release, version 1.7, and on July 1, 2018 it will enter a 3 year Long Term Support period.
即在 7月1日 AngularJS 發布 1.7.0 版本之後,AngularJS 將進入一個為期 3 年的 LTS 時期。也就是說 2018年7月1日 起至 2021年6月30日,AngularJS 不再合併任何會導致 breaking changes 的 features 或 bugfix,只做必要的問題修復。詳細信息見這裡:Stable AngularJS and Long Term Support
看到這則消息時我還是感觸頗多的,作為我的前端啟蒙框架,我從 AngularJS 上汲取到了非常多的養分。雖然 AngularJS 作為一款優秀的前端 MVW 框架已經出色的完成了自己的歷史使命,但考慮到即便到了 2018 年,許多公司基於 AngularJS 的項目依然處於服役階段,結合我過去一年多在 mobx 上的探索和實踐,我決定給 AngularJS 強行再續一波命。(搭車求治拖延症良方,二月初起草的文章五月份才寫完,新聞都要過期了)
準備工作
在開始之前,我們需要給 AngularJS 搭配上一些現代化 webapp 開發套件,以便後面能更方便地裝載上 mobx 引擎。
AngularJS 配合 ES6/next
現在是2018年,使用 ES6 開發應用已經成為事實標準(有可能的推薦直接上 TS )。如何將 AngularJS 搭載上 ES6 這裡不再贅述。
基於組件的應用架構
AngularJS 在 1.5.0 版本後新增了一系列激動人心的特性,如 onw-way bindings、component lifecycle hooks、component definition 等,基於這些特性,我們可以方便的將 AngularJS 系統打造成一個純組件化的應用(如果你對這些特性很熟悉可直接跳過至AngularJS 搭配 mobx)。我們一個個來看:
onw-way bindings 單向綁定
AngularJS 中使用來定義組件的單向數據綁定,例如我們這樣定義一個組件:
angular
.module("app.components",[])
.directive("component",()=>({
restrict:"E",
template:"
count: {{$ctrl.count}}
increase"
scope:{
},
bindToController:true,
controllerAs:"$ctrl",
})
使用時:
{{app.count}}
componentcount="app.count">component>
當我們點擊組件的 increase 按鈕時,可以看到組件內的 count 加 1 了,但是並不受影響。
區別於 AngularJS 賴以成名的雙向綁定特性,單向數據綁定能更有效的隔離操作影響域,從而更方便的對數據變化溯源,降低 debug 難度。
component lifecycle hooks 組件生命周期鉤子
1.5.3 開始新增了幾個組件的生命周期鉤子(目的是為更方便的向 Angular2+ 遷移),分別是(1.5.8增加),寫起來大概長這樣:
classController{
$onInit(){
// initialization
}
$onChanges(changesObj){
const{user}=changesObj;
if(user&& !user.isFirstChange()){
// changing
}
}
$onDestroy(){}
$postLink(){}
$doCheck(){}
}
angular
.module("app.components",[])
.directive("component",()=>({
controller:Controller,
...
}))
事實上在 1.5.3 之前,我們也能藉助一些機制來模擬組件的生命周期(如、等),但基本上都需要藉助這座『『橋樑』』。但現在我們有了框架原生 lifecycle 的加持,這對於我們構建更純粹的、框架無關的 ViewModel 來講有很大幫助。更多關於 lifecycle 的信息可以看官方文檔:AngularJS lifecycle hooks
component definition
AngularJS 1.5.0 後增加了語法用於更方便清晰的定義一個組件,如上述例子中的組件我們可以用語法改寫成:
angular
.module("app.components",[])
.component("component",{
template:"
count: {{$ctrl.count}}
increase"
bindings:{
count:"
onUpdate:"&"
},
})
本質上就是的語法糖,bindings 是的語法糖,只不過語法更簡單語義更明了,定義組件變得更方便,與社區流行的風格也更一致(熟悉 vue 的同學應該已經發現了)。更多關於 AngularJS 組件化開發的 best practice,可以看官方的開發者文檔:Understanding Components
AngularJS 搭配 mobx
準備工作做了一堆,我們也該開始進入本文的正題,即如何給 AngularJS 搭載上 mobx 引擎(本文假設你對 mobx 中的基礎概念已經有一定程度的了解,如果不了解可以先移步mobx repomobx official doc):
1. mobx-angularjs
引入mobx-angularjs庫連接 mobx 和 angularjs 。
npmimobx-angularjs-S
2. 定義 ViewModel
在標準的 MVVM 架構里,ViewModel/Controller 除了構建視圖本身的狀態數據(即局部狀態)外,作為視圖跟業務模型之間溝通的橋樑,其主要職責是將業務模型適配(轉換/組裝)成對視圖更友好的數據模型。因此,在 mobx 視角下,ViewModel 主要由以下幾部分組成:
視圖(局部)狀態對應的 observable data
classViewModel{
@observable
isLoading=true;
@observable
isModelOpened=false;
}
可觀察數據(對應的 observer 為 view),即視圖需要對其變化自動做出響應的數據。在 mobx-angularjs 庫的協助下,通常 observable data 的變化會使關聯的視圖自動觸發 rerender(或觸發網路請求之類的副作用)。ViewModel 中的 observable data 通常是視圖狀態(UI-State),如 isLoading、isOpened 等。
由 應用/視圖 狀態衍生的 computed data
Computed values are values that can be derived from the existing state or other computed values.
classViewModel{
@computed
get userName(){
}
}
計算數據指的是由其他 observable/computed data 轉換而來,更方便視圖直接使用的衍生數據(derived data)。在重業務輕交互的 web 類應用中(通常是各種企業服務軟體), computed data 在 ViewModel中應該佔主要部分,且基本是由業務 store 中的數據(即應用狀態)轉換而來。computed 這種數據推導關係描述能確保我們的應用遵循 single source of truth 原則,不會出現數據不一致的情況,這也是 RP 編程中的基本原則之一。
action
ViewModel 中的 action 除了一小部分改變視圖狀態的行為外,大部分應該是直接調用 Model/Store 中的 action 來完成業務狀態的流轉。建議把所有對 observable data 的操作都放到被 aciton 裝飾的方法下進行。
mobx 配合下,一個相對完整的 ViewModel 大概長這樣:
import UserStorefrom"./UserStore";
classViewModel{
@inject(UserStore)
store;
@observable
isDropdownOpened=false;
@computed
get userName(){
return`${this.store.firstName}${this.store.lastName}`;
}
@action
toggel(){
this.isDropdownOpened= !isDropdownOpened;
}
updateFirstName(firstName){
this.store.updateFirstName(firstName);
}
}
3. 連接 AngularJS 和 mobx
increse
classViewModel{
@observablecount=;
@actionincrese(){
this.count++;
}
}
exportdefaultangular
.module("app",[])
.component("container",{
template,
controller:Controller,
})
.component("counter",{
template:"
{{$ctrl.count}}
"
bindings:{value:"
})
.name;
可以看到,除了常規的基於 mobx 的 ViewModel 定義外,我們只需要在模板的根節點加上指令,我們的 angularjs 組件就能很好的運作的 mobx 的響應式引擎下,從而自動的對 observable state 的變化執行 rerender。
mobx-angularjs 加速應用的魔法
從上文的示例代碼中我們可以看到,將 mobx 跟 angularjs 銜接運轉起來的是指令,我們翻下mobx-angularjs代碼:
constlink:angular.IDirectiveLinkFn=($scope)=>{
const{$$watchers=[]}=$scopeasany
constdebouncedDigest=debounce($scope.$digest.bind($scope),);
constdispose=reaction(
()=>[...$$watchers].map(watcher=>watcher.get($scope)),
()=> !$scope.$root.$$phase&&debouncedDigest()
)
$scope.$on("$destroy",dispose)
}
可以看到核心代碼其實就三行:
reaction(
()=>[...$$watchers].map(watcher=>watcher.get($scope)),
()=> !$scope.$root.$$phase&&debouncedDigest()
思路非常簡單,即在指令 link 之後,遍歷一遍當前 scope 上掛載的 watchers 並取值,由於這個動作是在 mobx reaction 執行上下文中進行的,因此 watcher 里依賴的所有 observable 都會被收集起來,這樣當下次其中任何一個 observable 發生變更時,都會觸發 reaction 的副作用對 scope 進行 digest,從而達到自動更新視圖的目的。
我們知道,angularjs 的性能被廣為詬病並不是因為 『臟檢查』 本身慢,而是因為 angularjs 在每次非同步事件發生時都是無腦的從根節點開始向下 digest,從而會導致一些不必要的 loop 造成的。而當我們在搭載上 mobx 的 push-based 的 change propagation 機制時,只有當被視圖真正使用的數據發生變化時,相關聯的視圖才會觸發局部 digest (可以理解為只有 observable data 存在 subscriber/observer 時,狀態變化才會觸發關聯依賴的重算,從而避免不必要資源消耗,即所謂的 lazy),區別於非同步事件觸發即無腦地, 這種方式顯然更高效。
進一步壓榨性能
我們知道 angularjs 是通過劫持各種非同步事件然後從根節點做 apply 的,這就導致只要我們用到了會被 angularjs 劫持的特性就會觸發 apply,其他的諸如都好說,我們有很多替代方案,但是這類事件監聽指令我們無法避免,就像上文例子中一樣,假如我們能杜絕潛藏的根節點 apply,想必應用的性能提升能更進一步。
思路很簡單,我們只要把之流替換成不觸發 apply 的版本即可。比如把原來的 ng event 實現這樣改一下:
forEach(
"click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),
function(eventName){
vardirectiveName=directiveNormalize("native-"+eventName);
ngEventDirectives[directiveName]=["$parse","$rootScope",function($parse,$rootScope){
return{
restrict:"A",
compile:function($element,attr){
varfn=$parse(attr[directiveName],/* interceptorFn */null,/* expensiveChecks */true);
returnfunctionngEventHandler(scope,element){
element.on(eventName,function(event){
fn(scope,{$event:event})
});
};
}
};
}];
}
);
時間監聽的回調中只是簡單觸發一下綁定的函數即可,不再 apply,bingo!
注意事項/ best practise
在 mobx 配合 angularjs 開發過程中,有一些點我們可能會 碰到/需要考慮:
避免 TTL
單向數據流優點很多,大部分場景下我們會優先使用 one-way binding 方式定義組件。通常你會寫出這樣的代碼:
classViewModel{
@computed
get unCompeletedTodos(){
returnthis.store.todos.filter(todo=> !todo.compeleted)
}
}
組件使用單向數據綁定定義:
angular
.module("xxx",[])
.component("todoPanel",{
template:"
{}
"
bindings:{todos:"
})
看上去沒有任何問題,但是當你把代碼扔到瀏覽器里時就會收穫一段 angularjs 饋贈的 TTL 錯誤:。實際上這並不是 mobx-angularjs 惹的禍,而是 angularjs 目前未實現 one-way binding 的 deep comparison 導致的,由於每次都會返回一個新的數組引用,而又是基於引用作對比,從而每次都是 false,最後自然報 TTL 錯誤了(具體可以看這裡One-way bindings + shallow watching)。
不過好在 mobx 優化手段中恰好有一個方法能間接的解決這個問題。我們只需要給 computed 加一個表示要做深度值對比的 modifier 即可:
@computed.struct
get unCompeletedTodos(){
returnthis.store.todos.filter(todo=> !todo.compeleted)
}
本質上還是對 unCompeletedTodos 的 memorization,只不過對比基準從默認的值對比(===)變成了結構/深度 對比,因而在第一次 get unCompeletedTodos 之後,只要計算出來的結果跟前次的結構一致(只有當 computed data 依賴的 observable 發生變化的時候才會觸發重算),後續的 getter 都會直接返回前面緩存的結果,從而不會觸發額外的 diff,進而避免了 TTL 錯誤的出現。
和觸發順序的問題
通常情況下我們希望在 ViewModel 中藉助組件的 lifecycle 鉤子做一些事情,比如在中觸發副作用(網路請求,事件綁定等),在里監聽傳入數據變化做視圖更新。
classViewModel{
$onInit(){
this.store.fetchUsers(this.id);
}
$onChanges(changesObj){
const{id}=changesObj;
if(id&& !id.isFirstChange()){
this.store.fetchUsers(id.currentValue)
}
}
}
可以發現其實我們在和中做了重複的事情,而且這種寫法也與我們要做視圖框架無關的數據層的初衷不符,藉助 mobx 的 observe 方法,我們可以將上面的代碼改造成這種:
import{ViewModel,postConstruct}from"mmlpx";
@ViewModel
classViewModel{
@observable
id=null;
@postConstruct
onInit(){
observe(this,"id",changedValue=>this.store.fetchUsers(changedValue))
}
}
熟悉 angularjs 的同學應該能發現,事實上 observe 做的事情跟是一樣的,但是為了保證數據層的 UI 框架無關性,我們這裡用 mobx 自己的觀察機制來替代了 angularjs 的 watch。
忘記你是在寫 AngularJS,把它當成一個簡單的動態模板引擎
不論是我們嘗試將 AngularJS 應用 ES6/TS 化還是引入 mobx 狀態管理庫,實際上我們的初衷都是將我們的 Model 甚至 ViewModel 層做成視圖框架無關,在藉助 mobx 管理數據的之間的依賴關係的同時,通過 connector 將 mobx observable data 與視圖連接起來,從而實現視圖依賴的狀態發生變化自動觸發視圖的更新。在這個過程中,angularjs 不再扮演一個框架的角色影響整個系統的架構,而僅僅是作為一個動態模板引擎提供 render 能力而已,後續我們完全可以通過配套的 connector,將 mobx 管理的數據層連接到不同的 view library 上。目前 mobx 官方針對 React/Angular/AngularJS 均有相應的 connector,社區也有針對 vue 的解決方案,並不需要我們從零開始。
在藉助 mobx 構建數據層之後,我們就能真正做到標準 MVVM 中描述的那樣,在 Model 甚至 VIewModel 不改一行代碼的前提下輕鬆適配其他視圖。view library 的語法、機制差異不再成為視圖層 升級/替換 的鴻溝,我們能通過改很少量的代碼來填平它,畢竟只是替換一個動態模板引擎而已。
Why MobX
React and MobX together are a powerful combination. React renders the application state by providing mechanisms to translate it into a tree of renderable components. MobX provides the mechanism to store and update the application state that React then uses.
Both React and MobX provide optimal and unique solutions to common problems in application development. React provides mechanisms to optimally render UI by using a virtual DOM that reduces the number of costly DOM mutations. MobX provides mechanisms to optimally synchronize application state with your React components by using a reactive virtual dependency state graph that is only updated when strictly needed and is never stale.
MobX 官方的介紹,把上面一段介紹中的 React 換成任意其他( Vue/Angular/AngularJS ) 視圖框架/庫(VDOM 部分適當調整一下) 也都適用。得益於 MobX 的概念簡單及獨立性,它非常適合作為視圖中立的狀態管理方案。簡言之是視圖層只做拿數據渲染的工作,狀態流轉由 MobX 幫你管理。
Why Not Redux
Redux 很好,而且社區也有很多跟除 React 之外的視圖層集成的實踐。單純的比較 Redux 跟 MobX 大概需要再寫一篇文章來闡述,這裡只簡單說幾點與視圖層集成時的差異:
雖然 Redux 本質也是一個觀察者模型,但是在 Redux 的實現下,狀態的變化並不是通過數據 diff 得出而是來手動通知的,而真正的 diff 則交給了視圖層,這不僅導致可能的渲染浪費(並不是所有 library 都有 vdom),在處理各種需要在變化時觸發副作用的場景也會顯得過於繁瑣。
由於第一條 Redux 不做數據 diff,因此我們無法在視圖層接手數據前得知哪個局部被更新,進而無法更高效的選擇性更新視圖。
Redux 在 store 的設計上是 opinionated 的,它奉行原則。應用可以完全由狀態數據來描述、且狀態可管理可回溯 這一點上我沒有意見,但並不是只有這一條出路,多 store 依然能達成這一目標。顯然 mobx 在這一點上是 unopinionated 且靈活性更強。
Redux 概念太多而自身做的又太少。可以對比一下ngRedux跟mobx-angularjs看看實現複雜度上的差異。
最後除了給 AngularJS 搭載上更高效、精確的高速引擎之外,我們最主要的目的還是為了將 業務模型層甚至 視圖模型層(統稱為應用數據層) 做成 UI 框架無關,這樣在面對不同的視圖層框架的遷移時,才可能做到遊刃有餘。而 mobx 在這個事情上是一個很好的選擇。
最後想說的是,如果條件允許的話,還是建議將 angularjs 系統升級成 React/Vue/Angular 之一,畢竟大部分時候基於新的視圖技術開發應用是能帶來確實的收益的,如 性能提升、開發效率提升 等。即便你短期內無法替換掉 angularjs(多種因素,比如已經基於 angularjs 開發/使用 了一套完整的組件庫,代碼體量太大改造成本過高),你依然可以在局部使用 mobx/mobx-angularjs 改造應用或開發新功能,在 mobx-angularjs 幫助你提升應用性能的同時,也給你後續的升級計劃創造了可能性。
PS: mobx-angularjs:https://github.com/mobxjs/mobx-angularjs
覺得本文對你有幫助?請分享給更多人
關注「前端大全」,提升前端技能
※主流瀏覽器圖片反防盜鏈方法總結
※Web 框架的架構模式探討
TAG:前端大全 |