1. Target Positioning
Simple, scalable state management
A simple, sufficient state management library. Still wants to solve the application state (data) management problem
2. Design Philosophy
Anything that can be derived from the application state, should be derived. Automatically.
Everything derived from application state should be automatically obtained. Such as UI, data serialization, service communication
That is, as long as we know which things are state-related (derived from application state), when state changes, all state-related things should be automatically completed, automatically update UI, automatically cache data, automatically notify server
This philosophy seems novel, actually it's data-driven. Thinking about it, React ecosystem (react + react-redux + redux + redux-saga) also satisfies this philosophy, after state changes (dispatch action triggers stateChange), UI automatically updates (Container update), automatically triggers cached data, notifies server and other side effects (saga)
3. Core Implementation
MobX is inspired by reactive programming principles as found in spreadsheets. It is inspired by MVVM frameworks like in MeteorJS tracker, knockout and Vue.js. But MobX brings Transparent Functional Reactive Programming to the next level and provides a stand alone implementation. It implements TFRP in a glitch-free, synchronous, predictable and efficient manner.
Referenced MeteorJS's tracker, knockout and Vue, these things' common characteristic is they all have built-in data binding, belong to so-called MVVM architecture, respectively borrowed:
-
MeteorJS's design philosophy: automatically track dependencies (
tracker, autorunetc.), no need to declare dependencies anymore, makes many things simpler -
knockout's data binding:
ko.observable -
Vue's runtime dependency collection and computed: implemented based on
getter&setterdata binding
So, MobX's core implementation is very similar to Vue, can be viewed as taking Vue's data binding mechanism out separately, then doing enhancement and extension:
-
Enhancement: observable not only supports Array, Object, also supports Map and immutable Value (corresponding to
boxed value) -
Extension: provides observer (expose data changes), spy (expose internal state), action (standard constraints, or to accommodate Flux)
P.S. From functionality perspective, having observable and observer can guarantee usability. action is constraint on flexibility, spy is for DevTools integration, neither is important
Additionally, MobX also uses ES Decorator syntax to let listening to changes combine with OOP, looks quite elegant, for example:
import { observable, computed } from "mobx";
class OrderLine {
@observable price = 0;
@observable amount = 1;
@computed get total() {
return this.price * this.amount;
}
}
Without this class annotation syntax, it's not beautiful at all:
var OrderLine = function() {
extendObservable(this, {
price: observable.ref(0),
amount: observable.ref(1),
total: computed(function() {
return this.price * this.amount;
})
});
}
Using this feels much more troublesome, far less elegant than annotation form. Using Decorator to combine observable and OOP system,算是 MobX's major highlight
P.S. Decorator feature is currently still in new proposal stage, belongs to very unstable feature, therefore mostly only use general form:
function myDecorator(target, property, descriptor){}
From babel transformation result, it's interception of Object.defineProperty (so Decorator method signature is completely consistent with Object.defineProperty)
P.S. Actually Vue ecosystem also has similar things combining with OOP, such as vuejs/vue-class-component
4. Structure
modify update trigger
action ------> state ------> computed -------> reaction
Compare Flux
Kept Flux's action, added a computed layer, proposed reaction concept
The action here is much thicker than Flux's action concept, equivalent to action + dispatcher + part in store responsible for responding to action to modify state, in short, MobX's action is verb, Flux's action is noun. MobX's action is an action, directly modifies state, Flux's action is just an event message, modified by event receiver (part in store responsible for responding to action to modify state)
computed has same meaning as Vue's computed, both refer to derived data depending on state (data that can be calculated from state), after state changes, automatically recalculate computed. Additionally, computed is called derivation conceptually, that is "derived", because computed depends on state, is data derived from state
reaction refers to responses made to state changes, such as updating views, or notifying server (using autorun). Biggest difference from computed is computed produces new data without side effects (while reaction has side effects but doesn't produce new data)
Basically consistent with Flux's (state, action) => state thinking, computed can be viewed as upper layer state, and an important part in reaction is updating views, then simplified to:
modify trigger
action ------> state -------> views
Compare Flux's structure:
action 传递 action update state
------> dispatcher ---------> stores ------------> views
As mentioned above, action + dispatcher + part in store responsible for responding to action to modify state is equivalent to MobX's action
Compare Redux
call new state
action --> store ------> reducers -----------> view
(From Redux)
Reducer in Redux is all stuffed into action in MobX, no need to use reducer to describe state structure anymore, also no need to care whether reducer is pure (MobX only requires computed to be pure function)
computed is a blank in Redux, so filled by reactjs/reselect, also to reuse data derivation logic, also comes with caching. So MobX is at least equivalent to Redux + reselect
Compare Vuex
commit mutate render
action ------> mutation ------> state ------> view
Vuex's characteristic is distinguishing synchronous/asynchronous action from design perspective, corresponding to mutation and action respectively
Compared to MobX, exactly two extremes. Vuex found Flux's action not refined enough, didn't consider asynchronous scenarios, only then proposed action above mutation, while MobX found distinguishing synchronous/asynchronous, pure and impure too troublesome, only then proposed verb action, encompassing asynchronous and side effects
computed is called getter in Vuex, not much difference between the two. Vuex also considered state derived data from the beginning, unlike Redux needing reselect to fill the blank
5. Advantages
From implementation perspective, only MobX has built-in data change listening, that is putting data binding's core work at data layer, biggest benefit of doing this is modifying state becomes very natural, no need for dispatch, no need to create action, want to change just change according to intuition
State Modification Method Conforms to Intuition
React example:
@observer
class TodoListView extends Component {
render() {
return <div>
<ul>
{this.props.todoList.todos.map(todo =>
<TodoView todo={todo} key={todo.id} />
)}
</ul>
Tasks left: {this.props.todoList.unfinishedTodoCount}
</div>
}
}
const TodoView = observer(({todo}) =>
<li>
<input
type="checkbox"
checked={todo.finished}
{/* Want to change, just change directly */}
onClick={() => todo.finished = !todo.finished}
/>{todo.title}
</li>
)
(Complete example see React components)
No need to define action to change state (even to add reducer to define state), want to change just change directly, no need through library API. This point is same as Vue data binding's advantage, library itself can listen to data changes, no need for user to manually notify changes, business writing becomes convenient
More Powerful DevTools
In Flux, action layer's core function is to make state changes traceable, action as state change cause can be recorded (DevTools or logger), while MobX uses function name as action's carried cause information, implements state change traceability through spy, can implement more powerful DevTools, such as making component's data dependency visualized

Component-Level Precise Data Binding
Compared to react-redux, mobx-react can achieve more precise view updates, component-granularity precise re-rendering, unlike react-redux needing to diff from outside (Container) downward to find Views needing re-rendering, MobX clearly knows data dependency relationships, no need to search. Then from performance perspective, at least saved cost of finding dirty View
Another performance point is mobx-react removed Container concept, actually through intercepting component lifecycle way to implement (specifically see source code analysis section below), this reduces React component tree depth, theoretically performance will be slightly better
Additionally, because dependency collection is completed by MobX, brought benefit is being able to analyze actually needed data dependencies, avoiding performance loss brought by artificially produced unnecessary Containers
P.S. For more information about runtime dependency collection mechanism, please check Runtime Dependency Collection Mechanism
No Restrictions on State Structure
Flux requires state to be a pure object, this not only forces users to spend energy designing state's structure, also forcibly separates data and corresponding operations, using MobX's words:
But this introduces new problems; data needs to be normalized, referential integrity can no longer be guaranteed and it becomes next to impossible to use powerful concepts like prototypes.
Restricting state from being casually modified, this way some original advantages built on data models are lost, such as prototypes
While MobX has no restrictions on state's structure and type, MobX's state definition is:
Graphs of objects, arrays, primitives, references that forms the model of your application.
Doesn't require single state tree, also doesn't require pure objects, for example:
class ObservableTodoStore {
@observable todos = [];
@observable pendingRequests = 0;
?
constructor() {
mobx.autorun(() => console.log(this.report));
}
?
@computed get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}
?
@computed get report() {
if (this.todos.length === 0)
return "<none>";
return `Next todo: "${this.todos[0].task}". ` +
`Progress: ${this.completedTodosCount}/${this.todos.length}`;
}
?
addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null
});
}
}
?
const observableTodoStore = new ObservableTodoStore();
Such state definition is MobX's basic gameplay, no need to extract shared data from business, also no need to worry whether current state structure can satisfy future scenarios (what if there are multiple data in future, what if data volume is too large, how to adjust state structure)... Data and corresponding operations can be associated together, organize however you like (use class, or keep Bean + Controller)
When migrating existing projects, can better highlight advantage of not restricting state structure, without changing original model definitions, intrusiveness is very small, only need to add some annotations, can obtain benefits brought by state management layer, why not? Imagine applying Redux to a complex old project, at least need:
-
Extract all shared states as state
-
Extract corresponding operations as reducer and saga, and ensure reducer structure is consistent with state
-
Define action, associate data and operations
-
Insert Container in appropriate places
-
Replace all parts modifying state with dispatch
... Forget it, cost is extremely high, not recommended to refactor
6. Source Code Analysis
mobx
Core part is Observable, that is part responsible for completing @observable decoration action:
export class IObservableFactories {
box<T>(value?: T, name?: string): IObservableValue<T> {
if (arguments.length > 2) incorrectlyUsedAsDecorator("box")
return new ObservableValue(value, deepEnhancer, name)
}
shallowBox<T>(value?: T, name?: string): IObservableValue<T> {
if (arguments.length > 2) incorrectlyUsedAsDecorator("shallowBox")
return new ObservableValue(value, referenceEnhancer, name)
}
array<T>(initialValues?: T[], name?: string): IObservableArray<T> {
if (arguments.length > 2) incorrectlyUsedAsDecorator("array")
return new ObservableArray(initialValues, deepEnhancer, name) as any
}
shallowArray<T>(initialValues?: T[], name?: string): IObservableArray<T> {
if (arguments.length > 2) incorrectlyUsedAsDecorator("shallowArray")
return new ObservableArray(initialValues, referenceEnhancer, name) as any
}
map<T>(initialValues?: IObservableMapInitialValues<T>, name?: string): ObservableMap<T> {
if (arguments.length > 2) incorrectlyUsedAsDecorator("map")
return new ObservableMap(initialValues, deepEnhancer, name)
}
//...还有很多
}
(From mobx/src/api/observable.ts)
Recursively downward hang getter&setter on data, for example Class Decorator's implementation:
const newDescriptor = {
enumerable,
configurable: true,
get: function() {
if (!this.__mobxInitializedProps || this.__mobxInitializedProps[key] !== true)
typescriptInitializeProperty(
this,
key,
undefined,
onInitialize,
customArgs,
descriptor
)
return get.call(this, key)
},
set: function(v) {
if (!this.__mobxInitializedProps || this.__mobxInitializedProps[key] !== true) {
typescriptInitializeProperty(
this,
key,
v,
onInitialize,
customArgs,
descriptor
)
} else {
set.call(this, key, v)
}
}
}
// 定义 getter&setter
if (arguments.length < 3 || (arguments.length === 5 && argLen < 3)) {
Object.defineProperty(target, key, newDescriptor)
}
(From mobx/src/utils/decorators.ts)
Array change monitoring see mobx/src/types/observablearray.ts, not much difference from Vue's implementation
mobx-react
"Container" implementation as follows:
// 注入的生命周期逻辑
const reactiveMixin = {
componentWillMount: function() {},
componentWillUnmount: function() {},
componentDidMount: function() {},
componentDidUpdate: function() {},
shouldComponentUpdate: function(nextProps, nextState) {}
}
// 劫持组件的生命周期
function mixinLifecycleEvents(target) {
patch(target, "componentWillMount", true)
;["componentDidMount", "componentWillUnmount", "componentDidUpdate"].forEach(function(
funcName
) {
patch(target, funcName)
})
if (!target.shouldComponentUpdate) {
target.shouldComponentUpdate = reactiveMixin.shouldComponentUpdate
}
}
(From mobx-react/src/observer.js)
Intercepting component lifecycle mainly has 3 functions:
-
Associate data updates with UI updates
-
Expose component state, integrate DevTools
-
Built-in shouldComponentUpdate optimization
react-redux triggers Container update through setState({}), while mobx-react triggers hijacked View update through forceUpdate:
const initialRender = () => {
if (this.__$mobxIsUnmounted !== true) {
let hasError = true
try {
isForcingUpdate = true
if (!skipRender) Component.prototype.forceUpdate.call(this)
hasError = false
} finally {
isForcingUpdate = false
if (hasError) reaction.dispose()
}
}
}
(From mobx-react/src/observer.js)
Part integrating DevTools:
componentDidMount: function() {
if (isDevtoolsEnabled) {
reportRendering(this)
}
},
componentDidUpdate: function() {
if (isDevtoolsEnabled) {
reportRendering(this)
}
}
Built-in shouldComponentUpdate:
shouldComponentUpdate: function(nextProps, nextState) {
if (this.state !== nextState) {
return true
}
return isObjectShallowModified(this.props, nextProps)
}
(From mobx-react/src/observer.js)
Reference Materials
-
Ten minute introduction to MobX and React: Examples combining with React usage
No comments yet. Be the first to share your thoughts.