首页 > 程序开发 > web前端 > JavaScript >

JavaScript状态容器:Redux介绍和使用

2017-09-13

JavaScript状态容器:Redux介绍和使用。Redux 是 JavaScript 状态容器,提供可预测化的状态管理。可以用来构建一致化的应用,运行与不同的环境(客户端、服务器、原生应用),并且易于测试。

1、简介

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。可以用来构建一致化的应用,运行与不同的环境(客户端、服务器、原生应用),并且易于测试。

Redux 可以用这三个基本原则描述:

(1)单一数据源

整个应用的 state 被存储在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

(2)state 是只读的

唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

(3)使用纯函数来执行修改

为了描述 action 如何改变 state tree,你需要编写 reducers。

2、Action

Action 是把数据从应用(view层、服务器响应、用户输入或其它非 view 的数据)传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。

Action 本质上是 JavaScript 普通对象。约定:action 内部必须使用一个字符串类型的 type 字段表示将要执行的动作。此外,action 对象的结构完全由你自己决定。多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件存放 action。

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

使用单独的模块或文件来定义 action type 常量并不是必须的,甚至根本不需要定义。对于小应用来说,使用字符串做 action type 更方便些。不过在大型应用中把他们显式的定义成常量还是利大于弊。

Action 创建函数

Action 创建函数就是生成 action 的方法。在 Redux 中,action 创建函数只是简单的返回一个 action。将 action 创建函数的结果传给 dispatch() 方法即可发起一次 dispatch 过程。

function addTodo(text){
    return {
        type: ADD_TODO,
        text
    }
}
dispatch(addTodo(text));

或者创建一个被绑定的 action 创建函数来自动调用 dispatch
const boundAddTodo = (text) => dispatch(addTodo(text));
boundAddTodo(text);

store 里能直接通过 store.dispatch() 调用 dispatch() 方法,但是多数情况下使用 react-redux 提供的connect() 帮助器来调用。

3、Reducer

Action 只是描述了有事情发生了这一事实,并没有指明应用如何更新state。而这正是 reducer 要做的事情。

3.1、设计 state 结构

在 Redux 应用中,所有 state 都被报存在一个单一的对象中,建议在写代码前先想一下这个对象的结构。如何才能以最简的形式吧应用的 state 用对象描述出来。通常,这个 state 树还需要存放一些其他数据,以及一些 UI 相关的 state。这样做没有问题,但是尽量把这些数据与 UI 相关的 state 分开。
以 TODO 应用为例,需要保存两种不同的数据:

当前选中的任务过滤条件

完整的任务列表

{
    visibilityFilter: 'SHOW_ALL',
    todos: [
        {
            text: 'consider using redux',
            completed: true
        },
        {
            text: 'keep all state in a single tree',
            completed: false
        }
    ]
}

注意:开发复杂的应用时,不可避免会有一些数据相互引用。建议尽可能的把 state 规范化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同实体或列表间通过 ID 相互引用数据。例如在实际开发中,在 state 里同时存放 todoById: {id -> todo} 和 todos: array 是比较好的方式。

3.2、Action 处理 (reducer)

reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

(previousState, action) => newState

之所以称为 reducer 是因为它将被传递给 Array.prototype.reduce(reducer, ?initialValue) 方法。

注意:要保证 reducer 纯净,在 reducer 里:

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

我们将以指定 state 的初始状态作为开始。 Redux 首次执行时,state 为 undefined,因此我们可借机设置并返回应用的初始 state。

import {VisibilityFilters} from './actions';
const initialState = {
    visibilityFilter: VisibilityFilter.SHOW_ALL,
    todos: []
};

function todoApp(state, action){
    if(typof state === 'undefined'){
        return initialState;
    }
    ....
}
或
function todoApp(state = initialState, action){
    switch(action.type){
        case SET_VISIBILITY_FILTER:
            return Object.assign({}, state, {
                visibilityFilter: action.filter
            });
        ... 
        default: return state

    }
}

3.3、拆分 Reducer

当 Reducer 的代码冗长时,可以将一些独立的 action 处理拆分到一个单独的函数里。

function todoApp(state=initialState, action){
    switch(action.type){
        case SET_VISIBILITY_FILTER: return ...
        case ADD_TODO: return ...
        case TOGGLE_TODO: return ...
        default: return sate;
    }
}

// 拆分
funciton todos(state=[], action){
    switch(action.tye){
        case ADD_DODO: return ...
        case TOGGLE_TODO: return ...
        defaule: return state;
    }
}
function todoApp(state=initialState, action){
    switch(action.type){
        case SET_VISIBILITY_FILTER: return ...
        case ADD_TODO:
        case TOGGLE_TODO: 
            reurn Object.assign({},state,{todos:todos(state.todos, action)});
        default: return state;
    }
}

注意:todos 以及接收 state,但它变成了一个数组!现在 todoApp 只把需要更新的一部分 state 传给 todos 函数,todos 函数自己确定如何更新这部分数据。这就是所谓的 reducer 合成。
通过 reducer 合成,可以开发一个函数做主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 并不需要设置初始化时的完整的 state。初始时,如果传入 undefined ,子 reducer 将负责返回他们的默认值。并且 每个子 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。

function todos(state=[], action){
    switch(action.type){
        .....
    }
}
function visibilityFilter(state = SHOW_ALL, action){
    switch(action.type){
        case SET_VISIBILITY_FILTER: return action.filter;
        default: state;
    }
}
function todoApp(state={}, action){
    return{
        visibilityFilter: visibilityFilter(state.visibilityFilter, action),
        todos: todos(state.todos, action)
    }
}

Redux 提供了 combineReducers() 工具类来做上面 todoApp 做的事情。

import (combineReducers) from 'redux;
const todoApp = combinReducers({visibilityFilter, todos});
// 或为子 reducer 设置别名
const todoApp = combinReducers({a: visibilityFilter, b: todos});
export default todoApp;

4、Store

action 用来描述 发生了什么,使用 reducers 来根据 action 更新 state。而 Store 就是把他们联系到一起的对象。
Store 就是用来维持应用所有 state 树的一个对象。改变 store 内的唯一途径是对它 dispatch 一个 action。要创建它,只需要把根部的 reducering 函数传递给 createStore。

import {createStore} from 'redux';
import todoApp from './reducers';
let store = createStore(todoApp);

其中,createStore 的第二个参数是可选的,用于设置 state 初始状态。这对开发同构应用非常有用,服务器端 Redux 应用的 state 结构可以与客户端保持一致,那么客户端可以将网络接收到的服务器端 state 直接用于本地初始化。

let store = createStore(todoApp, window.STATE_FROM_SERVER)             

Store 有以下职责:

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

注意:Redux 应用只有一个单一的 store 。当需要拆分数据处理逻辑时,应该使用 reducer 组合而不是创建多个 store。

const {createStore, combineReducers} = require('redux');
const ADD_TODO = 'ADD_TODO';
const COMPLETE_TODO = 'COMPLETE_TODO';
function addTodo(text){
  return {
    type:ADD_TODO,
    text
  }
}
function completlTodo(index){
  return {
    type: COMPLETE_TODO,
    index
  }
}

function todos(state=[], action){
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ];
    case COMPLETE_TODO:
      return [
          ...state.slice(0,action.index),
          Object.assign({}, state[action.index],{ completed: true}),
          ...state.slice(action.index+1)
      ];
    default: return state;
  }
}
const todoApp = combineReducers({todos});

let store = createStore(todoApp);
console.log(store.getState());
let unsubscribe = store.subscribe(() => {
  console.log(store.getState());
})
store.dispatch(addTodo("first add"));
store.dispatch(addTodo("second add"));
store.dispatch(completlTodo(1));

unsubscribe();
// 运行结果
{ todos: [] }
{ todos: [ { text: 'first add', completed: false } ] }
{ todos: 
   [ { text: 'first add', completed: false },
     { text: 'second add', completed: false } ] }
{ todos: 
   [ { text: 'first add', completed: false },
     { text: 'second add', completed: true } ] }

5、数据流

Redux 架构的设计核心是 严格的单向数据流。这意味着应用中所有数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。

Redux 应用中数据的生命周期遵循下面 4 个步骤:
(1)调用 store.dispatch(action)
Action 就是一个描述“发生了什么”的普通对象。你可以在任何地方调用 store.dispatch(action),包括组件中,XHR回调中,甚至定时器中。
(2)Redux store 调用 传入的 reducer 函数。
Store 会把两个参数传入 reducer,当前 state 树和 action。
(3)根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
根 reducer 的结构完全由你决定。Redux原生提供 combineReducers() 辅助函数,来把根 Reducer 拆分成多个函数,用于分别处理state树的一个分支。
(4)Redux store 保存了根 Reducer 返回的完整 state 树。
这个新的树就是应用的下一个 state,所有订阅 store.subscribe(listener) 的监听器都将被调用,监听器里可以调用 store.getState()获得当前 state.

6、异步 Action

6.1、Action

当调用异步 API 时,有两个非常重要的时刻:发起请求的时刻,接收到响应的时刻(也可能是超时)。这两个时刻都有可能会更改应用的 state,为此,需要 dispatch 普通的同步 action。一般情况下,每个 API 请求都需要 dispatch 至少三个 action:

一种通知 reducer 请求开始的 action
对于这种 action, reducer 可能会切换一下 state 中的 isFetching 标记。以此来告诉 UI 来显示进度条。 一种通知 reducer 请求成功结束的 action
对于这种 action, reducer 可能会把接收到的新数据合并到 state 中,并重置 isFetching。UI 则会隐藏进度条,并显示接收到的数据。 一种通知 reducer 请求失败的 action
对于这种 action, reducer 可能会重置 isFetching 。或者,有些 reducer 会保存这些失败信息,并在 UI 里显示出来。

为了区分这三种 action,可能在 action 里添加一个专门的 status 字段作为标记位:

{type: 'FETCH_POSTS' }
{type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{type: 'FETCH_POSTS', status: 'success', response: { ...} }

又或者为其设置不同的 type:

{type: 'FETCH_POSTS_REQUEST' }
{type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{type: 'FETCH_POSTS_SUCCESS', response: { ...} }

6.2、异步 Action Creator

如何将同步 action creator 和网络请求结合起来呢?标准的做法是使用 Redux Thunk middleware(并不是唯一方法,还可以使用 Redux-Promise 或 Redux-Promise-middleware等)。要引入 Redux-Thunk 这个专门的库才能使用。通过使用指定的 middleware, action creator 除了返回 action 对象外还可以返回函数。这时候 action creator 就成为 Thunk。
当 action creator 返回函数时,这个函数会被 Redux Thunk middleware 执行。这个函数并不需要保持纯净;它还可以带有副作用,包括异步执行 API 请求。这个函数还可以 dispatch action,就像 dispatch 前面定义的同步 action 一样。

import fetch from 'isomorphic-fetch';   
// 使用方法与同步 action creator 一样,
// store.dispatch(fetchPost('reactjs');
function fetchPost(subeddit){
    // 将 dispatch 方法通过参数传递给函数,
    // 以此来让它自己也能 dispatch action
    return function(dispatch){
        // 首次 dispatch: 更新应用的 state 来通知 API 请求发起了
        dispatch(requestPosts(suberddit));
        // Thunk middleware 调用的函数可以有返回值,
        // 它会被当做 dispatch 方法的返回值
        return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
        .then(response => response.json())
        .then(json => dispatch(receivePosts(subreddit,json)));          
    }
}

function fetchPostIfNeeded(subreddit){
    // 注意这个函数也接受了 getState 方法,
    // 可以用来选择接下来如果 dispatch
    // 当缓存的值是可用时
    // 可减少网络请求
    return (dispatch, getState) => {
        if(shouleFetchPosts(getState(), subreddit)){
            // 在 Thunk 里 dispatch 另一个 Thunk
            return dispatch(fecthPosts(subreddit));
        } else {
            // 告诉代码无需等待
            return Promise.resolve();
        }
    }
}

// 使用applyMiddleware() 引入 Redux-Thunk-middleware。
import thunkMiddleware from 'redux-thunk'
const store = creatStore(
    rootReducer,
    applyMiddleware(thunkMiddleware)
);
store.dispatch(fetchPosts('reactjs')
    .then(() => console.log(store.getState()))

上面代码中,由于目前大多数浏览器原生还不支持 fetch ,所以使用了 isomorphic-fetch。

参考Redux 中文文档

相关文章
最新文章
热点推荐