一.목표 포지셔닝
Simple, scalable state management
단순하고 충분한 상태 관리 라이브러리입니다. 여전히 애플리케이션 상태 (데이터) 관리 문제를 해결하고 싶습니다
二.설계 이념
Anything that can be derived from the application state, should be derived. Automatically.
애플리케이션 상태에서 파생될 수 있는 모든 것은 자동으로 파생되어야 합니다. 예를 들어 UI, 데이터 직렬화, 서비스 통신 등
즉, 어떤 것이 상태 관련 (애플리케이션 상태에서 파생) 인지 알고 있다면, 상태가 변화할 때 상태 관련 모든 일을 자동으로 완료해야 합니다. 자동으로 UI 를 업데이트하고, 자동으로 데이터를 캐시하며, 자동으로 server 에 알림합니다
이 이념은 일견 신기해 보이지만, 사실은데이터 구동입니다. 잘 생각해 보면, React 체계 (react + react-redux + redux + redux-saga) 도 이 이념을 만족합니다. 상태 변화 (dispatch action 이 stateChange 를 유발) 후, UI 가 자동으로 업데이트되고 (Container update), 자동으로 캐시 데이터를 트리거하며, server 등에 부작용을 알림합니다 (saga)
三.핵심 구현
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.
MeteorJS 의 tracker, knockout 및 Vue 를 참고했습니다. 이 몇 가지 것들의 공통점은 모두데이터 바인딩을 내장하고 있으며, 소위 MVVM 아키텍처에 속하며, 각각에서 차용했습니다:
-
MeteorJS 의 설계 이념: 자동으로 의존 관계를 추적 (
tracker, autorun등), 의존 관계를 선언할 필요가 없어 많은 일을 더 단순하게 만듦 -
knockout 의 데이터 바인딩:
ko.observable -
Vue 의 런타임 의존 수집과 computed:
getter&setter데이터 바인딩 구현에 기반
따라서, MobX 의 핵심 구현은 Vue 와 매우 유사하며, Vue 의 데이터 바인딩 메커니즘을 단독으로 꺼내어, 더욱 강화와 확장을 performed 것으로 볼 수 있습니다:
-
강화:observable 은 Array, Object 뿐만 아니라, Map 및 불변의 Value(
boxed value에 해당) 도 지원 -
확장:observer(데이터 변화를 노출), spy(내부 상태를 노출), action(규범 제약, 또는 Flux 에 영합하기 위해) 제공
P.S.기능으로 보면, observable 과 observer 만 있으면 사용을 보장할 수 있습니다. action 은 유연성에 대한 제약, spy 는 DevTools 접속용으로, 모두 중요하지 않습니다
게다가, MobX 는 ES Decorator 구문을 이용하여, 변화 감시를OOP 와 결합하여 매우 우아하게 보입니다. 예를 들어:
import { observable, computed } from "mobx";
class OrderLine {
@observable price = 0;
@observable amount = 1;
@computed get total() {
return this.price * this.amount;
}
}
이 클래스 주석 구문이 없다면, 전혀 아름답지 않습니다:
var OrderLine = function() {
extendObservable(this, {
price: observable.ref(0),
amount: observable.ref(1),
total: computed(function() {
return this.price * this.amount;
})
});
}
이렇게 사용하면 매우 번거롭게 느껴지며, 주석 형식만큼 우아하지 않습니다. Decorator 를 이용하여 observable 과 OOP 체계를 결합하는 것은 MobX 의 큰하이라이트입니다
P.S.Decorator 특성은 현재 new proposal 단계에 있으며, 매우 불안정한 특성에 속하므로, 대부분은 일반 형식만 사용합니다:
function myDecorator(target, property, descriptor){}
babel 변환 결과로 보면, Object.defineProperty 의 인터셉트라고 할 수 있습니다 (따라서 Decorator 메서드 시그니처는 Object.defineProperty 와 완전히 일치합니다)
P.S.실제 Vue 생태에도 OOP 와 결합한 유사한 것이 있습니다. 예를 들어 vuejs/vue-class-component
四.구조
modify update trigger
action ------> state ------> computed -------> reaction
Flux 와 비교
Flux 의 action 을 유지하고, computed 의 층을 새로 추가하며, reaction 의 개념을 제안했습니다
여기서의 action 은 Flux 의 action 개념보다훨씬 두껍고, action + dispatcher + store 내에서 action 에 응답하여 state 를 수정하는 부분 에 해당합니다. 간단히 말해, MobX 의 action 은 동사, Flux 의 action 은 명사입니다. MobX 의 action 은 동작으로, 직접 상태를 수정합니다. Flux 의 action 은 단순한 이벤트 메시지로, 이벤트 수신측 (store 내에서 action 에 응답하여 state 를 수정하는 부분) 이 상태를 수정합니다
computed 는 Vue 의 computed 와 같은 의미로, 모두 state 에 의존하는 파생 데이터 (state 에서 계산할 수 있는 데이터) 를 지칭하며, state 변화 후, 자동으로 computed 를 재계산합니다. 게다가, computed 는 개념적으로derivation, 즉 "파생"이라고 불립니다. computed 는 state 에 의존하며, state 에서 파생된 데이터이기 때문입니다
reaction 은 state 변화에 대한 응답을 지칭하며, 예를 들어 뷰를 업데이트하거나, server 에 알림합니다 (autorun 이용). computed 와의 최대 차이는 computed 가 새로운 데이터를 생성하고 부작용을 포함하지 않는 반면 (reaction 은 부작용을 포함하지만 새로운 데이터를 생성하지 않음) 입니다
Flux 의 (state, action) => state 사고와 기본적으로 일치하며, computed 를 상층의 state 로 볼 수 있고, reaction 내의 중요한 부분은 뷰를 업데이트하는 것이므로, 다음과 같이 간략화할 수 있습니다:
modify trigger
action ------> state -------> views
Flux 의 구조와 비교:
action 传递 action update state
------> dispatcher ---------> stores ------------> views
위에서 언급한 바와 같이, action + dispatcher + store 내에서 action 에 응답하여 state 를 수정하는 부분 이 MobX 의 action 과 동등합니다
Redux 와 비교
call new state
action --> store ------> reducers -----------> view
(Redux 에서 인용)
Redux 의 reducer 는 MobX 에서는 모두 action 에詰め込まれて 있으며, reducer 로 state 구조를 기술할 필요가 없고, reducer 가 순수한지 여부를気に할 필요도 없습니다 (MobX 는 computed 만이 순수 함수であることを 요구)
computed 는 Redux 에서는공백이므로, reactjs/reselect 로 메웁니다. 마찬가지로 데이터 파생 로직을 재이용하기 위함이며, 마찬가지로 캐시를 내장하고 있습니다. 따라서 MobX 는 적어도 Redux + reselect 에 해당합니다
Vuex 와 비교
commit mutate render
action ------> mutation ------> state ------> view
Vuex 의 특징은 설계상에서 동기/비동기 action 을 구분하며, 각각 mutation 과 action 에 대응한다는 것입니다
MobX 와 비교하여, 정확히2 개의 극단입니다. Vuex 는 Flux 의 action 이 충분히 세분화되지 않았고, 비동기 시나리오를 고려하지 않았다고 느껴, mutation 위에 action 을 제안했습니다. MobX 는 동기/비동기, 순수/불순수를 구분하는 것이 번거롭다고 느껴, 동사 action 을 제안하고, 비동기와 부작용을囊括했습니다
computed 는 Vuex 에서는 getter 라고 불리며,二者에 큰 차이는 없습니다. Vuex 도一开始부터 state 파생 데이터를 고려했으며, Redux 처럼 reselect 로 공백을 메울 필요는 없습니다
五.장점
구현으로 보면, MobX 만 데이터 변화 감시를 내장하고 있으며, 즉데이터 바인딩의 핵심 작업을 데이터 층으로 끌어올렸습니다. 이렇게 하는 최대의 이점은 state 수정이 매우 자연스러워지고, dispatch 할 필요가 없으며, action 을 생성할 필요도 없고, 변경하고 싶으면 직접직관에 따라 변경할 수 있습니다
상태 수정 방식이 직관에 부합
React 예:
@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}
{/* 想改就直接改 */}
onClick={() => todo.finished = !todo.finished}
/>{todo.title}
</li>
)
(완전한 예는 React components 참조)
상태를 변경하기 위해 action 을 정의할 필요가 없으며 (게다가 상태를 정의하기 위해 reducer 를 추가할 필요도 없고), 변경하고 싶으면 직접 변경하며, 라이브러리 API 를通할 필요가 없습니다. 이 점은 Vue 데이터 바인딩의 장점과 같으며, 라이브러리 자신이 데이터 변화를 감시할 수 있어, 사용자가 수동으로 변화를 알림할 필요가 없고, 업무 작성이 편리해집니다
더 강력한 DevTools
Flux 에서 action 층의 핵심 작용은 상태 변화를 추적 가능하게 하는 것으로, action 은 상태 변화의 원인으로 기록될 수 있습니다 (DevTools 또는 logger). MobX 는 함수명을 action 이携える 원인 정보로 하고, spy 를 통해 상태 변화를 추적 가능하게 하여, 더 강력한 DevTools 를 실현할 수 있습니다. 예를 들어컴포넌트의 데이터 의존 관계를 가시화할 수 있습니다

컴포넌트 레벨의 정확한 데이터 바인딩
react-redux 와 비교하여, mobx-react 는 더 정확한 뷰 업데이트, 컴포넌트 입도의 정확한 재렌더링을 실현할 수 있습니다. react-redux 처럼 외부 (Container) 에서 아래로 diff 하여 재렌더링이 필요한 View 를 찾을 필요가 없으며, MobX 는 데이터 의존 관계를 명확히 알고 있어, 찾을 필요가 없습니다. 따라서 성능으로 보면, 적어도dirty View 를 찾는 비용을 절약했습니다
또 하나의 성능 포인트는 mobx-react 가 Container 의 개념을 삭제한 것으로, 실제로는컴포넌트 라이프사이클을 인터셉트하는 방식을 통해 실현하고 있습니다 (구체적인 아래 소스 코드 간이 분석 부분 참조). 이로 인해 React 컴포넌트 트리의 깊이를 감소시켜, 이론상 성능은 약간 좋아집니다
게다가, 의존 수집은 MobX 에 의해 완료되므로, 실제로 필요한 데이터 의존 관계를 분석할 수 있어, 인위적으로 발생하는 불필요한 Container 에 의한 성능 손실을 회피할 수 있습니다
P.S.런타임 의존 수집 메커니즘의 더 많은 정보는, 런타임 의존 수집 메커니즘 참조
state 의 구조를 제한하지 않음
Flux 는 state 가 순수한 오브젝트일 것을 요구하며, 이로 인해 사용자에게 state 의 구조를 설계하는 정력을 쏟게 할 뿐만 아니라, 데이터와相应操作을 강제로 분리했습니다. MobX 의 말로 하면:
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.
state 를隨意로 수정할 수 없도록 제한하면, 데이터 모델上に 구축된 일부 原有의 이점이 없어집니다. 예를 들어 프로토타입 등
MobX 는 state 의 구조 및 타입에 아무런 제한이 없습니다. MobX 에서 state 의 정의는:
Graphs of objects, arrays, primitives, references that forms the model of your application.
단일 상태 트리를 요구하지 않으며, 순수한 오브젝트도 요구하지 않습니다. 예를 들어:
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();
이러한 state 정의는 MobX 의 기본적인玩法로, 업무에서 공유 데이터를 추출할 필요가 없으며, 현재의 state 구조가 장래의 시나리오를 만족시킬 수 있는지 걱정할 필요도 없습니다 (이후에複数の 데이터가 있는 경우 어떻게 하는가, 데이터량이 너무 큰 경우 어떻게 하는가, state 구조를 어떻게 조정하는가) ……데이터와相应操作을 관련지을 수 있으며, 어떻게 조직해도構いません (class 사용, 또는 Bean + Controller 유지)
기존 프로젝트를 이전할 때, state 구조를 제한하지 않는 장점이 더욱 현저하게 나타납니다. 原有의 model 정의를 변경하지 않고, 침입성이 매우 작습니다. 주석을 몇 개 추가하기만 하면, 상태 관리 층이 가져오는 이점을 획득할 수 있습니다. 왜楽을 하지 않겠습니까? 복잡한 오래된 프로젝트에 Redux 를 도입하는 것을 상상해 보세요. 적어도 다음이 필요합니다:
-
공유 상태를 모두 추출하여, state 로
-
대응하는 조작도 모두 추출하여, reducer 와 saga 로, reducer 구조가 state 와 일치함을 보증
-
action 을 정의하여, 데이터와 조작을 관련지음
-
적절한 곳에 Container 를 삽입
-
state 를 수정하는 모든 부분을 dispatch 로 치환
……算了, 비용이 극히 높고, 리팩토링을 권장하지 않습니다
六.소스 코드 간이 분석
mobx
핵심 부분은 Observable 로, @observable 장식 동작을 완료하는 책임을 가진 부분입니다:
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)
}
//...还有很多
}
(mobx/src/api/observable.ts 에서 인용)
재귀적으로 아래로 데이터身上에 모두 getter&setter 를 겁니다. 예를 들어 Class Decorator 의 구현:
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)
}
(mobx/src/utils/decorators.ts 에서 인용)
배열의 변화 감시는 mobx/src/types/observablearray.ts 참조. Vue 의 구현 과 큰 차이는 없습니다
mobx-react
「Container」의 구현은 다음과 같습니다:
// 注入的生命周期逻辑
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
}
}
(mobx-react/src/observer.js 에서 인용)
컴포넌트 라이프사이클을 인터셉트하는 주요 3 가지 작용:
-
데이터 업데이트와 UI 업데이트를 관련시킴
-
컴포넌트 상태를 노출하여, DevTools 에 접속
-
내장 shouldComponentUpdate 최적화
react-redux 는 setState({}) 를 통해 Container 업데이트를 트리거하지만, mobx-react 는 forceUpdate 를 통해 인터셉트된 View 업데이트를 트리거합니다:
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()
}
}
}
(mobx-react/src/observer.js 에서 인용)
DevTools 에 접속하는 부분:
componentDidMount: function() {
if (isDevtoolsEnabled) {
reportRendering(this)
}
},
componentDidUpdate: function() {
if (isDevtoolsEnabled) {
reportRendering(this)
}
}
내장의 shouldComponentUpdate:
shouldComponentUpdate: function(nextProps, nextState) {
if (this.state !== nextState) {
return true
}
return isObjectShallowModified(this.props, nextProps)
}
(mobx-react/src/observer.js 에서 인용)
참고 자료
-
Ten minute introduction to MobX and React:React 와 조합하여 사용하는 예
아직 댓글이 없습니다