原文连接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 小哥全力维护,若是有任何使用上的问题,欢迎随时联系😀。