谈谈 React 和 Redux

刚回到家的时候学习了 React 和 Redux,现在才想来总结一下,不知道会不会忘的差不多了...本来是想写一个问卷发布系统的,使用 React 和 Redux 已经完成了基础的几个功能,但是那个代码量...用 Angular 写简直轻轻松松的好吧...然后就去重构项目还有现在学的 Underscore 去了。是时候重新回顾下了。
此处主要讲的是 Redux 。

关于 Redux

redux 是facebook 提出的 flux 架构的一种优秀实现;而且不局限于为 react 提供数据状态处理。它是零依赖的,可以配合其他任何框架或者类库一起使用。要想配合 react,还得引入 react-redux。

关于 Flux

那什么是 Flux 呢?见下图
flux-overview.png
Flux 可以分为四个部分:

  • View: 视图层
  • Action: 视图层触发的动作
  • Dispatcher: 派发器,用来接受 Actions, 执行回调函数
  • Store:数据层,用来存放应用的状态,其变更会触发 View 层更新

Flux 的最大特点就是单向流动,他的过程大概如下:

  1. 用户访问 View ,触发了动作 Action
  2. Dispatcher 收到 Action ,根据 Action 类别进行相应的处理,处理结束后要求 Store 更新
  3. Store 进行更新,通知 View 层刷新
  4. View 层收到通知更新页面

额,其实我没有用 Flux,不敢讲太多了,简单的说就是一种单项数据流动的解决方案吧。我是直接学 Redux,对 Flux 也就大概了解这么多了。

Redux 和 Flux

Redux 是 Flux 的一种实现,但他们又有所不同,在 Flux 中,Store 可以有多个,但 Redux 有且只能有一个 Store,Flux 中存在 Dispatcher,在 Redux 则没有这个,而是用 reducer 代替了。 Flux 了=.=

理解Redux

Redux 由四部分组成:

  • Action
  • Reducer
  • Store
  • Views

我们结合具体的应用场景来看

Action

先从 Action 说起,一个 Action 是一个普通的对象。

export const SET_PAPER_TITLE = 'SET_PAPER_TITLE';
export const ADD_QUESTION = 'ADD_QUESTION';
export const REMOVE_QUESTION = 'REMOVE_QUESTION';
export function setPaperTitle(newTitle) {
    return {
        type: SET_PAPER_TITLE,
        value: newTitle
    }
}
export function addQuestion(type) {
    return {
        type: ADD_QUESTION,
        questionType: type
    }
}
export function removeQuestion(questionId) {
    return {
        type: REMOVE_QUESTION,
        questionId: questionId
    }
}

type 属性是必须的,表示动作类别,其他的参数可以自定。
我们先不用管 Action 有什么用,后面会提到。

Reducer

在 Action 这一层中,可以筛选掉脏数据,多余的参数不会传入,真正处理数据是在 Reducer 中。

import { SET_PAPER_TITLE, ADD_QUESTION, REMOVE_QUESTION, ADD_OPTION, REMOVE_OPTION, SET_QUESTION_TITLE, SET_OPTION_TITLE } from '../action/action'
import { combineReducers } from 'redux'
function paperReducer(state=[], action) {
    switch(action.type) {
        case SET_PAPER_TITLE:
            return Object.assign({}, state, {
                title: action.value
            });
        default:
            return state;
    }
}
function questionsReducer(state=[], action) {
    switch(action.type) {
        case ADD_QUESTION:
            return [
                ...state,
                {
                    title: '',
                    type: action.questionType,
                    content: ['','','','']
                }
            ];
        case REMOVE_QUESTION:
            return [
                ...state.slice(0, action.questionId),
                ...state.slice(action.questionId+1)
            ];
        case ADD_OPTION:
        case REMOVE_OPTION:
        case SET_QUESTION_TITLE:
        case SET_OPTION_TITLE:
            return [
                ...state.slice(0, action.questionId),
                questionReducer(state[action.questionId], action),
                ...state.slice(action.questionId+1)
            ];
        default:
            return state;
    }
}
var paperApp = combineReducers({
    paper: paperReducer,
    questions: questionsReducer
});
export default paperApp;

这里列了很多 Action,但在上面的 Action 部分我只提了两个,其他的其实都一样的,就是参数可能有些区别而已。
这里就要好好说一笔了,reducer 之所以叫 reducer,一个原因就是他很类似 JavaScript 中数组的 reduce 方法,接收两个参数,一个是当前状态,一个是处理方法。
但在 Redux 中,不要直接修改当前状态即 state,而应该返回一个新的 state,而这样做的其中一个好处就是可以实现时间旅行的功能,即可以回溯到任意版本的数据,并且对于判断 state 是否发生修改也很重要,如果是在原引用上修改我们得用 deepEqual 深度遍历来对比值,而如果返回了一个新的对象则可以直接使用 === 来判断两个数据是否一致,不一致则触发更改。reducer 方法应该都能看懂,接下来是 combineReducers 这个。
我上面其实涉及到三个 reducer 了,分别是

  • paperReducer
  • questionsReducer
  • questionReducer

但为了减少空间,我没有把 questionReducer 写出来。其实注意看从29行到32行的四个 Action,我都传入 state 中的部分数据给了 questionReducer 去做处理。因为这些都是涉及一个问题的修改的,所以就单独列出来。这样就不致于一个 reducer 写太多东西。
实质上还是两个 reducer 而已,这里的 combineReducers 就是把这两个 reducer 合并在一起。这两个 reducer 一个是处理问卷信息如问卷标题,一个是处理问卷的问题比如新建删除问题,问题修改这些。

Store

使用 combineReducers 就要求我们 Store 的设计合理,因为 Store 和 reducer 是要对应的,questionsReducer 只要处理问题,而 paperReducer 只要处理问卷本身,所以也就不需要往 paperReducer 传入完整的 state 信息。我们看下 Store。

import React from 'react'
import { createStore } from 'redux'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import paperApp from './reducer/reducer'
import App from './containers/App'

const initialState = {
    paper: {
        title: 'asd',
        time: "2016-7-19",
        author: "Ruiming",
    },
    questions: [{
        title: '',
        type: 'radio',
        content: ['', '', '', '']
    }, {
        title: '',
        type: 'checkbox',
        content: ['','','','']
    }]
};
let store = createStore(paperApp, initialState);
let rootElement = document.getElementById('index');
render (
    <Provider store={store}>
        <App />
    </Provider>,
    rootElement
);

我们初始一个数据 initialState,调用 createStore 方法需要传入两个参数,分别就是我们上面 combineReducers 后的paperApp和我们的数据。在初始数据中,有两个对象分别是paperquestions,这就刚好和 combineReducers 中的paperquestions对应,从而实现传递部分state
下面的 render 部分是就是渲染部分了, Provider 是 react-redux 提供的一个容器,将 Store 作为属性传递给该容器。

Views

我们看下 App 的内容

import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { addQuestion, addOption, setPaperTitle, setQuestionTitle, setOptionTitle, removeQuestion, removeOption } from '../action/action'
import Header from '../components/Header'
import NewQuestionBar from '../components/NewQuestionBar'
import OptionsBar from '../components/OptionsBar'

class App extends Component {
    render() {
        const { dispatch, title, questions } = this.props;
        return <div ref="paper">
            <Header title={title}
                    setPaperTitle={(title) => dispatch(setPaperTitle(title))}
                />
            <NewQuestionBar addQuestion={(type) => dispatch(addQuestion(type))} />
            <section className="paper">
                <ul className="paper-list">
                    {questions.map(function(question, i) {
                        return <OptionsBar content={question.content}
                                           addQuestion={() => dispatch(addQuestion())}
                                           addOption={(questionId) => dispatch(addOption(questionId))}
                        />
                    }.bind(this))
                    }
                </ul>
            </section>
        </div>
    }
}

function select(state) {
    return {
        paper: state.paper,
        questions: state.questions
    }
}

export default connect(select)(App);

上面我去掉了很多部分,只保留了 addQuestionaddOption 这两个,我们看下这两个就好了。其实这部分的写法就是 React 写法而已,只是多了个 dispatch 方法。注意下最后的 connect 方法,他的作用是:

  • 把所需要的 state 属性挂载到组件的 props 上。
  • 为组件的 props 添加 dispatch 方法。

Redux 运行过程

介绍完了 Redux 的四个部分,接下来就要说下这个运行的流程了。这才是理解 Redux 的关键。

  1. 用户在视图触发 dispatch 事件
  2. Redux 响应用户操作生成 action
  3. action 传到 store 层,可以使用中间件进行一些处理
  4. action 传 stateaction 给 reducer 处理
  5. reducer 返回一个新的 state
  6. store 读取 reducer 返回的内容,设置新的状态

大致就是以上过程,action 是唯一可以改变状态的途径,不仅包括用户的触发,也可以是来自服务器的推送,action 进行预处理后会传到 store,由 store 发送动作给 reducer,带上当前状态和当前动作,reducer 根据动作的 type 来进行不同的处理。注意 action 和 reducer 都是纯函数。

Redux 大致就是这么一回事,我们可以看到所有数据都来自一个对象 Store,这样就方便了调试测试,可以把 Store 就想象成一个数据库。state 只读,只能通过 action 改变,并且必须保证 reducer 是纯函数,所谓纯函数就是相同参数传入无数次他们都返回相同的东西,其实就是内部没有使用外部变量,外部变量总是伴随着不确定性。

React 和 Angular

React 现在很博人眼球,虚拟DOM提升了页面渲染性能,并且衍生的 React Native 也非常有诱惑力,单向数据流动虽然带来了清晰的逻辑和更高的性能,但降低了开发效率。另外,使用 React 开发的应用天然组件化,也方便了后期的维护。

Angular 是一个功能完善全面的框架,还自带了 $http, ngRoute, jQlite, $q, service 等等。提供了一整套的解决方案,估计这点很合大公司的胃口。并且 Angular 社区成熟活跃,生态完整,目前仍是最流行的前端框架,没有之一,非常适合用来写单页应用。数据双向绑定给开发带来了很大的便利,但双向绑定带来了性能损耗并且脏值检查性能也不好。

对比起来,我还是更喜欢 Angular,他的开发效率高,而且写起来很清晰,虽然他也有一些问题,但是在没有达到一个量级前这些问题是很难被体现出来的。而 React 可能在开发多端或者注重体积或者是在大型应用中可能才会去考虑吧。

另外 Angular2 也实现了虚拟 DOM,同样支持服务端渲染,使用 web worker 提升性能,TypeScript 提升了 JavaScript 项目的健壮性,而 Web Components 无疑是是未来趋势。我对 Angular2 同样满怀期待,我认为 Angular2 未来也会同 Angular1 一样火起来。

计划是再学习 react-router,写一个完整的 React 应用,后端使用 Koa 来开发。不过还是等手头上的东西处理完先吧。

除了 Angular1 和 Angular2 以及 React ,还有 Vue 和 Vue2 以及阿里的 Weex,前端的水深着呢!


深入到源码:解读 redux 的设计思路与用法