Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redux深度揭秘 #5

Open
jnotnull opened this issue Aug 3, 2017 · 0 comments
Open

Redux深度揭秘 #5

jnotnull opened this issue Aug 3, 2017 · 0 comments

Comments

@jnotnull
Copy link
Owner

jnotnull commented Aug 3, 2017

背景

根据React官方定义,React是解决UI层的库,所以在实际项目中要想完成功能,必须借助其他手段来完成其它层的定义和控制。Redux的出现很好的解决了数据流的问题,完成了其它层的定义和控制。

和传统MVC相比的优势

我们先看下传统的MVC结构。

                         + Input
                         |
                         |
                 +-------v------+
       +---------+  Controller  +---------+
       |         +--------------+         |
       |                                  |
       |                                  |
       |                                  |
+------v-------+                  +-------v------+
|     Model    |                  |      View    |
+--------------+                  +--------------+

从图中我们可以看出以下问题:

  1. 当Controller和Model进行交互时候,他们会改变Model的取值,但是随着项目复杂度的增加,可能会有很多Controll操作相同的Model,带来的问题就是最后不知道有哪些操作了Model,这也带来了数据的不确定性。
  2. 因为不可预测,所以很难做到undo。
                                       +--------+
                      +----------------+ Action <-------------+
                      |                +--------+             |
                      |                                       |
                      |                                       |
+--------+      +-----v------+        +---------+        +----+---+
| Action +------> Dispatcher +--------> Reducer +-------->  View  |
+--------+      +------------+        +---------+        +--------+

而Redux的出现很好的解决了这些问题。根据官方所述,它主要有以下特点。

  1. 因为使用了pure函数,所以任何时候数据的输出都是可预测的,包括UI,这也极大的方便进行单元测试;
  2. 通过记录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还定义了三个原则:

  1. 单一数据源
  2. State 是只读的
  3. 使用纯函数来执行修改

三个原则中都是针对数据的规范,由此我们可以得出结论,数据就是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

参考文章:

  1. https://blog.gisspan.com/2017/02/Redux-Vs-MVC,-Why-and-How.html
  2. https://fakefish.github.io/react-webpack-cookbook/
  3. https://blog.pusher.com/the-what-and-why-of-redux/
  4. https://zapier.com/engineering/how-to-build-redux/
  5. http://community.pearljam.com/discussion/95759/redux-why-is-it-called-that
  6. http://www.avitzurel.com/blog/2016/08/03/connected-higher-order-components-with-react-and-redux/
@jnotnull jnotnull changed the title 自己动手构建Redux Redux深度解密 Aug 8, 2017
@jnotnull jnotnull changed the title Redux深度解密 Redux深度揭秘 Aug 8, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant