dva-generator
dva-generator copied to clipboard
Redux深度揭秘
背景
根据React官方定义,React是解决UI层的库,所以在实际项目中要想完成功能,必须借助其他手段来完成其它层的定义和控制。Redux的出现很好的解决了数据流的问题,完成了其它层的定义和控制。
和传统MVC相比的优势
我们先看下传统的MVC结构。
+ Input
|
|
+-------v------+
+---------+ Controller +---------+
| +--------------+ |
| |
| |
| |
+------v-------+ +-------v------+
| Model | | View |
+--------------+ +--------------+
从图中我们可以看出以下问题:
- 当Controller和Model进行交互时候,他们会改变Model的取值,但是随着项目复杂度的增加,可能会有很多Controll操作相同的Model,带来的问题就是最后不知道有哪些操作了Model,这也带来了数据的不确定性。
- 因为不可预测,所以很难做到undo。
+--------+
+----------------+ Action <-------------+
| +--------+ |
| |
| |
+--------+ +-----v------+ +---------+ +----+---+
| Action +------> Dispatcher +--------> Reducer +--------> View |
+--------+ +------------+ +---------+ +--------+
而Redux的出现很好的解决了这些问题。根据官方所述,它主要有以下特点。
- 因为使用了pure函数,所以任何时候数据的输出都是可预测的,包括UI,这也极大的方便进行单元测试;
- 通过记录action,我们能知道谁在什么时候修改了数据,这就让时间旅行成为现实。我们只要记录下修改上下文就可以了。
Redux思想
因为Redux有这多好处,那我们现在就来重点看下它到底是何物。首先从它的名字说起吧。
根据维基百科Redux 的解释: brought back, restored
可以看出它强调的就是状态的undo,如何做到这一点呢,靠的就是pure函数。pure函数是我们熟悉的了:对于相同的输入值,能够产生相同的输出,并且不依赖外部上下文。
关于Redux名字的讨论,有兴趣的可以看下这个帖子 Redux? Why is it called that? ,全当娱乐了。
下面我们来重点看下Redux组成。它主要分为三个部分 Action、Reducer、及 Store。先看下Reducer,根据名字可以看出来它是类似Reduce的角色。Reduce来源于函数式编程,参考MSDN 的定义,它会对数组中的所有元素调用指定的回调函数。该回调函数的返回值为累积结果,并且此返回值在下一次调用该回调函数时作为参数提供。方法签名如下:
array1.reduce(callbackfn[, initialValue])
这里的callbackfn就是reducer。由此我们可以模仿上面方法签名得出如下表达式:
Final State = [Action1, Action2, ..., ActionN].reduce(reducer, Initial State);
这就形成了Redux的基本核心思路:通过对Action数组的reduce处理,得到最终的状态。不仅如此,为了动作可控,Redux还定义了三个原则:
- 单一数据源
- State 是只读的
- 使用纯函数来执行修改
三个原则中都是针对数据的规范,由此我们可以得出结论,数据就是Redux的心脏,所有动作都是围绕它来做的。
Action和store
说完reducer之后我们再看下Action和store。按照官方所述,Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。
Action长啥样子呢?
{
type: ADD_TODO,
content: ''
}
可以看到就是一个普通的带有type的对象,type用来区分动作,其他都为参数。那store又是啥呢,很明显,它是对reducer的进一步混装,要想调用reducer里面的方法,必须走store的dispatch方法。
自己实现Redux -- state和action
理解了Redux的核心思想后,我们自己动手来实现一个Redux!
因为数据如此重要,我们首先从它开始入手。比如我们想开发一个TODO list,按照Redux单一数据源原则,我们定义如下:
window.state = {
todos: [],
nextId: 1
}
按照上面对Action的理解,我们定义如下Action:
{type: `ADD_TODO`}
{type: `UPDATE_TODO`, id:1, content: 'xx' }
因为按照第二个和第三个原则,我们不能直接修改state,所以我们要定义下纯函数:
add(state){
state.todos[state.nextId] = {
id: state.nextId,
content: `TODO${state.nextId}`
};
state.nextId++;
return state;
}
update(state, action){
state.todos[action.id].content = action.content;
return state;
}
但是你会发现这么写是有问题的,因为你直接修改了state的值,而对象的引用并没有变,这就无法做到undo了,所以我们必须引入新的概念,那就是Immutability。
add(state){
const id = state.nextId;
const newTODO = {
id,
content: ''
};
return {
...state,
nextId: id + 1,
todos: {
...state.todos,
[id]: newTODO
}
};
}
update(state, action){
const {id, content} = action;
const editedTODO = {
...state.todos[id],
content
};
return {
...state,
todos: {
...state.todos,
[id]: editedTODO
}
};
}
reducer
那现在我们就把它们封装到reducer中了
const CREATE_TODO = 'CREATE_TODO';
const UPDATE_TODO = 'UPDATE_TODO';
const reducer = (state = initialState, action) => {
switch (action.type) {
case CREATE_TODO: {
const id = state.nextId;
const newTODO = {
id,
content: ''
};
return {
...state,
nextId: id + 1,
todos: {
...state.todos,
[id]: newTODO
}
};
}
case UPDATE_TODO: {
const {id, content} = action;
const editedTODO = {
...state.todos[id],
content
};
return {
...state,
todos: {
...state.todos,
[id]: editedTODO
}
};
}
default:
return state;
}
};
现在我们用自己的reducer测试下:
const state0 = reducer(undefined, {
type: CREATE_TODO
});
我们可以看到state0为如下结果:
{nextId:2, todos:{id: 1, content: ''}}
我们再测试下UPDATE方法:
const state1 = reducer(state0, {
type: UPDATE_TODO,
id: 1,
content: 'Hello, world!'
});
我们可以看到state1为如下结果:
{nextId:2, todos:{id: 1, content: 'Hello, world!'}}
看看,测试起来都非常方便。那这两个Action如何一起调用了,很简单:
const actions = [
{type: CREATE_TODO},
{type: UPDATE_TODO, id: 1, content: 'Hello, world!'}
];
const state = actions.reduce(reducer, undefined);
我们得到了同样的结果:
{nextId:2, todos:{id: 1, content: 'Hello, world!'}}
store
完成action和reducer的构造之后,我们再来构造store。因为我们已经知道,调用reducer要走dispatch,所以先给出如下结构:
const createStore = (reducer, preloadedState) => {
let currentState = undefined;
return {
dispatch: (action) => {
currentState = reducer(preloadedState, action);
},
getState: () => currentState
};
};
增加下测试方法:
const store = createStore(reducer, window.state);
store.dispatch({
type: CREATE_TODO
});
console.log(store.getState());
非常赞,已经有了Redux的影子了,但是光有这些还不够。当数据发生变化时候,必须要进行通知,不然就没法进行界面渲染了。我们修改createStore如下:
const createStore = (reducer, preloadedState) => {
let currentState = undefined;
let nextListeners = [];
return {
dispatch: (action) => {
currentState = reducer(preloadedState, action);
nextListeners.forEach(handler => handler());
},
getState: () => currentState,
subscribe: handler => {
nextListeners.push(listener)
return function unsubscribe() {
var index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
};
};
添加listener到nextListeners后,返回unsubscribe,以供取消订阅。我们写下renderDOM:
store.subscribe(() => {
ReactDOM.render(
<div>{JSON.stringify(store.getState())}</div>,
document.getElementById('root')
);
});
到此,一个基本的Redux已经完成了。
那如何在React的组件中使用我们自己创建的store呢。只要把store作为属性传递进去就可以了:
const TODOApp = ({todos, handeladd, handeledit}) => (
<div>
<ul>
{
todos && Object.keys(todos).map((id, content) => (
<li key={id}>{content}</li>
))
}
</ul>
<button onClick={handeladd}>add</button>
</div>
);
class TODOAppContainer extends React.Component {
constructor(props) {
super();
this.state = props.store.getState();
this.handeladd = this.handeladd.bind(this);
this.handeledit = this.handeledit.bind(this);
}
componentWillMount() {
this.unsubscribe = this.props.store.subscribe(() =>
this.setState(this.props.store.getState())
);
}
componentWillUnmount() {
this.unsubscribe();
}
handeladd() {
this.props.store.dispatch({
type: CREATE_TODO
});
}
handeledit(id, content) {
this.props.store.dispatch({
type: UPDATE_TODO,
id,
content
});
}
render() {
return (
<TODOApp
{...this.state}
handeladd={this.handeladd}
handeledit={this.handeledit}
/>
);
}
}
ReactDOM.render(
<TODOAppContainer store={store}/>,
document.getElementById('root')
);
Provider 和 Connect
但是这样的做法显示是耦合太重了,我们针对React专门提供 Provider 和 Connect 方法,这就是 react-redux。
参考react-redux的做法,我们首先来新建一个Provider来包括APP,它的主要作用就是让store传递到所有子节点上去,getChildContext真是可以做这样的功能。
class Provider extends React.Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
另外,对于store中的数据变化要反映到组件中,我们通过connect来完成。根据定义,connect的方法签名如下:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
我们重点关注前面两个参数。
const connect = (mapStateToProps, mapDispatchToProps) => {
return (Component) => {
class Connected extends React.Component {
onStoreOrPropsChange(props) {
const {store} = this.context;
const state = store.getState();
const stateProps = mapStateToProps(state, props);
const dispatchProps = mapDispatchToProps(store.dispatch, props);
this.setState({
...stateProps,
...dispatchProps
});
}
componentWillMount() {
const {store} = this.context;
this.onStoreOrPropsChange(this.props);
this.unsubscribe = store.subscribe(() =>
this.onStoreOrPropsChange(this.props)
);
}
componentWillReceiveProps(nextProps) {
this.onStoreOrPropsChange(nextProps);
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return <Component {...this.props} {...this.state}/>;
}
}
Connected.contextTypes = {
store: PropTypes.object
};
return Connected;
}
};
调用方式:
const TODOAppContainer = connect(
mapStateToProps,
mapDispatchToProps
)(TODOApp);
再加上Provider
ReactDOM.render(
<Provider store={store}>
<TODOAppContainer/>
</Provider>,
document.getElementById('root')
);
至此我们已经构造了一个同步的Redux了。
完整代码路径:https://github.com/jnotnull/build-your-own-redux
参考文章:
- https://blog.gisspan.com/2017/02/Redux-Vs-MVC,-Why-and-How.html
- https://fakefish.github.io/react-webpack-cookbook/
- https://blog.pusher.com/the-what-and-why-of-redux/
- https://zapier.com/engineering/how-to-build-redux/
- http://community.pearljam.com/discussion/95759/redux-why-is-it-called-that
- http://www.avitzurel.com/blog/2016/08/03/connected-higher-order-components-with-react-and-redux/