咱们是袋鼠云数栈 UED 团队,致力于打造优良的一站式数据中台产品。咱们始终保持工匠精力,摸索前端路线,为社区积攒并流传教训价值。
本文作者:霜序(掘金)
前言
在之前的文章中,咱们讲述了 React 的数据流治理,从 props → context → Redux,以及 Redux 相干的三方库 React-Redux。
那其实说到 React 的状态管理器,除了 Redux 之外,Mobx 也是利用较多的治理计划。Mobx 是一个响应式库,在某种程度上能够看作没有模版的 Vue,两者的原理差不多
先看一下 Mobx 的简略应用,线上示例
export class TodoList { @observable todos = []; @computed get getUndoCount() { return this.todos.filter((todo) => !todo.done).length; } @action add(task) { this.todos.push({ task, done: false }); } @action delete(index) { this.todos.splice(index, 1); }}
Mobx 借助于装璜器来实现,是的代码更加简洁。应用了可察看对象,Mobx 能够间接批改状态,不必像 Redux 那样写 actions/reducers。Redux 是遵循 setState 的流程,MobX就是干掉了 setState 的机制
通过响应式编程使得状态治理变得简略和可扩大。MobX v5 版本利用 ES6 的proxy
来追踪属性,以前的旧版本通过Object.defineProperty
实现的。通过隐式订阅,主动追踪被监听的对象变动
Mobx 的执行流程,一张官网联合上述例子的图
MobX将利用变为响应式可演绎为上面三个步骤
定义状态并使其可察看
应用
observable
对存储的数据结构成为可察看状态创立视图以响应状态的变动
应用
observer
来监听视图,如果用到的数据产生扭转视图会自动更新更改状态
应用
action
来定义批改状态的办法
Mobx外围概念
observable
给数据对象增加可察看的性能,反对任何的数据结构
const todos = observable([{ task: "Learn Mobx", done: false}])// 更多的采纳装璜器的写法class Store { @observable todos = [{ task: "Learn Mobx", done: false }]}
computed
在 Redux 中,咱们须要计算曾经 completeTodo 和 unCompleteTodo,咱们能够采纳:在 mapStateToProps 中,通过 allTodos 过滤出对应的值,线上示例
const mapStateToProps = (state) => { const { visibilityFilter } = state; const todos = getTodosByVisibilityFilter(state, visibilityFilter); return { todos };};
在 Mobx 中能够定义相干数据发生变化时自动更新的值,通过@computed
调用getter
/setter
函数进行变更
一旦 todos 的产生扭转,getUndoCount 就会主动计算
export class TodoList { @observable todos = []; @computed get getUndo() { return this.todos.filter((todo) => !todo.done) } @computed get getCompleteTodo() { return this.todos.filter((todo) => todo.done) }}
action
动作是任何用来批改状态的货色。MobX 中的 action 不像 redux 中是必须的,把一些批改 state 的操作都标准应用 action 做标注。
在 MobX 中能够随便更改todos.push({ title:'coding', done: false })
,state 也是能够有作用的,然而这样横七竖八不好定位是哪里触发了 state 的变动,倡议在任何更新observable
或者有副作用的函数上应用 actions。
在严格模式useStrict(true)
下,强制应用 action
// 非action应用<button onClick={() => todoList.todos.push({ task: this.inputRef.value, done: false })}> Add New Todo</button>// action应用<button onClick={() => todoList.add(this.inputRef.value)}> Add New Todo</button>class TodoList { @action add(task) { this.todos.push({ task, done: false }); }}
Reactions
计算值 computed 是主动响应状态变动的值。反馈是主动响应状态变动是的副作用,反馈能够确保相干状态变动时指定的副作用执行。
autorun
autorun
负责运行所提供的sideEffect
并追踪在sideEffect
运行期间拜访过的observable
的状态承受一个函数
sideEffect
,当这个函数中依赖的可察看属性发生变化的时候,autorun
外面的函数就会被触发。除此之外,autorun
外面的函数在第一次会立刻执行一次。autorun(() => { console.log("Current name : " + this.props.myName.name);});// 追踪函数外的间接援用不会失效const name = this.props.myName.name;autorun(() => { console.log("Current name : " + name);});
reaction
reaction
是autorun
的变种,在如何追踪observable
方面给予了更细粒度的管制。 它接管两个函数,第一个是追踪并返回数据,该数据用作第二个函数,也就是副作用的输出。autorun 会立刻执行一次,然而 reaction 不会
reaction( () => this.props.todoList.getUndoCount, (data) => { console.log("Current count : ", data); });
observer
应用 Redux 时,咱们会引入 React-Redux 的 connect 函数,使得咱们的组件可能通过 props 获取到 store 中的数据
在 Mobx 中也是一样的情理,咱们须要引入 observer 将组件变为响应式组件
包裹 React 组件的高阶组件,在组件的 render 函数中任何应用的observable
发生变化时,组件都会调用 render 从新渲染,更新 UI
⚠️ 不要放在顶层 Page,如果一个 state 扭转,整个 Page 都会 render,所以 observer 尽量取包裹小组件,组件越小从新渲染的变动就越小
@observerexport default class TodoListView extends Component { render() { const { todoList } = this.props; return ( <div className="todoView"> <div className="todoView__list"> {todoList.todos.map((todo, index) => ( <TodoItem key={index} todo={todo} onDelete={() => todoList.delete(index)} /> ))} </div> </div> ); }}
Mobx原理实现
前文中提到 Mobx 实现响应式数据,采纳了Object.defineProperty
或者Proxy
下面讲述到应用 autorun 会在第一次执行并且依赖的属性变动时也会执行。
const user = observable({ name: "FBB", age: 24 })autorun(() => { console.log(user.name)})
当咱们应用 observable 创立了一个可察看对象user
,autorun 就会去监听user.name
是否产生了扭转。等于user.name
被 autorun 监控了,一旦有任何变动就要去告诉它
user.name.watchers.push(watch)// 一旦user的数据产生了扭转就要去告诉观察者user.name.watchers.forEach(watch => watch())
observable
装璜器个别承受三个参数: 指标对象、属性、属性描述符
通过下面的剖析,通过 observable 创立的对象都是可察看的,也就是创建对象的每个属性都须要被察看
每一个被察看对象都须要有本人的订阅办法数组
const counter = observable({ count: 0 })const user = observable({ name: "FBB", age: 20 })autorun(function func1() { console.log(`${user.name} and ${counter.count}`)})autorun(function func2() { console.log(user.name)})
对于上述代码来说,counter.count 的 watchers 只有 func1,user.name 的 watchers 则有 func1/func2
实现一下观察者类 Watcher,借助 shortid 来辨别不同的观察者实例
class Watcher { id: string value: any; constructor(v: any, property: string) { this.id = `ob_${property}_${shortid()}`; this.value = v; } // 调用get时,收集所有观察者 collect() { dependenceManager.collect(this.id); return this.value; } // 调用set时,告诉所有观察者 notify(v: any) { this.value = v; dependenceManager.notify(this.id); }}
实现一个简略的装璜器,须要拦挡咱们属性的 get/set 办法,并且应用 Object.defineProperty 进行深度拦挡
export function observable(target: any, name: any, descriptor: { initializer: () => any; }) { const v = descriptor.initializer(); createDeepWatcher(v) const watcher = new Watcher(v, name); return { enumerable: true, configurable: true, get: function () { return watcher.collect(); }, set: function (v: any) { return watcher.notify(v); } };};function createDeepWatcher(target: any) { if (typeof target === "object") { for (let property in target) { if (target.hasOwnProperty(property)) { const watcher = new Watcher(target[property], property); Object.defineProperty(target, property, { get() { return watcher.collect(); }, set(value) { return watcher.notify(value); } }); createDeepWatcher(target[property]) } } }}
在下面 Watcher 类中的get/set
中调用了 dependenceManager 的办法还未写完。在调用属性的get
办法时,会将函数收集到以后 id 的 watchers 中,调用属性的set
办法则是去告诉所有的 watchers,触发对应收集函数
那这这里其实咱们还须要借助一个类,也就是依赖收集类DependenceManager
,马上就会实现
autorun
后面说到 autorun 会立刻执行一次,并且会将函数收集起来,存储到对应的observable.id
的 watchers 中。autorun 实现了收集依赖,执行对应函数。再执行对应函数的时候,会调用到对应observable
对象的get
办法,来收集依赖
export default function autorun(handler) { dependenceManager.beginCollect(handler) handler() dependenceManager.endCollect()}
实现DependenceManager
类:
- beginCollect: 标识开始收集依赖,将依赖函数存到一个类全局变量中
- collect(id): 调用
get
办法时,将依赖函数放到存入到对应 id 的依赖数组中 - notify: 当执行
set
的时候,依据 id 来执行数组中的函数依赖 - endCollect: 革除刚开始的函数依赖,以便于下一次收集
class DependenceManager { _store: any = {} static Dep: any; beginCollect(handler: () => void) { DependenceManager.Dep = handler } collect(id: string) { if (DependenceManager.Dep) { this._store[id] = this._store[id] || {} this._store[id].watchers = this._store[id].watchers || [] if (!this._store[id].watchers.includes(DependenceManager.Dep)) this._store[id].watchers.push(DependenceManager.Dep); } } notify(id: string) { const store = this._store[id]; if (store && store.watchers) { store.watchers.forEach((watch: () => void) => { watch.call(this); }) } } endCollect() { DependenceManager.Dep = null }}
一个简略的 Mobx 框架都搭建好了~
computed
computed 的三个特点:
- computed 办法是一个 get 办法
- computed 会依据依赖的属性从新计算值
- 依赖 computed 的函数也会被从新执行
发现 computed 的实现大抵和 observable 类似,从以上特点能够推断出 computed 须要两次收集依赖,一次是收集 computed 所依赖的属性,一次是依赖 computed 的函数
首先定义一个 computed 办法,是一个装璜器
export function computed(target: any, name: any, descriptor: any) { const getter = descriptor.get; // get 函数 const _computed = new ComputedWatcher(target, getter); return { enumerable: true, configurable: true, get: function () { _computed.target = this return _computed.get(); } };}
实现 ComputedWatcher 类,和 Watcher 类差不多。在执行 get 办法的时候,咱们和之前一样,去收集一下依赖 computed 的函数,丰盛 get 办法
class ComputedWatcher { // 标识是否绑定过recomputed依赖,只须要绑定一次 hasBindAutoReCompute: boolean | undefined; value: any; // 绑定recompute 和 外部波及到的察看值的关系 _bindAutoReCompute() { if (!this.hasBindAutoReCompute) { this.hasBindAutoReCompute = true; dependenceManager.beginCollect(this._reComputed, this); this._reComputed(); dependenceManager.endCollect(); } } // 依赖属性变动时调用的函数 _reComputed() { this.value = this.getter.call(this.target); dependenceManager.notify(this.id); } // 提供给内部调用时收集依赖应用 get() { this._bindAutoReCompute() dependenceManager.collect(this.id); return this.value }}
observer
observer 绝对实现会简略一点,其实是利用 React 的 render 函数对依赖进行收集,咱们采纳在 componnetDidMount 中调用 autorun 办法
export function observer(target: any) { const componentDidMount = target.prototype.componentDidMount; target.prototype.componentDidMount = function () { componentDidMount && componentDidMount.call(this); autorun(() => { this.render(); this.forceUpdate(); }); };}
至此一个简略的 Mobx 就实现了,线上代码地址
文章中应用的 Object.defineProperty 实现,Proxy 实现差不多,线上代码地址
Mobx vs Redux
数据流
Mobx 和 Redux 都是单向数据流,都通过 action 触发全局 state 更新,再告诉视图
Redux 的数据流
Mobx 的数据流
批改数据的形式
- 他们批改状态的形式是不同的,Redux 每一次都返回了新的 state。Mobx 每次批改的都是同一个状态对象,基于响应式原理,
get
时收集依赖,set
时告诉所有的依赖 - 当 state 产生扭转时,Redux 会告诉所有应用 connect 包裹的组件;Mobx 因为收集了每个属性的依赖,可能精准告诉
- 当咱们应用 Redux 来批改数据时采纳的是 reducer 函数,函数式编程思维;Mobx 应用的则是面向对象代理的形式
- 他们批改状态的形式是不同的,Redux 每一次都返回了新的 state。Mobx 每次批改的都是同一个状态对象,基于响应式原理,
Store 的区别
- Redux 是繁多数据源,采纳集中管理的模式,并且数据均是一般的 JavaScript 对象。state 数据可读不可写,只有通过 reducer 来扭转
- Mobx 是多数据源模式,并且数据是通过
observable
包裹的 JavaScript 对象。state 既可读又可写,在非严格模式下,action 不是必须的,能够间接赋值
一些补充
observable 应用函数式写法
在采纳的 proxy 写法中,能够劫持到一个对象,将对象存在 weakMap 中,每次触发对应事件去获取相干信息
Proxy 监听 Map/Set
总结
本文从 Mobx 的简略示例开始,讲述了一下 Mobx 的执行流程,引入了对应的外围概念,而后从零开始实现了一个简版的 Mobx,最初将 Mobx 和 Redux 做了一个简略的比照
参考链接
- 从零实现 Mobx:深刻了解 Mobx 原理
- MobX 实现原理揭秘
- 引入Mobx
- 实现一个简略的 MobX
- 用故事解读 MobX源码