>

Redux从入门到跳楼,源码深入分析及运用

- 编辑:至尊游戏网站 -

Redux从入门到跳楼,源码深入分析及运用

Redux 源码剖析及应用

2018/05/23 · JavaScript · Redux

原文出处: 今日头条技术团队   

使用redux+react已有一段时间,刚开始使用并未深入了解其源码,最近静下心细读源码,感触颇深~

本文主要包含Redux设计思想、源码解析、Redux应用实例应用三个方面。

参考链接:

背景:

React 组件 componentDidMount 的时候初始化 Model,并监听 Model 的 change 事件,当 Model 发生改变时调用 React 组件的 setState 方法重新 render 整个组件,最后在组件 componentWillUnmount 的时候取消监听并销毁 Model。

最开始实现一个简单实例:例如add加法操作,只需要通过React中 setState 去控制变量增加的状态,非常简单方便。

但是当我们需要在项目中增加乘法/除法/幂等等复杂操作时,就需要设计多个state来控制views的改变,当项目变大,里面包含状态过多时,代码就变得难以维护并且state的变化不可预测。可能需要增加一个小功能时,就会引起多处改变,导致开发效率降低,代码可读性不高

例如以往使用较多backbone形式:至尊游戏网站 1

如上图所示,可以看到 Model 和 View 之间关系复杂,后期代码难以维护。

为了解决上述问题,在 React 中引入了 Redux。Redux 是 JavaScript 状态容器,提供可预测化的状态管理方案。下面详细介绍~~

  • Redux中文文档
  • Redux 入门教程-阮一峰
  • 看漫画,学 Redux
  • 在react-native中使用redux
  • [React Native]Redux的基本使用方式
  • Redux管理复杂应用数据逻辑

目的:

1、深入理解Redux的设计思想

2、剖析Redux源码,并结合实际应用对源码有更深层次的理解

3、实际工程应用中所遇到的问题总结,避免再次踩坑

目录

  • 应用场景
  • 使用的三原则
    • 单一数据源
    • 状态是只读的
    • 通过纯函数修改State
  • redux状态管理的流程及相关概念
    • store
    • Action
    • Action 创建函数(Action Creator)
    • Reducer
  • redux如何与组件结合
    • 具体示例1
    • 具体示例2

一、Redux设计思想

应用场景

React设计理念之一为单向数据流,这从一方面方便了数据的管理。但是React本身只是view,并没有提供完备的数据管理方案。随着应用的不断复杂化,如果用react构建前端应用的话,就要应对纷繁复杂的数据通信和管理,js需要维护更多的状态(state),这些state可能包括用户信息、缓存数据、全局设置状态、被激活的路由、被选中的标签、是否加载动效或者分页器等等。

这时,Flux架构应运而生,Redux是其最优雅的实现,Redux是一个不依赖任何库的框架,但是与react结合的最好,其中react-redux等开源组件便是把react与redux组合起来进行调用开发。

备注:

1.如果你不知道是否需要 Redux,那就是不需要它

2.只有遇到 React 实在解决不了的问题,你才需要 Redux

Redux使用场景:

  • 某个组件的状态,需要共享
  • 某个状态需要在任何地方都可以拿到
  • 一个组件需要改变全局状态
  • 一个组件需要改变另一个组件的状态

比如,论坛应用中的夜间设置、回到顶部、userInfo全局共享等场景。redux最终目的就是让状态(state)变化变得可预测.

背景:

传统 View 和 Model :一个 view 可能和多个 model 相关,一个 model 也可能和多个 view 相关,项目复杂后代码耦合度太高,难以维护。

redux 应运而生,redux 中核心概念reducer,将所有复杂的 state 集中管理,view 层用户的操作不能直接改变 state从而将view 和 data 解耦。redux 把传统MVC中的 controller 拆分为action和reducer

使用的三原则

  • 单一数据源

整个应用的state,存储在唯一一个object中,同时也只有一个store用于存储这个object.

  • 状态是只读的

唯一能改变state的方法,就是触发action操作。action是用来描述正在发生的事件的一个对象

  • 通过纯函数修改State

纯函数的问题,也是来自于函数式编程思想,我们在中学时学的函数就是纯函数,对于同一个输入,必然有相同的输出。这就保证了数据的可控性,这里的纯函数就是reducer

设计思想:

(1)Web 应用是一个状态机,视图与状态是一一对应的。

(2)所有的状态,保存在一个对象里面。

Redux 让应用的状态变化变得可预测。如果想改变应用的状态,就必须 dispatch 对应的 action。而不能直接改变应用的状态,因为保存这些状态的地方(称为 store)只有 get方法(getState) 而没有 set方法

只要Redux 订阅(subscribe)相应框架(例如React)内部方法,就可以使用该应用框架保证数据流动的一致性。

redux状态管理的流程及相关概念

至尊游戏网站 2

image

  • store

Store 就是保存数据的地方,保存着本程序所有的redux管理的数据,你可以把它看成一个容器。整个应用只能有一个 Store。(一个store是一个对象, reducer会改变store中的某些值)

Redux 提供createStore这个函数,用来生成 Store。

import { createStore } from 'redux';
const store = createStore(fn);

上面代码中,createStore函数接受另一个函数作为参数,返回新生成的 Store 对象。这个fn就是reducer纯函数,通常我们在开发中也会使用中间件,来优化架构,比如最常用的异步操作插件,redux-thunk,如果配合redux-thunk来创建store的话,代码示例:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers/rootReudcer';

let createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
let store = createStoreWithMiddleware(rootReducer);

redux-thunk的源码及其简单,如下:

// 判断action是否是函数,如果是,继续执行递归式的操作。所以在redux中的异步,只能出现在action中,而且还需要有中间件的支持。
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

同步action与异步action最大的区别是:

同步只返回一个普通action对象。而异步操作中途会返回一个promise函数。当然在promise函数处理完毕后也会返回一个普通action对象。thunk中间件就是判断如果返回的是函数,则不传导给reducer,直到检测到是普通action对象,才交由reducer处理。


Store 有以下职责:

  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。

一般情况下,我们只需要getState()和dispatch()方法即可,即可以解决绝大部分问题。

我们可以自定义中间件

比如我们自定义一个可以打印出当前的触发的action以及出发后的state变化的中间件,代码改动如下:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers/rootReudcer';

let logger = store => next => action => {
    if(typeof action === 'function') {
        console.log('dispatching a function')
    }else{
        console.log('dispatching', action);
    }

    let result = next(action);
    // getState() 可以拿到store的状态, 也就是所有数据
    console.log('next state', store.getState());
    return result;
}

let middleWares = {
    logger, 
    thunk
}
// ... 扩展运算符
let createStoreWithMiddleware = applyMiddleware(...middleWares)(createStore);

let store = createStoreWithMiddleware(rootReducer);

补充:我们自定义的中间件,也有对应的开源插件,redux-logger至尊游戏网站,,人家的更厉害。

如果,app中涉及到登录问题的时候,可以使用redux-persist第三方插件,这个第三方插件来将store对象存储到本地,以及从本地恢复数据到store中,比如说保存登录信息,下次打开应用可以直接跳过登录界面,因为我们目前的应用属于内嵌程序,不登陆的话也进不来,所以不用它。

  • Action

Action 是一个对象,描述了触发的动作,仅此而已。我们约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。通常它长一下这个样子:

{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

除了 type 字段外,action 对象的结构完全由你自己决定,来看一个复杂点的:

{
    type: 'SET_SCREEN_LAST_REFRESH_TIME',
    screenId,
    lastRefreshTime,
    objectId
}

通常我们会添加一个新的模块文件来存储这些actions types,比如我们新建一个actionTypes.js来保存:

//主页actions
export const FETCH_HOME_LIST = 'FETCH_HOME_LIST';
export const RECEIVE_HOME_LIST = 'RECEIVE_HOME_LIST';
//分类页actions
export const FETCH_CLASS_LIST = 'FETCH_CLASS_LIST';
export const RECEIVE_CLASS_LIST = 'RECEIVE_CLASS_LIST';
//分类详细页actions
export const FETCH_CLASSDITAL_LIST = 'FETCH_CLASSDITAL_LIST';
export const RECEIVE_CLASSDITAL_LIST = 'RECEIVE_CLASSDITAL_LIST';
export const RESET_CLASSDITAL_STATE = 'RESET_CLASSDITAL_STATE'; 
// 设置页actions
export const CHANGE_SET_SWITCH = 'CHANGE_SET_SWITCH';
export const CHANGE_SET_TEXT = 'CHANGE_SET_TEXT';
// 用户信息
export const USER_INFO = 'USER_INFO';

引用的时候,可以:

import * as types from './actionTypes';
  • Action 创建函数(Action Creator)

Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。在 Redux 中的 action 创建函数只是简单的返回一个 action。

import * as types from './actionTypes';
// 设置详情页内容文字主题
let changeText = (theme) => {
    return {
        type: types.CHANGE_SET_TEXT,
        theme
    }
}   

// 函数changeText就是一个简单的action creator。

完整的action文件(setAction.js)

import * as types from './actionTypes';

let setTitle = (value) => {
    return (dispatch, getState) => {
        dispatch(changeValue(value))
    }
}

let setText = (text) => {
    return dispatch => {
        dispatch(changeText(text))
    }
}

// 修改标题颜色主题
let changeValue = (titleTheme) => {
    return {
        type: types.CHANGE_SET_SWITCH,
        titleTheme
    }
}

// 设置详情页内容文字颜色
let changeText = (textColor) => {
    return {
        type: types.CHANGE_SET_TEXT,
        textColor
    }
}

export {
    setText,
    setTitle
};

可以看到上述setTitle、setText函数,返回的并不是一个action对象,而是返回了一个函数,这个默认redux是没法处理的,这就需要使用中间件处理了,redux-thunk中间件用于处理返回函数的函数,上面也介绍了redux-thunk的使用基本方式。

  • Reducer

Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。
Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。

函数签名:

(previousState, action) => newState

Reducer必须保持绝对纯净,永远不要在 reducer 里做这些操作:

  • 修改传入参数;
  • 执行有副作用的操作,如 API 请求和路由跳转;
  • 调用非纯函数,如 Date.now() 或 Math.random();

完整的Reducer方法,(setReducer.js):

import * as types from '../actions/actionTypes';

const initialState = {
    titleTheme: false,
    textColor: false
}
// 这里一个技巧是使用 ES6 参数默认值语法 来精简代码
let setReducer = (state = initialState, action) => {

    switch(action.type){
        case types.CHANGE_SET_SWITCH:
            return Object.assign({}, state, {
                titleTheme: action.titleTheme,
            })

        case types.CHANGE_SET_TEXT:
            return Object.assign({}, state, {
                textColor: action.textColor
            })

        default:
            return state;
    }
}

export default setReducer

注意:

  • 不要修改 state。 使用 Object.assign() 新建了一个副本。不能这样使用 Object.assign(state, {
    titleTheme: action.titleTheme,
    }),因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。你也可以开启对ES7提案对象展开运算符的支持, 从而使用 { ...state, ...newState } 达到相同的目的。
  • 在 default 情况下返回旧的 state。遇到未知的 action 时,一定要返回旧的 state

关于拆分Reducer

这里只是举例了一个简单的功能的reducer,如果有不同的功能,需要设计很多reducer方法,注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。

比如我们这个项目的reducer文件结构:

至尊游戏网站 3

image.png

其中rootReducer.js就是一个根reducer文件,使用了Redux 的 combineReducers() 工具类来进行封装整合。

/**
 * rootReducer.js
 * 根reducer
 */
import { combineReducers } from 'redux';
import Home from './homeReducer';
import Class from './classReducer';
import ClassDetial from './classDetialReducer';
import setReducer from './setReducer';
import userReducer from './userReducer';

export default rootReducer = combineReducers({
    Home,
    Class,
    ClassDetial,
    setReducer,
    userReducer,
})

这样根据这个根reducer,可以生成store,请看上文store的创建过程。

Action Creator:

只能通过dispatch action来改变state,这是唯一的方法

action通常的形式是: action = { type: ‘ … ‘, data: data } action一定是有一个type属性的对象

在dispatch任何一个 action 时将所有订阅的监听器都执行,通知它们有state的更新至尊游戏网站 4

redux如何与组件结合

以上部分介绍了Redux 涉及的基本概念,下面介绍与组件交互的工作流程。

梳理一下Redux的工作流程:

至尊游戏网站 5

image

1.首先,用户发出 Action。

store.dispatch(action);

2.Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。

let nextState = todoApp(previousState, action);

3.state一旦有变化,Store就会调用监听函数,组件可以感知state的变化,更新View。

let newState = store.getState();
component.setState(newState);

具体示例1:

至尊游戏网站 6

fsdf.gif

设置页面有个switch按钮,可以全局设置标题栏的主题。

Store:

Redux中只有一个store,store中保存应用的所有状态;判断需要更改的状态分配给reducer去处理。

可以有多个reducer,每个reducer去负责一小部分功能,最终将多个reducer合并为一个根reducer

作用:

  • 维持state树;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器。
代码拆分:

1.设置按钮所在组件:

// SetContainer.js

import React from 'react';
import {connect} from 'react-redux';
import SetPage from '../pages/SetPage';

class SetContainer extends React.Component {
    render() {
        return (
            <SetPage {...this.props} />
        )
    }
}

export default connect((state) => {

    const { setReducer } = state;
    return {
        setReducer
    }

})(SetContainer);

这是容器组件,将SetPage组件与redux结合起来,其中最重要的方法是connect,这个示例中是将setReducer作为属性传给SetPage组件,关于connect的详解,请移步到connect()。

2.SetPage组件

import React, {
    Component
} from 'react';
import {
    StyleSheet,
    Text,
    Image,
    ListView,
    TouchableOpacity,
    View,
    Switch,
    InteractionManager,
} from 'react-native';

import Common from '../common/common';
import Loading from '../common/Loading';
import HeaderView from '../common/HeaderView';

import {setText,setTitle} from '../actions/setAction';

export default class SetPage extends Component {
    constructor(props){
        super(props);
        this.state = {
            switchValue: false,
            textValue: false
        }

        this.onValueChange = this.onValueChange.bind(this);
        this.onTextChange = this.onTextChange.bind(this);
    }

    componentDidMount() {
        // console.log(this.props)
    }

    onValueChange(bool) {
        const { dispatch } = this.props;
        this.setState({
            switchValue: bool
        })
        dispatch(setTitle(bool));
    }

    onTextChange(bool) {
        const { dispatch } = this.props;

        this.setState({
            textValue: bool
        });

        dispatch(setText(bool));
    }

    render() {
        return (
            <View>
                <HeaderView
                  titleView= {'设置'}
                  />

                <View>
                    <View style={styles.itemContainer}>
                        <Text style={{fontSize: 16}}>全局设置标题主题</Text>
                        <Switch 
                            onValueChange={this.onValueChange}
                            value={this.state.switchValue}
                        />
                    </View>

                    <View style={styles.itemContainer}>
                        <Text style={{fontSize: 16}}>设置详情页文字主题</Text>
                        <Switch 
                            onValueChange={this.onTextChange}
                            value={this.state.textValue}
                        />
                    </View>
                </View>
            </View>
        )
    }
}

const styles = StyleSheet.create({
    itemContainer:{
        paddingLeft: 20,
        paddingRight: 20,
        height: 40,
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center'
    }
})

可以只看全局设置标题主题这个方法,设置详情页文字颜色和他同理。这里可以清晰的看到,用户切换主题switch按钮的时候,触发的方法:

dispatch(setTitle(bool));

3.我们查看一下setTitle这个action的源码:

// setAction.js
import * as types from './actionTypes';

let setTitle = (value) => {
    return (dispatch, getState) => {
        dispatch(changeValue(value))
    }
}

let setText = (text) => {
    return dispatch => {
        dispatch(changeText(text))
    }
}

// 修改标题主题
let changeValue = (titleTheme) => {
    return {
        type: types.CHANGE_SET_SWITCH,
        // 这里将titleTheme状态返回
        titleTheme
    }
}

// 设置详情页内容文字主题
let changeText = (textColor) => {
    return {
        type: types.CHANGE_SET_TEXT,
        textColor
    }
}

export {
    setText,
    setTitle
};

4.action只是负责发送事件,并不会返回一个新的state供页面组件调用,它是在reducer中返回的:

// setReducer.js

import * as types from '../actions/actionTypes';

const initialState = {
    titleTheme: false,
    textColor: false
}

let setReducer = (state = initialState, action) => {

    switch(action.type){
        case types.CHANGE_SET_SWITCH:
            return Object.assign({}, state, {
                titleTheme: action.titleTheme,
            })

        case types.CHANGE_SET_TEXT:
            return Object.assign({}, state, {
                textColor: action.textColor
            })

        default:
            return state;
    }
}

export default setReducer

最简单的reducer,就是根据初始值和action对象,返回一个新的state,提供给store,这样,页面里可以从store中获取到这些全局的state,用于更新组件。

我们只是写了怎样发送action和接收action发出newState的,下面来看这个标题组件是怎样和redux结合的。

5.HeaderView组件

/**
 * Created by ljunb on 16/5/8.
 * 导航栏标题
 */
import React from 'react';
import {
    StyleSheet,
    View,
    Text,
    Image,
    TouchableOpacity,
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import Common from '../common/common';
import {connect} from 'react-redux';

class HeaderView extends React.Component {

    constructor(props){
        super(props);

        this.state = {

        }
    }

    render() {
        // 这里,在这里
        const { titleTheme } = this.props.setReducer;
        let NavigationBar = [];

        // 左边图片按钮
        if (this.props.leftIcon != undefined) {
            NavigationBar.push(
                <TouchableOpacity
                    key={'leftIcon'}
                    activeOpacity={0.75}
                    style={styles.leftIcon}
                    onPress={this.props.leftIconAction}
                    >
                    <Icon color="black" size={30} name={this.props.leftIcon}/>
                </TouchableOpacity>
            )
        }

        // 标题
        if (this.props.title != undefined) {
            NavigationBar.push(
                <Text key={'title'} style={styles.title}>{this.props.title}</Text>
            )
        }

        // 自定义标题View
        if (this.props.titleView != undefined) {
            let Component = this.props.titleView;

            NavigationBar.push(
                <Text key={'titleView'} style={[styles.titleView, {color: titleTheme ? '#FFF' : '#000'}]}>{this.props.titleView}</Text>
            )
        }


        return (
            <View style={[styles.navigationBarContainer, {backgroundColor: titleTheme ? 'blue' : '#fff'}]}>
                {NavigationBar}
            </View>
        )
    }
}

const styles = StyleSheet.create({

    navigationBarContainer: {
        marginTop: 20,
        flexDirection: 'row',
        height: 44,
        justifyContent: 'center',
        alignItems: 'center',
        borderBottomColor: '#ccc',
        borderBottomWidth: 0.5,
        backgroundColor: 'white'
    },

    title: {
        fontSize: 15,
        marginLeft: 15,
    },
    titleView: {
        fontSize: 15,
    },
    leftIcon: {
       left: -Common.window.width/2+40,
    },
})


export default connect((state) => {

    const { setReducer } = state;
    return {
        setReducer
    }

})(HeaderView);

这个组件同样利用connect方法绑定了redux,变成了容器组件(container component)。

connect真的很关键,请详细查看官方文档,上面有链接。

其他不相关的内容忽略,核心代码是:

// 拿到全局的state 当有变化的时候,会马上修改
const { titleTheme } = this.props.setReducer;

具体示例2:

至尊游戏网站 7

image.png

利用redux来请求数据、下拉刷新、上拉加载更多。

1.首先,封装action。

import * as types from './actionTypes';
import Util from '../common/utils'; 
// action创建函数,此处是渲染首页的各种图片
export let home = (tag, offest, limit, isLoadMore, isRefreshing, isLoading) => {
    let URL = 'http://api.huaban.com/fm/wallpaper/pins?limit=';
    if (limit) URL += limit;
    offest ? URL += '&max=' + offest : URL += '&max=';
    tag ? URL += '&tag=' + encode_utf8(tag) : URL += '&tag='

    return dispatch => {
        // 分发事件  不修改状态   action是 store 数据的唯一来源。
        dispatch(feachHomeList(isLoadMore, isRefreshing, isLoading));
        return Util.get(URL, (response) => {
            // 请求数据成功后
            dispatch(receiveHomeList(response.pins))
        }, (error) => {
            // 请求失败
            dispatch(receiveHomeList([]));
        });

    }

}

function encode_utf8(s) {
    return encodeURIComponent(s);
}

// 我们约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。
let feachHomeList = (isLoadMore, isRefreshing, isLoading) => {
    return {
        type: types.FETCH_HOME_LIST,
        isLoadMore: isLoadMore,
        isRefreshing: isRefreshing,
        isLoading: isLoading,
    }
}

let receiveHomeList = (homeList) => {
    return {
        type: types.RECEIVE_HOME_LIST,
        homeList: homeList,
    }
}
  • feachHomeList表示正在请求数据的动作;
  • receiveHomeList表示请求数据完后的动作;
  • dispatch(feachHomeList(isLoadMore, isRefreshing, isLoading));表示分发请求数据的动作;

2.封装reducer函数

import * as types from '../actions/actionTypes';
// 设置初始状态
const initialState = {
    HomeList: [],
    isLoading: true,
    isLoadMore: false,
    isRefreshing: false,
};

let homeReducer = (state = initialState, action) => {

    switch (action.type) {
        case types.FETCH_HOME_LIST:
            return Object.assign({}, state, {
                isLoadMore: action.isLoadMore,
                isRefreshing: action.isRefreshing,
                isLoading: action.isLoading
            })

        case types.RECEIVE_HOME_LIST:
            // 如果请求成功后,返回状态给组件更新数据
            return Object.assign({}, state, {
            // 如果是正在加载更多,那么合并数据
                HomeList: state.isLoadMore ? state.HomeList.concat(action.homeList) : action.homeList,
                isRefreshing: false,
                isLoading: false,
            })

        case types.RESET_STATE: // 清除数据
            return Object.assign({},state,{
                HomeList:[],
                isLoading:true,
            })
        default:
            return state;
    }
}

export default homeReducer;
  • 这里并没有处理没有更多数据的情况。

3.容器组件

import React from 'react';
import {connect} from 'react-redux';
import Home from '../pages/Home';

class HomeContainer extends React.Component {
    render() {
        return (
            <Home {...this.props} />
        )
    }
}

export default connect((state) => {
    const { Home } = state;
    return {
        Home
    }
})(HomeContainer);
  • 这里主要是利用connect函数将Home state绑定到Home组件中,并作为它的props;

4.UI组件

  • 组件挂载请求数据
...
let limit = 21;
let offest = '';
let tag = '';
let isLoadMore = false;
let isRefreshing = false;
let isLoading = true;
...
componentDidMount() {
    InteractionManager.runAfterInteractions(() => {
      const {dispatch} = this.props;
      // 触发action 请求数据
      dispatch(home(tag, offest, limit, isLoadMore, isRefreshing, isLoading));
    })
}
...
  • 下拉刷新
// 下拉刷新
  _onRefresh() {
    if (isLoadMore) {
      const {dispatch, Home} = this.props;
      isLoadMore = false;
      isRefreshing = true;
      dispatch(home('', '', limit, isLoadMore, isRefreshing, isLoading));
    }
  }
  • 上拉加载更多
// 上拉加载
  _onEndReach() {

    InteractionManager.runAfterInteractions(() => {
      const {dispatch, Home} = this.props;
      let homeList = Home.HomeList;
      isLoadMore = true;
      isLoading = false;
      isRefreshing = false;
      offest = homeList[homeList.length - 1].seq
      dispatch(home(tag, offest, limit, isLoadMore, isRefreshing, isLoading));
    })

  }
  • render方法
render() {
    // 这里可以拿到Home状态
    const { Home,rowDate } = this.props;
     tag = rowDate;

    let homeList = Home.HomeList;
    let titleName = '最新';
    return (
      <View>
        <HeaderView
          titleView= {titleName}
          leftIcon={tag ? 'angle-left' : null}
          />
        {Home.isLoading ? <Loading /> :
          <ListView
            dataSource={this.state.dataSource.cloneWithRows(homeList) }
            renderRow={this._renderRow}
            contentContainerStyle={styles.list}
            enableEmptySections={true}
            initialListSize= {10}
            onScroll={this._onScroll}
            onEndReached={this._onEndReach.bind(this) }
            onEndReachedThreshold={10}
            renderFooter={this._renderFooter.bind(this) }
            style={styles.listView}
            refreshControl={
              <RefreshControl
                refreshing={Home.isRefreshing}
                onRefresh={this._onRefresh.bind(this) }
                title="正在加载中……"
                color="#ccc"
                />
            }
            />
        }
      </View>

    );

  }

至此,一个简单的Reducer程序完成了,我们稍微总结一下:

  • 整个应用只有一个store,用来保存所有的状态,视图不需要自己维护状态。
  • 视图通过connect函数绑定到store,当store状态变化后,store会通知视图刷新。
  • 触发一个action之后,会经过可能N个reducers处理,最后根reducer会将所有reducers处理之后的状态合并,然后交给store,store再通知视图刷新。

本文的源码地址: 案例Demo

Reducer:

store想要知道一个action触发后如何改变状态,会执行reducer。reducer是纯函数,根reducer拆分为多个小reducer ,每个reducer去处理与自身相关的state更新

注:不直接修改整个应用的状态树,而是将状态树的每一部分进行拷贝并修改拷贝后的变量,然后将这些部分重新组合成一颗新的状态树。应用了数据不可变性(immutable),易于追踪数据改变。此外,还可以增加例如撤销操作等功能。

Views:

容器型组件 Container component 和展示型组件 Presentational component)

建议是只在最顶层组件(如路由操作)里使用 Redux。其余内部组件仅仅是展示性的,所有数据都通过 props 传入。

容器组件 展示组件
Location 最顶层,路由处理 中间和子组件
Aware of Redux
读取数据 从 Redux 获取 state 从 props 获取数据
修改数据 向 Redux 派发 actions 从 props 调用回调函数

Middleware:

中间件是在action被发起之后,到达reducer之前对store.dispatch方法进行扩展,增强其功能。

例如常用的异步action => redux-thunk、redux-promise、redux-logger等

Redux中store、action、views、reducers、middleware等数据流程图如下:至尊游戏网站 8

简化数据流程图:至尊游戏网站 9

Redux核心:

  • 单一数据源,即:整个Web应用,只有一个Store,存储着所有的数据【数据结构嵌套太深,数据访问变得繁琐】,保证整个数据流程是Predictable。
  • 将一个个reducer自上而下一级一级地合并起,最终得到一个rootReducer。 => Redux通过一个个reducer完成了对整个数据源(object tree)的拆解访问和修改。 => Redux通过一个个reducer实现了不可变数据(immutability)。
  • 所有数据都是只读的,不能修改。想要修改只能通过dispatch(action)来改变state。

二、Redux源码解析

前记— redux的源码比较直观简洁~

Redux概念和API,请直接查看官方英文API和官方中文API

Redux目录结构:

|---src   |---applyMiddleware.js   |---bindActionCreators.js   |---combineReducers.js   |---compose.js   |---createStore.js 定义createStore   |---index.js redux主文件,对外暴露了几个核心API

1
2
3
4
5
6
7
|---src
  |---applyMiddleware.js
  |---bindActionCreators.js
  |---combineReducers.js
  |---compose.js
  |---createStore.js 定义createStore
  |---index.js redux主文件,对外暴露了几个核心API

以下分别是各个文件源码解析(带中文批注):

1) combineReducers.js

  • 实质:组合多个分支reducer并返回一个新的reducer,参数也是state和action,进行state的更新处理
  • 初始化:store.getState()的初始值为reducer(initialState, { type: ActionTypes.INIT })
  • Reference:

至尊游戏网站 10

combineReducers() 所做的只是生成一个函数,这个函数来调用一系列reducer,每个reducer根据它们的key来筛选出state中的一部分数据并处理,然后这个生成的函数再将所有reducer的结果合并成一个最终的state对象。

在实际应用中,reducer中对于state的处理是新生成一个state对象(深拷贝):至尊游戏网站 11

因此在combineReducers中每个小reducers的 nextStateForKey !== previousStateForKey 一定为 true => hasChange也一定为true

那么问题来了,为什么要每次都拷贝一个新的state,返回一个新的state呢?
解释:
  1. Reducer 只是一些纯函数,它接收之前的 state 和 action,并返回新的 state。刚开始可能只有一个 reducer,随着应用变大,把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分,因为 reducer 只是函数,可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来处理一些通用任务,如分页器等。因为Reducer是纯函数,因此在reducer内部直接修改state是副作用,而返回新值是纯函数,可靠性增强,便于追踪bug。
  2. 此外由于不可变数据结构总是修改引用,指向同一个数据结构树,而不是直接修改数据,可以保留任意一个历史状态,这样就可以做到react diff从而局部刷新dom,也就是react非常快速的原因。
  3. 因为严格限定函数纯度,所以每个action做了什么和会做什么总是固定的,甚至可以把action存到一个栈里,然后逆推出以前的所有state,即react dev tools的工作原理。再提及react,一般来说操作dom只能通过副作用,然而react的组件都是纯函数,它们总是被动地直接展现store中得内容,也就是说,好的组件,不受外部环境干扰,永远是可靠的,出了bug只能在外面的逻辑层。这样写好纯的virtual dom组件,交给react处理副作用,很好地分离了关注点。

2) applyMiddleware.js

  • 实质:利用中间件来包装store的dispatch方法,如果有多个middleware则需要使用compose函数来组合,从右到左依次执行middleware
  • Reference:applymiddleware方法、middleware介绍

至尊游戏网站 12Reducer有很多很有意思的中间件,可以参考中间件

3) createStore.js

  • 实质:
  1. 若不需要使用中间件,则创建一个包含dispatch、getState、replaceReducer、subscribe四种方法的对象
  2. 若使用中间件,则利用中间件来包装store对象中的dispatch函数来实现更多的功能
  • createStore.js中代码简单易读,很容易理解~

(警告)注:

  1. redux.createStore(reducer, preloadedState, enhancer)如果传入了enhancer函数,则返回 enhancer(createStore)(reducer, preloadedState)如果未传入enhancer函数,则返回一个store对象,如下:

至尊游戏网站 13

  1. store对象对外暴露了dispatch、getState、subscribe、replaceReducer方法
  2. store对象通过getState() 获取内部最新state
  3. preloadedState为 store 的初始状态,如果不传则为undefined
  4. store对象通过reducer来修改内部state值
  5. store对象创建的时候,内部会主动调用dispatch({ type: ActionTypes.INIT })来对内部状态进行初始化。通过断点或者日志打印就可以看到,store对象创建的同时,reducer就会被调用进行初始化。

Reference:)

考虑实际应用中通常使用的中间件thunk和logger:

  • thunk源码:

至尊游戏网站 14

  • logger源码:

至尊游戏网站 15

整个store包装流程:至尊游戏网站 16

4) bindActionCreators.js

  • 实质:将所有的action都用dispatch包装,方便调用
  • Reference:

至尊游戏网站 17

5) compose.js

  • 实质:组合多个Redux的中间件
  • Reference:

至尊游戏网站 18

6) index.js

  • 实质:抛出Redux中几个重要的API函数

三、实例应用Redux

Redux的核心思想:Action、Store、Reducer、UI View配合来实现JS中复杂的状态管理,详细讲解请查看:Redux基础

React+Redux结合应用的工程目录结构如下:

|—actions    addAction.js    reduceAction.js |—components    |—dialog    |—pagination |—constant |—containers    |---add        addContainer.js        add.less    |—reduce        reduceContainer.js        reduce.less |—reducers    addReducer.js    reduceReducer.js |—setting    setting.js |—store    configureStore.js |—entry    index.js |—template    index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|—actions
   addAction.js
   reduceAction.js
|—components
   |—dialog
   |—pagination
|—constant
|—containers
   |---add
       addContainer.js
       add.less
   |—reduce
       reduceContainer.js
       reduce.less
|—reducers
   addReducer.js
   reduceReducer.js
|—setting
   setting.js
|—store
   configureStore.js
|—entry
   index.js
|—template
   index.html

优势:明确代码依赖,减少耦合,降低复杂度~~

下面是实际工程应用中使用react+redux框架进行重构时,总结使用redux时所涉及部分问题&&需要注意的点:

1. Store

在创建新的store即createStore时,需要传入由根Reducer、初始化的state树及应用中间件。

1)根Reducer

重构的工程应用代码很多,不可能让全部state的变更都通过一个reducer来处理。需要拆分为多个小reducer,最后通过combineReducers来将多个小reducer合并为一个根reducer。拆分reducer时,每个reducer负责state中一部分数据,最终将处理后的数据合并成为整个state。注意每个reducer只负责管理全局state中它负责的一部分。每个 reducer的state参数都不同,分别对应它管理的那部分state数据。

实际工程代码重构中以功能来拆分reducer:至尊游戏网站 19

是es6中对象的写法,每个reducer所负责的state可以更改属性名。

2)initialState => State树

设计state结构:在Redux应用中,所有state都被保存在一个单一对象中,其中包括工程全局state,因此对于整个重构工程而言,提前设计state结构显得十分重要。

尽可能把state范式化:大部分程序处理的数据都是嵌套或互相关联的,开发复杂应用时,尽可能将state范式化,不存在嵌套。可参考State范式化

2、Action

唯一触发更改state的入口,通常是dispatch不同的action。

API请求尽量都放在Action中,但发送请求成功中返回数据不同情况尽量在Reducer中进行处理。

  • action.js:

至尊游戏网站 20

  • reducer.js

至尊游戏网站 21

注:

1、如若在请求发送后,需要根据返回数据来判断是否需要发送其他请求或者执行一些非纯函数,那么可以将返回数据不同情况的处理在Action中进行。

2、假设遇到请求错误,需要给用户展示错误原因,如上述reducer代码中errorReason。 需要考虑到是否可能会在提示中增加DOM元素或者一些交互操作,因此最好是将errorReason在action中赋值,最后在reducer中进行数据处理【reducer是纯函数】。

  • action.js

至尊游戏网站 22

3、Reducer

reducer是一个接收旧state和action,返回新state的函数。 (prevState, action) => newState

切记要保持reducer纯净,只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。永远不要在reducer中做这些操作:

a、修改传入参数 b、执行有副作用的操作,如API请求和路由跳转等 c、调用非纯函数,例如Date.now() 或 Math.random()

1
2
3
a、修改传入参数
b、执行有副作用的操作,如API请求和路由跳转等
c、调用非纯函数,例如Date.now() 或 Math.random()

永远不要修改旧state!比如,reducer 里不要使用 Object.assign(state, newData),应该使用Object.assign({}, state, newData)。这样才不会覆盖旧的 state。

  • reducer.js:

至尊游戏网站 23

4、View(Container)

渲染界面

a、mapStateToProps

利用mapStateToProps可以拿到全局state,但是当前页面只需要该页面的所负责部分state数据,因此在给mapStateToProps传参数时,只需要传当前页面所涉及的state。因此在对应的reducer中,接收的旧state也是当前页面所涉及的state值。

b、mapDispatchToProps

在mapDispatchToProps中利用bindActionCreators让store中dispatch页面所有的Action,以props的形式调用对应的action函数。

所有的 dispatch action 均由 container 注入 props 方式实现。

c、connect ( react-redux )

react-redux 提供的 connect() 方法将组件连接到 Redux,将应用中的任何一个组件connect()到Redux Store中。被connect()包装好的组件都可以得到一些方法作为组件的props,并且可以得到全局state中的任何内容。

connect中封装了shouldComponentUpdate方法至尊游戏网站 24

如果state保持不变那么并不会造成重复渲染的问题,内部组件还是使用mapStateToProps方法选择该组件所需要的state。需要注意的是:单独的功能模块不能使用其他模块的state.

d、bind

在constructor中bind所有event handlers => bind方法会在每次render时都重新返回一个指向指定作用域的新函数

  • container.js

至尊游戏网站 25

四、总结

整篇文章主要是源码理解和具体项目应用中整个Redux处理state的流程,我对Redux有了更深层次的理解。

Redux+React已广泛应用,期待在未来的使用过程中,有更多更深刻的理解~

如有错误,欢迎指正 (~ ̄▽ ̄)~

参考链接:

  • redux系列源码解析:
  • redux github:
  • redux剖析:

    1 赞 收藏 评论

至尊游戏网站 26

本文由技术教程发布,转载请注明来源:Redux从入门到跳楼,源码深入分析及运用