Redux

Redux - 为JavaScrpit应用提供可预测的状态容器

2017-06-11 | 阅读

redux

中文文档

官方文档

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。Redux 由Flux演变而来,这里先介绍一下Flux

Flux

Facebook提出了Flux框架用于管理数据流。Flux是一个相当宽松的概念框架,不同于其它大多数MVC框架的双向数据绑定,Flux提倡的是单向数据流动,即永远只有从模型到视图的数据流动。

Flux引入了Dispatcher和Action的概念:Dispatcher是一个全局的分发器负责接收Action,而Store可以在Dispatcher上监听到Action并做出相应的操作。简单的理解可以认为类似于全局的消息发布订阅模型。Action可以来自于用户的某个界面操作,比如点击提交按钮;也可以来自服务器端的某个数据更新。当数据模型发生变化时,就触发刷新整个界面

reduxflux的不同在于,redux没有dispatch,且不支持多个store.

简介

由于JavaScript单页用用开发日趋复杂,JavaScript需要管理非常多的状态,管理大量且不断变化的state非常困难,Redux试图让state的变化可预测。Redux拥有以下三大原则 :

  • 单一数据源
  • State是只读的
  • 使用纯函数来执行修改

Redux 拥有三个基础组件,storeactionaction

Action

Action是通知,一般由View发出,告知Store应该发生变化。Action是一个对象,其中type属性是必须的,可以附带一些负载属性 ,如 :

const action = {
  type: 'ADD_TODO',
  text: 'Learn Redux'
};

通过store.dispatch() 来发送Action.

Reducer

Store收到 Action后,必须计算出一个新的State,这个计算过程称为ReducerReducer是一个函数,接受Action和当前状态作为参数,返回一个新的State,如:

const reducer = (state = 0, action) => {
  switch (action.type) {
    case 'ADD':
      return state + action.payload;
    default: 
      return state;
  }
};

const state = createStore(reducer);

Reducer是一个纯函数,即只要拥有相同的输入,必得到相同的输出,所以不要在reducer中做以下操作 :

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

保证Reducer的纯函数,避免副作用.

还需要注意,

1.不要修改state,而是创建一个副本 ,如 :

{ ...state, ...newState }

2.在default情况下,返回旧的state.

示例代码:

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    default:
      return state
  }
}

Store

Store负责联系actionreducer,一个应用只能有一个StoreStore有以下职责:

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

使用createStore来创建Store :

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

// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

// 发起一系列 action
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))

使用combineReducers拆分reducer

当应用成长到足够复杂时,我们需要将reducer函数分散到多个实现中,每个单独的reducer函数,都管理着状态中独立的部分。

combinReducers这个函数,产生了一个能够将多个reduce函数的结果整理成一个状态的总reduce函数,而这个函数用于传递给createStore函数进行初始化。

生成的函数,处理action时,会调用每一个子reducer,然后将结果汇集到一个单独的状态对象中。 这就要求对子reducer进行一些限制,以避免异常。 而生成的最终状态对象的结构,与子reducers的key相对应。

举例 ,如果我们这样创建:

combineReducers({todos:myTodoReducer,counter:myCounterReducer})

而最终生成的状态是这样的 :

{todos:{},counter:{}}

对于combineReducers函数,每次处理时都会调用所有的子reducer函数,所以必须满足 :

  • 如果action没有识别,则默认处理必须要返回当前状态
  • 不能返回undefined

Redux数据流

Redux应用中的生命周期遵循下面4个步骤:

1.调用store.dispatch(action)

发送事件,可以在任何时候调用store.dispatch.

2. store调用传入的reducer函数

Store 会把两个参数传入 reducer: 当前的 state 树和 action。

3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树

4. store储存根reducer返回的完整state树

新生成的树,就是下一个state,所有订阅的监听器都将被调用。可以通过store.getState来获取当前状态

使用react-redux

使用react-redux来与React结合。

Middleware

Middleware是action被发起之后,到达reducer之前的扩展点,可以扩展添加日志记录、崩溃报告、调用异步接口等功能。如添加一个日志功能的Middleware :

const logger = store => dispatch => action => {
  console.log('dispatching', action)
  let result = dispatch(action)
  console.log('next state', store.getState())
  return result
}
const store = createStore( reducer ,applyMiddleware(logger));

这个logger函数,很奇特,我们要研究一下applyMiddleware,该函数大致实现如下:

function applyMiddleware(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse()

  let dispatch = store.dispatch;
  middlewares.forEach(middleware =>
    dispatch = middleware(store)(dispatch)
  )

  return Object.assign({}, store, { dispatch })
}

所以 , 传入一个store参数和dispatch参数后,获得了一个新的dispatch函数。虽然这个logger函数看起来很奇怪,但是原理还是比较简单的。

applyMiddleware函数中Middleware的顺序决定了执行时的顺序,为从右向左依次执行。

异步Action

store.dispatch(),我们可以传递一个返回值为action的函数,这种函数,我们可以成为action creator 。当我们有异步操作时,我们希望一个支持异步操作的action creator , 将网路请求等异步操作封装到action creator中 ,于是我们引入redux-thunk这个库 :

import thunkMiddleware from 'redux-thunk'
const store = createStore(
  rootReducer,
  applyMiddleware(
    thunkMiddleware, // 允许我们 dispatch() 函数
  )
)

如下形式创建异步action creator

export function fetchPosts(subreddit) {
  return function (dispatch) {
    // 首次 dispatch:更新应用的 state 来通知
    // API 请求发起了。
    dispatch(requestPosts(subreddit))
    // thunk middleware 调用的函数可以有返回值,
    // 它会被当作 dispatch 方法的返回值传递。
    return (`http://www.subreddit.com/r/${subreddit}.json`)
      .then(response => response.json())
      .then(json =>
        // 可以多次 dispatch!
        // 这里,使用 API 请求结果来更新应用的 state。
        dispatch(receivePosts(subreddit, json))
      )

      // 在实际应用中,还需要
      // 捕获网络请求的异常。
  }
}

这样,我们在UI组建中,直接dispatch这个action,以执行网络请求 :

store.dispatch(fetchPosts('reactjs')).then(() =>
  console.log(store.getState())
)

thunkMiddleware的实现大致如下:

const thunk = store => next => action =>
  typeof action === 'function' ?
    action(store.dispatch, store.getState) :
    next(action)

如果dispatch函数的参数为函数,也就是如上面的fetchPosts时,会执行fetchPosts的返回值函数,也就是最终生成一个Pormise对象。