数据流框架
演变体系的思考
Flux 体系本质上定义了应用的:「数据流」、「事件」、「数据到视图的响应」的一系列处理规则和方式。
抓住不变的和本质来进行思考。
- Unidirectional Dataflow
- SingleState Tree v.s. Multiple state-tree (Redux / HMR / Time-trackable)
- Pure-Render(pure render function without state)
- Reducer(functional computation) old state => pure function => new state
- Action to mutate state (Flux)
- Immutable State (Immutable.js)
- Reactive State as Reactive Functional Programming (Mobx)
- Reactive Stream (rx.js)
- Sync & Async mechanism both components loader and actions.
- Virtual Dependency State Graph (Mobx)
MV*
MVC
传统:Controllers => Model => Views 模式。
- Backbone
MVVM
MVVM:单向 & 双向绑定的数据流机制。
- Angular 1 / 2
Flu*
程序架构的目的,无非为了提高交付质量,在软件工程方面具体展现在:性能、安全、可维护性、可扩展性上。
我们很懒,追求简单。
flux 本质和 MVC 一样,是一种程序架构的思想
- flux 以:
action --> dispatcher --> store --> view --> action
的单向数据流通。 - React 的数据就好像水流一样,逐渐注入每一个 Componenets。而来自服务端的事件和 UI 事件,就好像一股股新的水流再重新灌入这个巨大的「状态机」中,引发组件的状态变化。避免了
Controlers <=> Views
之间的纷繁复杂的连接。 - 这样其实也是一种典型的 松耦合 模式。
React 是 Facebook 开发前端框架之一,功能很像 MVC 中的 V,负责前端 UI 的组件的创建。
- React 的事件系统使用了 StateMachine 的模型,利用
setState(data, callback)
来讲回调放入this.state
中,并重新渲染整个组件。整个开发过程基本上不需要手动干预DOM操作。 - 清晰地理解:React 的事件体系以及 React Components 的复用性、可维护、可扩展性的实现。整个 ReactApp,很像一个聚集了一堆状态机的超级状态机,这个状态机,会用 actions 来调节自己的 State 从而 Render View.
- Store 和 State、Props 的关系也是很重要的。
- 相比当今主流 MV,Flux 意在解决 MV 模块之间的复杂耦合的问题,以提高数据、UI组件的 可预测性,提出*单向数据流的概念,来替代传统的模式。因为数据流是单向的,所以便于测试调试,提高程序的逻辑清晰度,减少未知 Bug 的数量。
Reference Materials:
Reflux
Reflux has refactored Flux to be a bit more dynamic and be more Functional Reactive Programming (FRP) friendly:
- The singleton dispatcher is removed in favor for letting every action act as dispatcher instead.
- Because actions are listenable, the stores may listen to them. Stores don’t need to have big switch statements that do static type checking (of action types) with strings.
- Stores may listen to other stores, i.e. it is possible to create stores that can aggregate data further, similar to a map/reduce.
waitFor
is replaced in favor to handle serial and parallel data flows:- Aggregate data stores (mentioned above) may listen to other stores in serial
- Joins for joining listeners in parallel
- Action creators are not needed because RefluxJS actions are functions that will pass on the payload they receive to anyone listening to them.
Redux
- Official Documentation
- Awesome List of Redux
- Skeleton Reference
- Full-Stack Redux App Design
- A Cartoon Guide to Redux
最佳实践:ReduxToolkit
Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen. These restrictions are reflected in the three principles of Redux.
Redux is a much better implementation of a flux–like, unidirectional data flow. Redux makes actions composable, reduces the boilerplate code and makes hot–reloading possible in the first place.
- The whole state of your app is stored in an object tree inside a single store.
- Application state is all stored in one single tree structure which is difference from React component’s state object.
- The only way to change the state tree is to emit an action, an object describing what happened.
- To specify how the actions transform the state tree, you write pure reducers.
import { createStore } from 'redux'
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
// Create a Redux store holding the state of your app.
// Its API is { subscribe, dispatch, getState }.
let store = createStore(counter)
// You can subscribe to the updates manually, or use bindings to your view layer.
store.subscribe(() =>
console.log(store.getState())
)
// The only way to mutate the internal state is to dispatch an action.
// The actions can be serialized, logged or stored and later replayed.
store.dispatch({ type: 'INCREMENT' }) // 1
store.dispatch({ type: 'INCREMENT' }) // 2
store.dispatch({ type: 'DECREMENT' }) // 1
Core conceptions:
Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using
store.dispatch()
.Action Creator: Action creators are exactly that—functions that create actions.
let addTodo = (text) => { type: ADD_TODO, text };
const boundAddTodo = (text) => dispatch(addTodo(text));
Async Actions: Use async actions return
Promise
and when promise then is called to re-calcuate a new state.
Reducers: Note that a reducer is a pure function. It only computes the next state. It should be completely predictable: calling it with the same inputs many times should produce the same outputs. It shouldn’t perform any side effects like API calls or router transitions. These should happen before an action is dispatched.
In a more complex app, you’re going to want different entities to reference each other. We suggest that you keep your state as normalized as possible, without any nesting. Keep every entity in an object stored with an ID as a key, and use IDs to reference it from other entities, or lists. Think of the app’s state as a database.function todoApp(state = initialState, action) {
// For now, don’t handle any actions
// and just return the state given to us.
return state
}
- Handle more reducers to use
combineReducers
const reducer = combineReducers({
a: doSomethingWithA,
b: processB,
c: c
})
Store: The store has the following responsibilities:
- Holds application state;
- Allows access to state via
getState()
; - Allows state to be updated via
dispatch(action)
; - Registers listeners via
subscribe(listener)
; - Handles unregistering of listeners via the function returned by
subscribe(listener)
.
- Middlewares: Middleware is the suggested way to extend Redux with custom functionality. Middleware lets you wrap the store’s
dispatch
method for fun and profit. The key feature of middleware is that it is composable. Multiple middleware can be combined together, where each middleware requires no knowledge of what comes before or after it in the chain.redux-thunk
redux-logger
redux-dev-tool
redux-sega
[redux-query](https://amplitude.engineering/introducing-redux-query-7734e7215b3b#.w3wul45ef)
: deal with REST-ful API style with high order function.
DataFlow:
Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen.
Redux architecture revolves around a strict unidirectional data flow.
- You can call
store.dispatch(action)
from anywhere in your app, including components and XHR callbacks, or even at scheduled intervals. - The Redux store calls the reducer function you gave it.
- The root reducer may combine the output of multiple reducers into a single state tree.
- The Redux store saves the complete state tree returned by the root reducer.
The react-redux
:
Offer the Provider
and Container
conceptions.
import * as todoActionCreators from './todoActionCreators'
import * as counterActionCreators from './counterActionCreators'
import { bindActionCreators } from 'redux'
function mapStateToProps(state) {
return { todos: state.todos }
}
function mapDispatchToProps(dispatch) {
return {
todoActions: bindActionCreators(todoActionCreators, dispatch),
counterActions: bindActionCreators(counterActionCreators, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)
Mobx
In other words; when using manual subscriptions, your app will eventually be inconsistent.
➡️ Mobx Doc
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.
Advantages:
- Using classes and real references. With MobX you don’t need to normalize your data. This makes the library very suitable for very complex domain models
- Referential integrity is guaranteed.
- Simpler actions are easier to maintain. Modifying state when using MobX is very straightforward. Just
message.title = xxx
, mobx will do the rest of the things. - Fine grained observability is efficient. MobX builds a graph of all the derivations in your application to find the least number of re-computations that is needed to prevent staleness.
Key questions to answer:
- How do
Observables
been created? - How does
derivations
collect dependency of Observables? - How to make all derivations update both consistently and efficiently?
- How to schedule the reactions?
核心概念理解:
Make things Observable :)
- Observable state
@obserable
, make a variable (different types) observable.
// how to make an object observable
extendObservable({}, props, decorators, o)
=> asObservableObject(target, options.name, defaultDecorator.enhancer)
=> {
// A class to make object Observable
adm = new ObservableObjectAdministration(target, name, defaultEnhancer)
addHiddenFinalProp(target, "$mobx", adm)
return adm
}
// To make an array object
class ObservableArray extends Array {
// override array methods to make is obversable
push() {}
}
MobX reacts to any existing observable property that is read during the execution of a tracked function.
- reading is dereferencing an object’s property, which can be done through “dotting into” it (eg. user.name) or using the bracket notation (eg. user[‘name’]).
- trackable functions are the expression of computed, the render() method of an observer component, and the functions that are passed as the first param to when, reaction and autorun.
- during means that only those observables that are being read while the function is executing are tracked. It doesn’t matter whether these values are used directly or indirectly by the tracked function.
MobX will always try to minimize the number of computations that are needed to produce a consistent state.
MobX doesn’t run all derivations, but ensures that only computed values that are involved in some reaction are kept in sync with the observable state. Those derivations are called to be reactive.
Change Observable:
Actions @action
: Actions are anything that modify the state. It takes a function and returns a function with the same signature, but wrapped with transaction
, untracked
, and allowStateChanges
.
Especially the fact that transaction
is applied automatically yields great performance benefits, which can be used to batch a bunch of updates without notifying any observers until the end of the transaction; actions will batch mutations and only notify computed values and reactions after the (outer most) action has finished. This makes sure intermediate or incomplete values produced during an action are not visible to the rest of the application until the action has finished.
Async flow to use runInAction
to handle the state change.
Reacting to Observable:
- Computed values
@computed
. Creates a computed property. Theexpression
should not have side effects but return a value. The expression will automatically be re-evaluated if any observables it uses changes, but only if it is in use by some reaction. - Reactions:
@reaction
Reactions are similar to a computed value, but instead of producing a new value, a reaction produces a side effect for things like printing to the console, making network requests, incrementally updating the React component tree to patch the DOM, etc. In short, reactions bridge reactive and imperative programming.- Auto-run
@autorun
: can be used in those cases where you want to create a reactive function that will never have observers itself. This is usually the case when you need to bridge from reactive to imperative code, for example for logging, persistence, or UI-updating code.- Take a function, and make it reactive.
- After each run, subscribe to all observable data it accessed while running.
- Re-run on data changes.
- Optimize dependency tree.
- Observer
@observer
: can be used to turn ReactJS components into reactive components. It wraps the component’s render function inmobx.autorun
to make sure that any data that is used during the rendering of a component forces a re-rendering upon change. - Difference between
@computed
and@autorun
: they are both reactively invoked expressions, but use@computed
if you want to reactively produce a value that can be used by other observers andautorun
if you don’t want to produce a new value but rather want to achieve an effect. For example imperative side effects like logging, making network requests etc.
- Auto-run
var numbers = observable([1,2,3]);
var sum = computed(() => numbers.reduce((a, b) => a + b, 0));
var disposer = autorun(() => console.log(sum.get()));
// prints '6'
numbers.push(4);
// prints '10'
disposer();
numbers.push(5);
// won't print anything, nor is `sum` re-evaluated
In depth:
- ➡️ Becoming fully reactive: an in-depth explanation of MobX
- Runtime dependency tree analysis.