原文連接html
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.vue
即在 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 Supportreact
看到這則消息時我仍是感觸頗多的,做爲個人前端啓蒙框架,我從 AngularJS 上汲取到了很是多的營養。雖然 AngularJS 做爲一款優秀的前端 MVW 框架已經出色的完成了本身的歷史使命,但考慮到即使到了 2018 年,許多公司基於 AngularJS 的項目依然處於服役階段,結合我過去一年多在 mobx 上的探索和實踐,我決定給 AngularJS 強行再續一波命🙃。(搭車求治拖延症良方,二月初起草的文章五月份才寫完,新聞都要過時了😑)git
在開始以前,咱們須要給 AngularJS 搭配上一些現代化 webapp 開發套件,以便後面能更方便地裝載上 mobx 引擎。angularjs
如今是2018年,使用 ES6 開發應用已經成爲事實標準(有可能的推薦直接上 TS )。如何將 AngularJS 搭載上 ES6 這裏再也不贅述,能夠看我以前的這篇文章:Angular1.x + ES6 開發風格指南github
AngularJS 在 1.5.0 版本後新增了一系列激動人心的特性,如 onw-way bindings、component lifecycle hooks、component definition 等,基於這些特性,咱們能夠方便的將 AngularJS 系統打形成一個純組件化的應用(若是你對這些特性很熟悉可直接跳過至 [AngularJS 搭配 mobx](#AngularJS 搭配 mobx))。咱們一個個來看:web
onw-way bindings 單向綁定 AngularJS 中使用 <
來定義組件的單向數據綁定,例如咱們這樣定義一個組件:npm
angular
.module('app.components', [])
.directive('component', () => ({
restrict: 'E',
template: '<p>count: {{$ctrl.count}}</p><button ng-click="$ctrl.count = $ctrl.count + 1">increase</button>'
scope: {
count: '<'
},
bindToController: true,
controllerAs: '$ctrl',
})
複製代碼
使用時:編程
{{app.count}}
<component count="app.count"></component>
複製代碼
當咱們點擊組件的 increase 按鈕時,能夠看到組件內的 count 加 1 了,可是 app.count
並不受影響。
區別於 AngularJS 賴以成名的雙向綁定特性 scope: { count: '='}
,單向數據綁定能更有效的隔離操做影響域,從而更方便的對數據變化溯源,下降 debug 難度。 雙向綁定與單向綁定有各自的優點與劣勢,這裏再也不討論,有興趣的能夠看我這篇回答:單向數據綁定和雙向數據綁定的優缺點,適合什麼場景?
component lifecycle hooks 組件生命週期鉤子
1.5.3 開始新增了幾個組件的生命週期鉤子(目的是爲更方便的向 Angular2+ 遷移),分別是 $onInit
$onChanges
$onDestroy
$postLink
$doCheck
(1.5.8增長),寫起來大概長這樣:
class Controller {
$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 以前,咱們也能借助一些機制來模擬組件的生命週期(如 $scope.$watch
、$scope.$on('$destroy')
等),但基本上都須要藉助$scope
這座‘‘橋樑’’。但如今咱們有了框架原生 lifecycle 的加持,這對於咱們構建更純粹的、框架無關的 ViewModel 來說有很大幫助。更多關於 lifecycle 的信息能夠看官方文檔:AngularJS lifecycle hooks
component definition
AngularJS 1.5.0 後增長了 component
語法用於更方便清晰的定義一個組件,如上述例子中的組件咱們能夠用component
語法改寫成:
angular
.module('app.components', [])
.component('component', {
template: '<p>count: {{$ctrl.count}}</p><button ng-click="$ctrl.onUpdate({count: $ctrl.count + 1})">increase</button>'
bindings: {
count: '<',
onUpdate: '&'
},
})
複製代碼
本質上component
就是directive
的語法糖,bindings 是 bindToController + controllerAs + scope
的語法糖,只不過component
語法更簡單語義更明瞭,定義組件變得更方便,與社區流行的風格也更一致(熟悉 vue 的同窗應該已經發現了😆)。更多關於 AngularJS 組件化開發的 best practice,能夠看官方的開發者文檔:Understanding Components
準備工做作了一堆,咱們也該開始進入本文的正題,即如何給 AngularJS 搭載上 mobx 引擎(本文假設你對 mobx 中的基礎概念已經有必定程度的瞭解,若是不瞭解能夠先移步 mobx repo mobx official doc):
引入 mobx-angularjs 庫鏈接 mobx 和 angularjs 。
npm i mobx-angularjs -S
複製代碼
在標準的 MVVM 架構裏,ViewModel/Controller 除了構建視圖自己的狀態數據(即局部狀態)外,做爲視圖跟業務模型之間溝通的橋樑,其主要職責是將業務模型適配(轉換/組裝)成對視圖更友好的數據模型。所以,在 mobx 視角下,ViewModel 主要由如下幾部分組成:
視圖(局部)狀態對應的 observable data
class ViewModel {
@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.
class ViewModel {
@computed
get userName() {
return `${this.user.firstName} ${this.user.lastName}`;
}
}
複製代碼
計算數據指的是由其餘 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 UserStore from './UserStore';
class ViewModel {
@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);
}
}
複製代碼
<section mobx-autorun>
<counter value="$ctrl.count"></counter>
<button type="button" ng-click="$ctrl.increse()">increse</button>
</section>
複製代碼
import template from './index.tpl.html';
class ViewModel {
@observable count = 0;
@action increse() {
this.count++;
}
}
export default angular
.module('app', [])
.component('container', {
template,
controller: Controller,
})
.component('counter', {
template: '<section><header>{{$ctrl.count}}</header></section>'
bindings: { value: '<' }
})
.name;
複製代碼
能夠看到,除了常規的基於 mobx 的 ViewModel 定義外,咱們只須要在模板的根節點加上 mobx-autorun
指令,咱們的 angularjs 組件就能很好的運做的 mobx 的響應式引擎下,從而自動的對 observable state 的變化執行 rerender。
從上文的示例代碼中咱們能夠看到,將 mobx 跟 angularjs 銜接運轉起來的是 mobx-autorun
指令,咱們翻下 mobx-angularjs 代碼:
const link: angular.IDirectiveLinkFn = ($scope) => {
const { $$watchers = [] } = $scope as any
const debouncedDigest = debounce($scope.$digest.bind($scope), 0);
const dispose = 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),區別於異步事件觸發即無腦地 $rootScope.$apply
, 這種方式顯然更高效。
咱們知道 angularjs 是經過劫持各類異步事件而後從根節點作 apply 的,這就致使只要咱們用到了會被 angularjs 劫持的特性就會觸發 apply,其餘的諸如 $http
$timeout
都好說,咱們有不少替代方案,可是 ng-click
這類事件監聽指令咱們沒法避免,就像上文例子中同樣,假如咱們能杜絕潛藏的根節點 apply,想必應用的性能提高能更進一步。
思路很簡單,咱們只要把 ng-click
之流替換成不觸發 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) {
var directiveName = directiveNormalize('native-' + eventName);
ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) {
return {
restrict: 'A',
compile: function($element, attr) {
var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true);
return function ngEventHandler(scope, element) {
element.on(eventName, function(event) {
fn(scope, {$event:event})
});
};
}
};
}];
}
);
複製代碼
時間監聽的回調中只是簡單觸發一下綁定的函數便可,再也不 apply,bingo!
在 mobx 配合 angularjs 開發過程當中,有一些點咱們可能會 碰到/須要考慮:
避免 TTL 單向數據流優勢不少,大部分場景下咱們會優先使用 one-way binding 方式定義組件。一般你會寫出這樣的代碼:
class ViewModel {
@computed
get unCompeletedTodos() {
return this.store.todos.filter(todo => !todo.compeleted)
}
}
複製代碼
<section mobx-autorun>
<todo-panel todos="$ctrl.unCompeletedTodos"></todo-panel>
</section>
複製代碼
todo-panel
組件使用單向數據綁定定義:
angular
.module('xxx', [])
.component('todoPanel', {
template: '<ul><li ng-repeat="todo in $ctrl.todos track by todo.id">{{todo.content}}</li></ul>'
bindings: { todos: '<' }
})
複製代碼
看上去沒有任何問題,可是當你把代碼扔到瀏覽器裏時就會收穫一段 angularjs 饋贈的 TTL 錯誤:Error: $rootScope:infdigInfinite $digest Loop
。實際上這並非 mobx-angularjs 惹的禍,而是 angularjs 目前未實現 one-way binding 的 deep comparison 致使的,因爲每次 get unCompeletedTodos
都會返回一個新的數組引用,而<
又是基於引用做對比,從而每次 prev === current
都是 false,最後天然報 TTL 錯誤了(具體能夠看這裏 One-way bindings + shallow watching )。
不過好在 mobx 優化手段中剛好有一個方法能間接的解決這個問題。咱們只須要給 computed 加一個表示要作深度值對比的 modifier 便可:
@computed.struct
get unCompeletedTodos() {
return this.store.todos.filter(todo => !todo.compeleted)
}
複製代碼
本質上仍是對 unCompeletedTodos 的 memorization,只不過對比基準從默認的值對比(===)變成告終構/深度 對比,於是在第一次 get unCompeletedTodos 以後,只要計算出來的結果跟前次的結構一致(只有當 computed data 依賴的 observable 發生變化的時候纔會觸發重算),後續的 getter 都會直接返回前面緩存的結果,從而不會觸發額外的 diff,進而避免了 TTL 錯誤的出現。
$onInit
和 $onChanges
觸發順序的問題 一般狀況下咱們但願在 ViewModel 中藉助組件的 lifecycle 鉤子作一些事情,好比在 $onInit
中觸發反作用(網絡請求,事件綁定等),在 $onChanges
裏監聽傳入數據變化作視圖更新。
class ViewModel {
$onInit() {
this.store.fetchUsers(this.id);
}
$onChanges(changesObj) {
const { id } = changesObj;
if(id && !id.isFirstChange()) {
this.store.fetchUsers(id.currentValue)
}
}
}
複製代碼
能夠發現其實咱們在 $onInit
和 $onChanges
中作了重複的事情,並且這種寫法也與咱們要作視圖框架無關的數據層的初衷不符,藉助 mobx 的 observe 方法,咱們能夠將上面的代碼改形成這種:
import { ViewModel, postConstruct } from 'mmlpx';
@ViewModel
class ViewModel {
@observable
id = null;
@postConstruct
onInit() {
observe(this, 'id', changedValue => this.store.fetchUsers(changedValue))
}
}
複製代碼
熟悉 angularjs 的同窗應該能發現,事實上 observe 作的事情跟 $scope.$watch
是同樣的,可是爲了保證數據層的 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 的語法、機制差別再也不成爲視圖層 升級/替換 的鴻溝,咱們能經過改不多量的代碼來填平它,畢竟只是替換一個動態模板引擎而已😆。
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 幫你管理。
Redux 很好,並且社區也有不少跟除 React 以外的視圖層集成的實踐。單純的比較 Redux 跟 MobX 大概須要再寫一篇文章來闡述,這裏只簡單說幾點與視圖層集成時的差別:
dispatch(action)
來手動通知的,而真正的 diff 則交給了視圖層,這不只致使可能的渲染浪費(並非全部 library 都有 vdom),在處理各類須要在變化時觸發反作用的場景也會顯得過於繁瑣。單一 store
原則。應用能夠徹底由狀態數據來描述、且狀態可管理可回溯 這一點上我沒有意見,但並非只有單一 store
這一條出路,多 store 依然能達成這一目標。顯然 mobx 在這一點上是 unopinionated 且靈活性更強。除了給 AngularJS 搭載上更高效、精確的高速引擎以外,咱們最主要的目的仍是爲了將 業務模型層甚至 視圖模型層(統稱爲應用數據層) 作成 UI 框架無關,這樣在面對不一樣的視圖層框架的遷移時,纔可能作到遊刃有餘。而 mobx 在這個事情上是一個很好的選擇。
最後想說的是,若是條件容許的話,仍是建議將 angularjs 系統升級成 React/Vue/Angular 之一,畢竟大部分時候基於新的視圖技術開發應用是能帶來確實的收益的,如 性能提高、開發效率提高 等。即使你短時間內沒法替換掉 angularjs(多種因素,好比已經基於 angularjs 開發/使用 了一套完整的組件庫,代碼體量太大改形成本太高),你依然能夠在局部使用 mobx/mobx-angularjs 改造應用或開發新功能,在 mobx-angularjs 幫助你提高應用性能的同時,也給你後續的升級計劃創造了可能性。
PS: mobx-angularjs 目前由我和另外一個 US 小哥全力維護,若是有任何使用上的問題,歡迎隨時聯繫😀。