redux学习笔记

来源:互联网 发布:帝国cms 模板 编辑:程序博客网 时间:2024/05/16 11:37

概述

Redux提供一种状态管理的方式。

有以下三个要点:

  • 应用中所有的state都以一个对象树的形式储存在一个单一的store中。
  • 唯一改变state的办法就是触发action(一个描述发生什么的对象)
  • 为了描述action如何改变state树,需要编写reducers

一般我们使用一个普通对象来描述应用的state,如:

{  todos: [{    text: 'Eat food',    completed: true  }, {    text: 'Exercise',    completed: false  }],  visibilityFilter: 'SHOW_COMPLETED'}

这个应用的state包含两个属性,todos与visibilityFilter,其中前者为一个对象数组,后者为一个简单字符串。

要想更新state中的数据,需要发起一个action。 action也是一个普通的js对象,用来描述发生了什么:

{ type: 'ADD_TODO', text: 'Go to swimming pool' }{ type: 'TOGGLE_TODO', index: 1 }{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

上面展示了三个action,type属性是必须的,描述了这个action具体是什么动作;后面的其他属性是任意的,是要传递的数据。

最后,为了描述每一个action发生时是如何改变state的, 我们需要编写一些函数,即reducer。reducer将action与state串起来。
reducer就是一个接收action与state为参数,并返回新state的函数:

function todos(state = [], action) {  switch (action.type) {  case 'ADD_TODO':    return state.concat([{ text: action.text, completed: false }]);  case 'TOGGLE_TODO':    return state.map((todo, index) =>      action.index === index ?        { text: todo.text, completed: !todo.completed } :        todo   )  default:    return state;  }}function visibilityFilter(state = 'SHOW_ALL', action) {  if (action.type === 'SET_VISIBILITY_FILTER') {    return action.filter;  } else {    return state;  }}

上面是两个reducer,分别对应更改state中的todos和visibilityFilter。

最后是一个总的reducer来调用上面两个reducer,进而来管理整个应用的state:

function todoApp(state = {}, action) {  return {    todos: todos(state.todos, action),    visibilityFilter: visibilityFilter(state.visibilityFilter, action)  };}

这差不多就是 Redux 思想的全部。我们没有使用任何 Redux 的 API。Redux 里有一些工具来简化这种模式,但是主要的想法是如何根据这些 action 对象来更新 state。

三大原则

单一数据源

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

state是只读的

唯一改变 state 的方法就是触发 action

使用纯函数来执行修改

为了描述action如何修改state,需要编写reducers。
reducer是纯函数, 它接受之前和state和action,并返回新的state。

一般我们会把一个应用的reducer拆分成多个小的reducers,分别独立地操作state tree的不同部分。

Action

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

Action 本质上是 JavaScript 普通对象。

  • action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。
  • 除了 type 字段外,action 对象的结构完全由你自己决定。但是我们应该尽量减少在 action 中传递的数据。

Action创建函数

Action创建函数就是生成action对象的函数。
我们想要触发同一个action时,一般需要传递不同的数据,action创建函数就允许我们传入不同的数据来生成action。

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

针对ADD_TODO这一action,我们每次要触发次action时都要传入不同的text,此处的action创建函数就帮助我们方便地生成action。

action创建函数可以是异步非纯函数,你可以在其中处理ajax响应。

dispatch

action描述发生了什么动作,配合reducer后具体的表现就是这个action导致了state的变化。因此我们说Action是把数据从应用传到store的有效载荷,它是store数据的唯一来源。
当我们触发一个action时就会更改相应的state,就会把数据传到store。一般会通过store.dispatch()dispatch()来进行触发:

const action = {  type: ADD_TODO,  text: 'Build my first Redux app'};store.dispatch(action);

一般我们都是将action创建函数的结果传给dispatch(),这样即可发起一次dispatch过程:

dispatch(addTodo('Build my first Redux app'))

异步action

一般都是在action创建函数中发起异步请求来获取数据。

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

  • 通知 reducer 请求开始的 action
  • 通知 reducer 请求成功的 action:对于这种 action,reducer 可能会把接收到的新数据合并到 state 中
  • 通知 reducer 请求失败的 action

针对这三种action,一般需要在type中加以区分,如:

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

我们可以书写几个同步action创建函数用于创建这几个action。
那么,如何把这些同步 action 创建函数和网络请求结合起来呢?

标准的做法是使用Redux Thunk 中间件

中间件

中间件 让你在每个 action 对象 dispatch 出去之前,注入一个自定义的逻辑来解释你的 action 对象。异步 action 是中间件的最常见用例。

中间件让我们能写表达更清晰的、潜在的异步 action creators。 它允许我们 dispatch 普通对象之外的东西,并且解释它们的值。比如,中间件能 “捕捉” 到已经 dispatch 的 Promises 并把他们变为一对请求和成功/失败的 action.

在组件中使用中间件需要在createStore时做相关配置:

const store = createStore(  reducer,  applyMiddleware(    middleware1,    middleware2,    ……  ))

applyMiddleware函数将接收到的中间件组成为一个数组,依次执行。


  • redux thunk middleware

通过使用thunkMiddleware ,action 创建函数除了返回 action 对象外还可以返回函数,并且你可以在这个返回的函数中取得dispatch,所以你也能写一个多次分发的 action creator 。

例子:

export function fetchPosts(subreddit) {  //注意!这里返回一个函数!  return function (dispatch) {    // 首次 dispatch了一个同步action creater,which代表了请求开始    dispatch(requestPosts(subreddit))    //异步请求    fetch(`http://www.subreddit.com/r/${subreddit}.json`)      then(      response => dispatch({        type: 'LOAD_POSTS_SUCCESS',        userId,        response      }),      error => dispatch({        type: 'LOAD_POSTS_FAILURE',        userId,        error      })    );  }}

如此一来我们只需通过dispatch(fetchPosts('reactjs'))就可以完成一个异步过程。

其实不光是异步操作要使用到 redux-thunk中间件,一些其他的情况也同样适用。
比如针对一个添加todo的action:

function addTodoWithoutCheck(text) {  return {    type: 'ADD_TODO',    text  };}

可能系统要求只有三个todo,那么我们可以利用redux-thunk:

function addTodo(text){//返回一个函数    return function(dispatch,getState){        if (getState().todos.length === 3) {          // 提前退出          return;        }else{            //触发真正的添加todo的action            dispatch(addTodoWithoutCheck(text));        }    }}

总结

  • redux-thunk允许action creator返回函数, redux-thunk会执行这个返回的函数。
  • 在action creator中可以dispatch其他的action,这就允许我们在请求开始和接收到响应时触发相应的action。

  • redux-promise-middleware

另一个处理异步的方法就是使用redux-promise-middleware中间件,它允许action creator返回一个Promise对象。

import promiseMiddleware from 'redux-promise-middleware';let store = createStore(reducer,initState,applyMiddleware(  promiseMiddleware()));

要使用这个中间件,就要dispatch一个payload属性为promise的action对象。 这样的简单action creator就像:

const foo = () => ({  type: 'FOO',  payload: {    promise: new Promise()  }});

这里我们的action type为‘FOO’。

  1. 当这个action被触发时,会立即dispatch一个type为‘FOO_PENDING’的action(就是在原有type名称后面加上_PENDING)
  2. 当promise settled之后,会dispatch第二个action。
    • 若promise状态为resolved,则会disptach‘FOO_FULFILLED’action
    • 若promise状态为rejected,则会dispatch‘FOO_REJECTED’action

redux-promise-middleware总会触发这两个action之一。

因此整个过程就是:我们只要触发一个返回Promise对象的action,然后在相应的reducer中分别处理 XXX_PENDING、 XXX_FULFILLEDh和XXX_REJECTED这三个action即可。

总结

像 redux-thunk 或 redux-promise 这样支持异步的 middleware 都包装了 store 的 dispatch() 方法,以此来让你 dispatch 一些除了 action 以外的其他内容,例如:函数或者 Promise。

Reducer

reducer用于描述一个action发生时如何更新state。

纯函数

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

(prevState,action)=>newState

reducer一定要是一个纯函数,永远不要在其中执行如下操作:

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

reducer是纯函数表示:只要传入参数相同,返回的计算得到的下一个state就一定相同。

reducer可以使用ES6的参数默认值语法来为state设置初始值:

function todoApp(state = {}, action) {    ……}

现在我们要处理SET_VISIBILITY_FILTER这个action:

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

注意:

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

  • 在 default 情况下返回旧的 state。遇到未知的 action 时,一定要返回旧的 state。

reducer拆分与合并

上面我们写的reducer实际上只针对state中的visibilityFilter部分,就像这样,我们可以编写多个reducer,分别针对state不同的部分,最后再编写一个总的reducer将所有reducer合并。

//子reducer todosfunction todos(state = [], action) {  switch (action.type) {    case ADD_TODO:      return [        ...state,        {          text: action.text,          completed: false        }      ]    case TOGGLE_TODO:      return state.map((todo, index) => {        if (index === action.index) {          return Object.assign({}, todo, {            completed: !todo.completed          })        }        return todo      })    default:      return state  }}//子reducer visibilityFilterfunction visibilityFilter(state = SHOW_ALL, action) {  switch (action.type) {    case SET_VISIBILITY_FILTER:      return action.filter    default:      return state  }}//主reducerfunction todoApp(state = {},action){    return {        visibilityFilter:visibilityFilter(state.visibilityFilter,action);        todos:todos(state.todos,action)    }}

这个主reducer返回一个新的state,state的visibilityFilter部分是子reducer visibilityFilter执行返回的结果,todos部分同理。在调用子reducer时,分别将state的对应部分传入子reducer中。即:

每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。

这样在每个子reducer中,设置默认值的时候也只需设置其负责的部分即可,如todos是一个空数组,visibilityFilter则是一个字符串。

combineReducers()

Redux提供了 combineReducers()来合并reducer,做了上面todoApp函数所做的事情。
可以使用combineReducers这样重构todoApp函数:

const todoApp = combineReducers({    todos,    visibilityFilter})

这种写法和上面的todoApp写法完全等价。

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

这里也使用到了ES6的对象简写方式。也可以选择设置不同的 key,或者调用不同的函数:

const reducer = combineReducers({  a: doSomethingWithA,  b: processB,  c: c})

注意:
你可以在任何级别的 reducer 中使用 combineReducer,不仅仅是在创建根 reducer 的时候。在不同的地方有多个组合的 reducer 是非常常见的,他们组合到一起来创建根 reducer。

Store

一个应用只有一个Store,其有以下作用:

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

createStore( )

根据已有的reducer创建store。
我们将之前使用combineReducers()合并得到的reducer传入createStore中:

let store = createStore(todoApp)

createStore() 的第二个参数是可选的, 用于设置 state 初始状态:

let store = createStore(todoApp, initialState)

发起Actions

store通过dispatch方法来发起action,用于更新state:

store.dispatch(addTodo('Learn about actions'))store.dispatch(addTodo('Learn about reducers'))store.dispatch(addTodo('Learn about store'))store.dispatch(toggleTodo(0))store.dispatch(toggleTodo(1))

搭配React

Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。我们之前写的代码也没有一处使用到了React。

尽管如此,Redux 还是和 React 这类框架搭配起来用最好,因为这类框架允许你以 state 函数的形式来描述界面。

在react中使用redux需要安装react-redux这个库。

容器组件和展示组件

  • 展示组件

    • 关注UI呈现
    • 不使用Redux
    • 仅通过props接收数据与回调函数
    • 一般手写生成
  • 容器组件:

    • 关注运作(数据获取,状态更新)
    • 直接使用Redux
    • 数据来源为Redux state
    • 通过派发actions来修改数据
    • 通常由 React Redux 自动生成

将组件分为这两类的好处:

  • 分离关注点。
    编写展示组件不用考虑与Redux数据交互,只需关注UI呈现,有关数据交互的部分都交给容器组件来完成

  • 提高复用性
    通过不同的容器组建,可以使用同一展示组件去展示完全不同的数据源

这里还有一组概念:有状态组件与无状态组件:那些在组件内部使用state的组件就是有状态组件。但这个概念与容器组件和展示组件没有必然的联系,展示组件也可以拥有自己的state,容器组件也可以是无状态的。

大部分的组件都应该是展示型的,但一般需要少数的几个容器组件把它们和 Redux store 连接起来。

connect()

技术上讲,容器组件就是使用 store.subscribe() 从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。

但一般我们都使用 React Redux 库的 connect() 方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。

使用connect之前,需要先定义两个函数,来指定如何把当前Redux store的state和action映射到展示组件的props中:

  • mapStateToProps:指定如何把state映射到props中
const mapStateToProps=(state)=>{    return {        todos:state.todos    }}

这样组件就可以通过props获取state的todos数据

  • mapDispatchToProps:接收dispatch方法并注入到组件的props中的回调函数
const mapDispatchtiProps = (dispatch)=>{    return {        onClick:(id)=>{            dispatch(toggleTodo(id));        }    }}

这样我们通过props中的onClick,就可以dispatch一个action。

最后,使用connect创建容器组件:

const todoList = connect(    mapStateToProps,    mapDispatchToProps)(todoApp);

最后括号中传入的参数即为展示组件,我们为展示组件创建了一个容器组建todoList。

Provider

所有容器组件都可以访问 Redux store,一种方式是把它以 props 的形式传入到所有容器组件中,但这太麻烦了。

建议的方式是使用指定的 React Redux 组件 <Provider>让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。

let store = createStore(reducer);render(    <Provider store={store}>        <App />    </Provider>,    document.getElementById('root'));
原创粉丝点击