老樹發新芽—使用 mobx 加速你的 AngularJS 應用,mobxangularjs

1月底的時候,Angular 官方博客發布了一則消息:

AngularJS is planning one more significant release, version 1.7, and on July 1, 2024 it will enter a 3 year Long Term Support period.

即在 7月1日 AngularJS 發布 1.7.0 版本之后,AngularJS 將進入一個為期 3 年的 LTS 時期。愛掏網 - it200.com也就是說 2024年7月1日 起至 2024年6月30日,AngularJS 不再合并任何會導致 breaking changes 的 features 或 bugfix,只做必要的問題修復。愛掏網 - it200.com詳細信息見這里:Stable AngularJS and Long Term Support

看到這則消息時我還是感觸頗多的,作為我的前端啟蒙框架,我從 AngularJS 上汲取到了非常多的養分。愛掏網 - it200.com雖然 AngularJS 作為一款優秀的前端 MVW 框架已經出色的完成了自己的歷史使命,但考慮到即便到了 2024 年,許多公司基于 AngularJS 的項目依然處于服役階段,結合我過去一年多在 mobx 上的探索和實踐,我決定給 AngularJS 強行再續一波命。愛掏網 - it200.com(搭車求治拖延癥良方,二月初起草的文章五月份才寫完,新聞都要過期了)

準備工作

在開始之前,我們需要給 AngularJS 搭配上一些現代化 webapp 開發套件,以便后面能更方便地裝載上 mobx 引擎。愛掏網 - it200.com

AngularJS 配合 ES6/next

現在是2024年,使用 ES6 開發應用已經成為事實標準(有可能的推薦直接上 TS )。愛掏網 - it200.com如何將 AngularJS 搭載上 ES6 這里不再贅述,可以看我之前的這篇文章:Angular1.x + ES6 開發風格指南

基于組件的應用架構

AngularJS 在 1.5.0 版本后新增了一系列激動人心的特性,如 onw-way bindings、component lifecycle hooks、component definition 等,基于這些特性,我們可以方便的將 AngularJS 系統打造成一個純組件化的應用(如果你對這些特性很熟悉可直接跳過至 [AngularJS 搭配 mobx](#AngularJS 搭配 mobx))。愛掏網 - it200.com我們一個個來看:

  • onw-way bindings 單向綁定 AngularJS 中使用 來定義組件的單向數據綁定,例如我們這樣定義一個組件:

    angular
     .module('app.components', [])
     .directive('component', () => ({
     restrict: 'E',
     template: '

    count: {{$ctrl.count}}

    '
    scope: { count: ' }, bindToController: true, controllerAs: '$ctrl', })

    使用時:

    {{app.count}}
    component count="app.count">component> 

    當我們點擊組件的 increase 按鈕時,可以看到組件內的 count 加 1 了,但是 app.count并不受影響。愛掏網 - it200.com

    區別于 AngularJS 賴以成名的雙向綁定特性 scope: { count: '='},單向數據綁定能更有效的隔離操作影響域,從而更方便的對數據變化溯源,降低 debug 難度。愛掏網 - it200.com 雙向綁定與單向綁定有各自的優勢與劣勢,這里不再討論,有興趣的可以看我這篇回答:單向數據綁定和雙向數據綁定的優缺點,適合什么場景?

  • 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這座‘‘橋梁’’。愛掏網 - it200.com但現在我們有了框架原生 lifecycle 的加持,這對于我們構建更純粹的、框架無關的 ViewModel 來講有很大幫助。愛掏網 - it200.com更多關于 lifecycle 的信息可以看官方文檔:AngularJS lifecycle hooks

  • component definition

    AngularJS 1.5.0 后增加了 component 語法用于更方便清晰的定義一個組件,如上述例子中的組件我們可以用component語法改寫成:

    angular
     .module('app.components', [])
     .component('component', {
     template: '

    count: {{$ctrl.count}}

    '
    bindings: { count: ', onUpdate: '&' }, })

    本質上component就是directive的語法糖,bindings 是 bindToController + controllerAs + scope 的語法糖,只不過component語法更簡單語義更明了,定義組件變得更方便,與社區流行的風格也更一致(熟悉 vue 的同學應該已經發現了)。愛掏網 - it200.com更多關于 AngularJS 組件化開發的 best practice,可以看官方的開發者文檔:Understanding Components

  • AngularJS 搭配 mobx

準備工作做了一堆,我們也該開始進入本文的正題,即如何給 AngularJS 搭載上 mobx 引擎(本文假設你對 mobx 中的基礎概念已經有一定程度的了解,如果不了解可以先移步 mobx repo mobx official doc):

1. mobx-angularjs

引入 mobx-angularjs 庫連接 mobx 和 angularjs 。愛掏網 - it200.com

npm i mobx-angularjs -S

2. 定義 ViewModel

在標準的 MVVM 架構里,ViewModel/Controller 除了構建視圖本身的狀態數據(即局部狀態)外,作為視圖跟業務模型之間溝通的橋梁,其主要職責是將業務模型適配(轉換/組裝)成對視圖更友好的數據模型。愛掏網 - it200.com因此,在 mobx 視角下,ViewModel 主要由以下幾部分組成:

  • 視圖(局部)狀態對應的 observable data

    class ViewModel {
     @observable
     isLoading = true;
    
    	@observable
    	isModelOpened = false;
    }
    

    可觀察數據(對應的 observer 為 view),即視圖需要對其變化自動做出響應的數據。愛掏網 - it200.com在 mobx-angularjs 庫的協助下,通常 observable data 的變化會使關聯的視圖自動觸發 rerender(或觸發網絡請求之類的副作用)。愛掏網 - it200.comViewModel 中的 observable data 通常是視圖狀態(UI-State),如 isLoading、isOpened 等。愛掏網 - it200.com

  • 由 應用/視圖 狀態衍生的 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)。愛掏網 - it200.com 在重業務輕交互的 web 類應用中(通常是各種企業服務軟件), computed data 在 ViewModel 中應該占主要部分,且基本是由業務 store 中的數據(即應用狀態)轉換而來。愛掏網 - it200.com computed 這種數據推導關系描述能確保我們的應用遵循 single source of truth 原則,不會出現數據不一致的情況,這也是 RP 編程中的基本原則之一。愛掏網 - it200.com

  • action ViewModel 中的 action 除了一小部分改變視圖狀態的行為外,大部分應該是直接調用 Model/Store 中的 action 來完成業務狀態的流轉。愛掏網 - it200.com建議把所有對 observable data 的操作都放到被 aciton 裝飾的方法下進行。愛掏網 - it200.com

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);
 }
}

3. 連接 AngularJS 和 mobx

section mobx-autorun> counter value="$ctrl.count">counter> button type="button" ng-click="$ctrl.increse()">incresebutton> 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: '
{{$ctrl.count}}
'
bindings: { value: ' } }) .name;

可以看到,除了常規的基于 mobx 的 ViewModel 定義外,我們只需要在模板的根節點加上 mobx-autorun 指令,我們的 angularjs 組件就能很好的運作的 mobx 的響應式引擎下,從而自動的對 observable state 的變化執行 rerender。愛掏網 - it200.com

mobx-angularjs 加速應用的魔法

從上文的示例代碼中我們可以看到,將 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,從而達到自動更新視圖的目的。愛掏網 - it200.com

我們知道,angularjs 的性能被廣為詬病并不是因為 ‘臟檢查’ 本身慢,而是因為 angularjs 在每次異步事件發生時都是無腦的從根節點開始向下 digest,從而會導致一些不必要的 loop 造成的。愛掏網 - it200.com而當我們在搭載上 mobx 的 push-based 的 change propagation 機制時,只有當被視圖真正使用的數據發生變化時,相關聯的視圖才會觸發局部 digest (可以理解為只有 observable data 存在 subscriber/observer 時,狀態變化才會觸發關聯依賴的重算,從而避免不必要資源消耗,即所謂的 lazy),區別于異步事件觸發即無腦地 $rootScope.$apply, 這種方式顯然更高效。愛掏網 - it200.com

進一步壓榨性能

我們知道 angularjs 是通過劫持各種異步事件然后從根節點做 apply 的,這就導致只要我們用到了會被 angularjs 劫持的特性就會觸發 apply,其他的諸如 $http $timeout 都好說,我們有很多替代方案,但是 ng-click 這類事件監聽指令我們無法避免,就像上文例子中一樣,假如我們能杜絕潛藏的根節點 apply,想必應用的性能提升能更進一步。愛掏網 - it200.com

思路很簡單,我們只要把 ng-click 之流替換成不觸發 apply 的版本即可。愛掏網 - it200.com比如把原來的 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!

注意事項/ best practise

在 mobx 配合 angularjs 開發過程中,有一些點我們可能會 碰到/需要考慮:

  • 避免 TTL 單向數據流優點很多,大部分場景下我們會優先使用 one-way binding 方式定義組件。愛掏網 - it200.com通常你會寫出這樣的代碼:

    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: '
    • {{todo.content}}
    '
    bindings: { todos: ' } })

    看上去沒有任何問題,但是當你把代碼扔到瀏覽器里時就會收獲一段 angularjs 饋贈的 TTL 錯誤:Error: $rootScope:infdigInfinite $digest Loop愛掏網 - it200.com實際上這并不是 mobx-angularjs 惹的禍,而是 angularjs 目前未實現 one-way binding 的 deep comparison 導致的,由于每次 get unCompeletedTodos 都會返回一個新的數組引用,而又是基于引用作對比,從而每次 prev === current 都是 false,最后自然報 TTL 錯誤了(具體可以看這里 One-way bindings + shallow watching )。愛掏網 - it200.com

    不過好在 mobx 優化手段中恰好有一個方法能間接的解決這個問題。愛掏網 - it200.com我們只需要給 computed 加一個表示要做深度值對比的 modifier 即可:

    @computed.struct
    get unCompeletedTodos() {
     return this.store.todos.filter(todo => !todo.compeleted)
    }
    

    本質上還是對 unCompeletedTodos 的 memorization,只不過對比基準從默認的值對比(===)變成了結構/深度 對比,因而在第一次 get unCompeletedTodos 之后,只要計算出來的結果跟前次的結構一致(只有當 computed data 依賴的 observable 發生變化的時候才會觸發重算),后續的 getter 都會直接返回前面緩存的結果,從而不會觸發額外的 diff,進而避免了 TTL 錯誤的出現。愛掏網 - it200.com

  • $onInit$onChanges 觸發順序的問題 通常情況下我們希望在 ViewModel 中借助組件的 lifecycle 鉤子做一些事情,比如在 $onInit 中觸發副作用(網絡請求,事件綁定等),在 $onChanges 里監聽傳入數據變化做視圖更新。愛掏網 - it200.com

    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。愛掏網 - it200.com

  • 忘記你是在寫 AngularJS,把它當成一個簡單的動態模板引擎

    不論是我們嘗試將 AngularJS 應用 ES6/TS 化還是引入 mobx 狀態管理庫,實際上我們的初衷都是將我們的 Model 甚至 ViewModel 層做成視圖框架無關,在借助 mobx 管理數據的之間的依賴關系的同時,通過 connector 將 mobx observable data 與視圖連接起來,從而實現視圖依賴的狀態發生變化自動觸發視圖的更新。愛掏網 - it200.com在這個過程中,angularjs 不再扮演一個框架的角色影響整個系統的架構,而僅僅是作為一個動態模板引擎提供 render 能力而已,后續我們完全可以通過配套的 connector,將 mobx 管理的數據層連接到不同的 view library 上。愛掏網 - it200.com目前 mobx 官方針對 React/Angular/AngularJS 均有相應的 connector,社區也有針對 vue 的解決方案,并不需要我們從零開始。愛掏網 - it200.com

    在借助 mobx 構建數據層之后,我們就能真正做到標準 MVVM 中描述的那樣,在 Model 甚至 VIewModel 不改一行代碼的前提下輕松適配其他視圖。愛掏網 - it200.comview library 的語法、機制差異不再成為視圖層 升級/替換 的鴻溝,我們能通過改很少量的代碼來填平它,畢竟只是替換一個動態模板引擎而已。愛掏網 - it200.com

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 部分適當調整一下) 也都適用。愛掏網 - it200.com得益于 MobX 的概念簡單及獨立性,它非常適合作為視圖中立的狀態管理方案。愛掏網 - it200.com簡言之是視圖層只做拿數據渲染的工作,狀態流轉由 MobX 幫你管理。愛掏網 - it200.com

Why Not Redux

Redux 很好,而且社區也有很多跟除 React 之外的視圖層集成的實踐。愛掏網 - it200.com單純的比較 Redux 跟 MobX 大概需要再寫一篇文章來闡述,這里只簡單說幾點與視圖層集成時的差異:

  1. 雖然 Redux 本質也是一個觀察者模型,但是在 Redux 的實現下,狀態的變化并不是通過數據 diff 得出而是 dispatch(action) 來手動通知的,而真正的 diff 則交給了視圖層,這不僅導致可能的渲染浪費(并不是所有 library 都有 vdom),在處理各種需要在變化時觸發副作用的場景也會顯得過于繁瑣。愛掏網 - it200.com
  2. 由于第一條 Redux 不做數據 diff,因此我們無法在視圖層接手數據前得知哪個局部被更新,進而無法更高效的選擇性更新視圖。愛掏網 - it200.com
  3. Redux 在 store 的設計上是 opinionated 的,它奉行 單一 store 原則。愛掏網 - it200.com應用可以完全由狀態數據來描述、且狀態可管理可回溯 這一點上我沒有意見,但并不是只有單一 store這一條出路,多 store 依然能達成這一目標。愛掏網 - it200.com顯然 mobx 在這一點上是 unopinionated 且靈活性更強。愛掏網 - it200.com
  4. Redux 概念太多而自身做的又太少。愛掏網 - it200.com可以對比一下 ngRedux 跟 mobx-angularjs 看看實現復雜度上的差異。愛掏網 - it200.com

最后

除了給 AngularJS 搭載上更高效、精確的高速引擎之外,我們最主要的目的還是為了將 業務模型層甚至 視圖模型層(統稱為應用數據層) 做成 UI 框架無關,這樣在面對不同的視圖層框架的遷移時,才可能做到游刃有余。愛掏網 - it200.com而 mobx 在這個事情上是一個很好的選擇。愛掏網 - it200.com

最后想說的是,如果條件允許的話,還是建議將 angularjs 系統升級成 React/Vue/Angular 之一,畢竟大部分時候基于新的視圖技術開發應用是能帶來確實的收益的,如 性能提升、開發效率提升 等。愛掏網 - it200.com即便你短期內無法替換掉 angularjs(多種因素,比如已經基于 angularjs 開發/使用 了一套完整的組件庫,代碼體量太大改造成本過高),你依然可以在局部使用 mobx/mobx-angularjs 改造應用或開發新功能,在 mobx-angularjs 幫助你提升應用性能的同時,也給你后續的升級計劃創造了可能性。愛掏網 - it200.com


原文發布時間:05/09

原文作者:kuitos

本文來源開源中國如需轉載請緊急聯系作者


聲明:所有內容來自互聯網搜索結果,不保證100%準確性,僅供參考。如若本站內容侵犯了原著者的合法權益,可聯系我們進行處理。
發表評論
更多 網友評論0 條評論)
暫無評論

返回頂部

主站蜘蛛池模板: youjizz护士| 欧美日韩视频在线| 中文字幕日韩欧美一区二区三区| 国产综合久久久久久鬼色| 第四色播日韩第一页| 一进一出动态图| 卡一卡2卡3卡精品网站| 日本人与黑人xxxxx18| 青草视频网站在线观看| 久久国产视频网| 国产乱子经典视频在线观看| 日韩中文字幕在线一区二区三区| 国产福利拍拍拍| 久久精品国产99国产精品| 国产日本在线视频| 最新精品亚洲成a人在线观看| 天天影视综合网| 久久成人国产精品免费软件| 国产亚洲婷婷香蕉久久精品| 日本一道dvd在线播放| 美女和男生一起差差差| 一男一女的一级毛片| 亚洲精品无码久久久久久| 国产精品成人va在线观看| 最新国产在线视频| 美女主动张腿让男人桶| a级毛片100部免费观看| 国产高清精品入口91| 欧美18videos极品massage| 高中生的放荡日记h| 中文字幕精品一区二区2021年| 免费少妇荡乳情欲视频| 国产精品无码久久综合| 日产精品久久久久久久| 狠狠色噜噜狠狠狠狠97不卡| 色先锋影音资源| 一区二区免费电影| 亚洲av无码第一区二区三区| 国产一区二区精品久久91| 大香伊蕉在人线国产75视频| 欧洲精品一卡2卡三卡4卡乱码|