[关闭]
@yacent 2017-11-06T00:59:00.000000Z 字数 13676 阅读 1777

Redux学习笔记

React学习笔记


设计理念

主要通过 Reducer,根据 action 来更新 state


三大原则


Action

Action只是指明了有事情要发生,但并未指明如何更新state

区分 ActionAction创建方法

Action只是声明了 actionTypes,而 action创建方法则返回具体的调用接口


Reducer

(state, action) => return new state

  1. const app = combineReducers({
  2. todo: todos,
  3. visibilityFilter
  4. })
  5. // 那么 store.getState()应该如下
  6. {
  7. todo: {...},
  8. visibilityFilter: 'all'
  9. }

combineReducers(reducers)

combineReducers 辅助函数的作用,是把一个由多个不同的Reducer函数作为 value的object,合并成一个最终的reducer函数,然后就可以对这个reducer调用 createStore

合并后的reducer可以调用各个子reducer,并把他们的结果合并成一个 state对象。 state对象的结构由传入的多个reducer的key决定。(即 state的结构,与 combineReducer当中的结构一样)

使用 combineReducers的好处是,可以将state当中的结构进行划分,使得每一个key值都只负责某一模块,结构更加清晰。

tips:

  1. combineReducers只是起辅助作用,你可以自行实现不同功能的 combineReducers
  2. 在reducer层级的任何一级都可以调用 combineReducers。并不是一定要在最外层使用,实际上,可以把一些复杂的reducer拆分成单独的孙子级reducer,甚至更多层。

高阶Reducer的力量

高阶reducer的力量
具体请看上面的链接

Store初始化state和combineReducers

阅读链接
初始化 Redux app 有如下的两种方式

但是,如果同时在自己的 reducer 和createStore当中都传入了值,那么 store的初始值应该是什么呢?
考虑下面一种最简单的情况

  1. function counter(state = 0, action) {
  2. switch (action.type) {
  3. case 'INCREAMENT': return state + 1;
  4. default: return state;
  5. }
  6. }
  7. import { createStore } from 'redux'
  8. let store = createStore(counter);
  9. console.log(store.getState()); // 0

因为 createStore传入的值为 undefined,在 counter reducers当中,由于接收到的state为undefined,根据es6的函数默认值的调用方法,可以知道,当传入的值为undefined的时候,会使用默认值 0

如果是如下的形式

  1. let store = createStore(counter, 42);
  2. console.log(store.getState()); // 42

这次因为传入的是42,而不是 undefined,所以state的值将会是 42;

如果 createStore 当中传入了第二个参数,那么其初始值的优先级会高于 reducer自己设置的默认值

State 键的名称与 combineReducers

阅读链接
总的一句,createReducers当中的 key值的组合,即 state的结构


Store

api

createStore()

因为每个 reducer当中都有 default 的处理,故可以初始化 store 的结构


严格的单向数据流

1) 所有数据遵循相同的声明周期
2) redux应用中数据的声明周期,四个步骤


容器组件 && 展示组件

容器组件

通过 给组件使用connect(mapStateToProps, mapDispatchToProps),来限定了组件当中所能展示的完整的state树当中部分state

使用 provider 来包裹容器,将state向下传递

  1. let store = createStore(todoApp)
  2. <Provider store={store}>
  3. <App />
  4. </Provider>

展示组件

容器组件:描述如何运行(数据的获取,状态的更新等)
展示组件:描述如何进行展示数据


高级

异步Action

使用中间件,进行异步的操作,以前的 action创建方法只能返回一个对象,而现在可以返回一个函数,由中间件对函数进行数据的处理

  1. function requestPosts(subreddit) {
  2. return {
  3. type: REQUEST_POSTS,
  4. subreddit
  5. }
  6. }
  7. function fetchPosts(subreddit) {
  8. return (dispatch, getState) => {
  9. dispatch(requestPost(subreddit))
  10. return fetch('***')
  11. .then(response => response.json())
  12. .then(json => dispatch(receivePost(subreddit, json)))
  13. }
  14. }
  15. // index.js
  16. const store = createStore(
  17. rootReducer,
  18. applyMiddleware(
  19. thunkMiddleware
  20. )
  21. )

Middleware

在express 或者 Koa等框架当中,Middleware 是指被嵌入到框架中,接收请求到产生响应过程中的代码

而在 redux 当中,则是指,action发起之后,到达 Reducer之前的扩展部分
常用的操作如下:


搭配 React Router

如果想要将 "#" 从URL当中移除,那么需要从React Router 导入 browserHistroy 来实现

  1. <Router history={browserHistroy}>
  2. <Route path="/(:filter)" component={App} />
  3. </Router>

前提:只要你不需要兼容古老的浏览器,比如IE9,你都可以使用 browserHistory


技巧

使用对象展开运算符(Object Spread Operator)

  1. function todoApp(state = initialState, action) {
  2. switch (action.type) {
  3. case SET_VISIBILITY_FILTER:
  4. return Object.assign({}, state, {
  5. visibilityFilter: action.filter
  6. })
  7. default:
  8. return state
  9. }
  10. }

由于 redux当中的 state是只读的,只能通过action去改变,并且reducer的原理是 (state, action)=> new state,返回一个新的state,无副作用,不会改变原来的state的状态。
但每次都使用 Object.assign(),会比较麻烦,可以尝试使用 ...运算符,

  1. function todoApp(state = initialState, action) {
  2. switch (action.type) {
  3. case SET_VISIBILITY_FILTER:
  4. return { ...state, visibilityFilter: action.filter }
  5. default:
  6. return state
  7. }
  8. }

对象展开运算符目前仅仅在 ES7当中能够使用,需要使用Babel进行

缩减样板代码

Actions

  1. const ADD_TODO = 'ADD_TODO';
  2. const REMOVE_TODO = 'REMOVE_TODO';

好处:

Action Creator 生成器

写简单的action creator很容易让人厌烦,且会产生较多的多余样板代码,如下

  1. export function addTodo(text) {
  2. return {
  3. type: 'ADD_TODO',
  4. text
  5. }
  6. }
  7. export function editTodo(id, text) {
  8. return {
  9. type: 'EDIT_TODO',
  10. id,
  11. text
  12. }
  13. }
  14. export function removeTodo(id) {
  15. return {
  16. type: 'REMOVE_TODO',
  17. id
  18. }
  19. }

可以写一个用于生成 action creator 的函数

  1. function makeActionCreator(type, ...argNames) {
  2. return function(...args) {
  3. let action = { type }
  4. argNames.forEach((arg, index) => {
  5. action[argNames[index]] = args[index]
  6. })
  7. return action;
  8. }
  9. }
  10. const ADD_TODO = 'ADD_TODO';
  11. makeActionCreator(ADD_TODO, 'todo')

服务端渲染

在返回完整的html时,还需要返回初始数据
把数据发送到客户端,需要以下步骤:

在客户端,使用服务器返回的 state 创建并初始化一个全新的 Redux store。
Redux 在服务端惟一要做的事情就是,提供应用所需的初始 state。

在服务端,即用 ReactDOM.renderToString 来将html输出

编写测试

常用的测试工具:
EnzymeReact Test Utils浅渲染

计算衍生数据

当因为太多的衍生计算和重复渲染导致出现性能问题时,大多数的Redux项目会开始使用 Reselect

实现撤销重做

使用Redux能够轻而易举地实现撤销历史,原因如下

追溯历史记录的state的结构,可以设计成如下形式

  1. {
  2. past: Array<T>,
  3. present: T,
  4. future: Array<T>
  5. }

设计Reducer

  1. const initialState = {
  2. past: [],
  3. present: null, // (?) 我们如何初始化当前状态?
  4. future: []
  5. }
  6. function undoable(state = initialState, action) {
  7. const { past, present, future } = state;
  8. switch (action.type) {
  9. case 'UNDO':
  10. const previous = past[past.length - 1]
  11. const newPast = past.slice(0, past.length - 1)
  12. return {
  13. past: newPast,
  14. present: previous,
  15. future: [ present, ...future ]
  16. }
  17. case 'REDO':
  18. const next = future[0]
  19. const newFuture = future.slice(1)
  20. return {
  21. past: [ ...past, present ],
  22. present: next,
  23. future: newFuture
  24. }
  25. default:
  26. // (?) 我们如何处理其他 action?
  27. return state
  28. }
  29. }

但是使用Reducer忽略了三个重要的问题

初识Reducer Enhancers

higher order function,即接收一个Reducer 返回一个新的 Reducer。combineReducers()也是 reducer enhancer,其为典型的例子

enhance reducer 可以用如下的方式进行展示

  1. function doNothingWith(reducer) {
  2. return function (state, action) {
  3. // do something
  4. return reducer(state, action)
  5. }
  6. }

修改后的 redoundo 如下

  1. function undoable(reducer) {
  2. // 以一个空的 action 调用 reducer 来产生初始的 state
  3. const initialState = {
  4. past: [],
  5. present: reducer(undefined, {}),
  6. future: []
  7. }
  8. // 返回一个可以执行撤销和重做的新的reducer
  9. return function (state = initialState, action) {
  10. const { past, present, future } = state;
  11. switch (action.type) {
  12. case 'UNDO':
  13. const previous = past[past.length - 1]
  14. const newPast = past.slice(0, past.length - 1)
  15. return {
  16. past: newPast,
  17. present: previous,
  18. future: [ present, ...future ]
  19. }
  20. case 'REDO':
  21. const next = future[0]
  22. const newFuture = future.slice(1)
  23. return {
  24. past: [ ...past, present ],
  25. present: next,
  26. future: newFuture
  27. }
  28. default:
  29. // 将其他 action 委托给原始的 reducer 处理
  30. const newPresent = reducer(present, action);
  31. if (present === newPresent) {
  32. return state
  33. }
  34. return {
  35. past: [ ...past, present ],
  36. present: newPresent,
  37. future: []
  38. }
  39. }
  40. }
  41. }

当然,现在社区已经实现了 redux-undo 的库

子应用隔离

即有点类似于ccs当中的前端代码库的实现,各个子应用当中的状态是相互隔离的

考虑一下这样的场景,有一个大应用,包含了很多的子应用

  1. import React, { Component } from 'react'
  2. import SubApp from './subapp'
  3. class BigApp extends React.Component {
  4. render() {
  5. return (
  6. <div>
  7. <SubApp />
  8. <SubApp />
  9. <SubApp />
  10. </div>
  11. )
  12. }
  13. }

这些 <SubApp> 是完全独立的。它们不会共享数据或action,也互相不可见且不需要通信。
如果每个子应用之间不需要共享数据,推荐使用这种独立的方式,来进行状态的读取与写入,即每个实例都有其自己的store,它们彼此是不可见的。

组织Reducer

深入理解 Reducer,能够明白各种 Reducer当中的实现方式,以及高级用法。

Reducer基础概念

一个 Redux reducer函数需要具备

Redux Reducer基础

核心概念

具体的阅读学习材料可以看 Reducer基础概念

纯函数和副作用

核心概念

推荐阅读列表

具体请点

不可变数据的管理

核心概念

范式化数据

核心概念

Reducer基础结构

Reducer的基本结构

首先要明确的是,整个应用只有一个单一的reducer函数,这个函数会被传给 createStore 的第一个参数。

一个单一的reducer最终需要做如下的几件事:

典型的Redux reducer的基本结构

  1. function counter(state = 0, action) {
  2. switch (action.type) {
  3. case 'INCREAMENT':
  4. return state + 1;
  5. case 'DECREMENT':
  6. return state - 1;
  7. default:
  8. return state;
  9. }
  10. }
State的基本结构

Redux 鼓励你根据要管理的数据来思考你的应用程序。数据就是你应用的 state,state 的结构和组织方式通常会称为 "shape"。在你组织 reducer 的逻辑时,state 的 shape 通常扮演一个重要的角色。

通常,state的顶层的状态树是一个普通的 JavaScript对象
大多数应用会处理多种数据类型,通常可以分为如下的三类:

Store代表着应用核心,因此应该用 域数据应用状态数据来定义State,而不是用UI状态。

一个典型的应用的state大致如下:

  1. domainData1: {},
  2. domainData2: {},
  3. appState1: {},
  4. appState2: {}.
  5. ui: {
  6. uiState1: {},
  7. uiState2: {},
  8. }

Reducer逻辑拆分

对于任何一个有意义的应用来说,将所有的更新逻辑都放入到单个 reducer 函数中都将会让程序变得不可维护。虽然说对于一个函数应该有多长没有准确的规定,但一般来讲,函数应该比较短,并且只做一件特定的事。因此,把很长的,同时负责很多事的代码拆分成容易理解的小片段是一个很好的编程方式。

因为 Redux reducer 也仅仅是一个函数,上面的概念也适用。你可以将 reducer 中的一些逻辑拆分出去,然后在父函数中调用这个新的函数。

Reducer重构示例

主要的几个思想

  1. 公共使用的函数抽取出来
  2. 使用 combineReducers来进行state的范式化
  3. 将reducer进行拆分,按照 域数据 和 应用状态 进行拆分

combineReducers用法

使用combineReducers需要注意如下的几个限制

如上 Reducer的基础结构的内容所示

定义state结构

通常都是通过 combineReducers 来对 state进行范式化

注意使用 combineReducers的时候,最好使用 key-value的形式,自定义key的名称,然后对应不同的reducer,具有语义化!

combineReducers进阶

Redux 引入了非常实用的 combineReducers 工具函数,但我们却粗暴地将它限制于单一的应用场景:把不同片段的 state 的更新工作委托给一个特定的 reducer,以此更新由普通的 JavaScript 对象构成的 state 树

在不同的reducer当中实现共享数据主要是有如下的两种方式

  1. 把所需数据当额外参数的形式传递给自定义函数
  2. 通过thunk的方式,如 redux-thunk 给action添加额外的数据
  1. function someSpecialActionCreator() {
  2. return (dispatch, getState) => {
  3. const state = getState();
  4. const dataFromB = selectImportantDataFromB(state);
  5. dispatch({
  6. type : "SOME_SPECIAL_ACTION",
  7. payload : {
  8. dataFromB
  9. }
  10. });
  11. }
  12. }

更多建议:
再次强调,Reducer 只是普通的函数,明确这一概念非常重要。combineReducers 虽然实用也只是冰山一角。

设计范式化的State

这部分是数据库表形式的设计,对于state的设计,尽量扁平化,比如一个博客的数据,不应该以每篇博客作为一个顶级层级,然后把作者、评论等相关信息嵌套进去,这样当其中某一部分发生修改时,会导致整个数据修改,会全部重新渲染,而且性能上,差很多。
应该博文相关信息为一块,评论为一块,用户为一块,然后找方法把他们连接起来。

范式化的数据包含下面几个概念:

Reducer逻辑复用

随着应用程序的增长,在 reducer 逻辑中开始出现一些常见的模式。你可能会发现一部分 reducer 逻辑对于不同类型的数据做着相同的工作,你想通过对每种数据类型复用相同的公共逻辑来减少重复的代码。或者,你可能想要在 store 中处理某个类型的数据的多个 “实例”。

比如,想在程序中追踪多个计数器,分别命名为 A,B,和C。定义初始的 counter reducer,然后使用 combineReducers 去设置状态。

  1. function counter(state = 0, action) {
  2. switch (action.type) {
  3. case "INCREMENT":
  4. return state + 1;
  5. default:
  6. return state;
  7. }
  8. }
  9. const rootReducer = combineReducers({
  10. counterA: counter,
  11. counterB: counter,
  12. counterC: counter
  13. })

如果调用了一次 {type: 'INRECEMENT'},实际上,三个计数器的值都会被增加,而不是仅仅其中一个。

使用高阶Reducer来定制行为

正如 Reducer逻辑拆分定义的那样,高阶reducer是一个接收reducer函数作为参数,并返回新的reducer函数的函数。

创建特定版本的reducer函数,每个版本只相应特定的action。
创建特定的reducer有两种最常见的方式,一个是使用给定的前缀或者后缀生成新的action常量,另一个是在action对象上附加额外的信息

第一种用的比较多,有点类似于 容器当中的 fetcherReducer 或者 queryReducer、workFlowReducer等等,都是自己生成带后缀的action常量

  1. function createCounterWithNamedType(counterName = '') {
  2. return function counter(state = 0, action) {
  3. switch (action.type) {
  4. case `INCREMENT_${counterName}`:
  5. return state + 1;
  6. case `DECREMENT_${counterName}`:
  7. return state - 1;
  8. default:
  9. return state;
  10. }
  11. }
  12. }
  13. function createCounterWithNameData(counterName = '') {
  14. return function counter(state = 0, action) {
  15. const {name} = action;
  16. if(name !== counterName) return state;
  17. switch (action.type) {
  18. case `INCREMENT`:
  19. return state + 1;
  20. case `DECREMENT`:
  21. return state - 1;
  22. default:
  23. return state;
  24. }
  25. }
  26. }
  27. const rootReducer = combineReducers({
  28. counterA: createCounterWithNamedType('A'),
  29. counterB: createCounterWithNamedType('B'),
  30. counterC: createCounterWithNamedType('C'),
  31. });
  32. store.dispatch({type: 'INCREMENT_B'});
  33. console.log(store.getState());
  34. // {counterA: 0, counterB: 1, counterC: 0}

不可变更新模式

更新嵌套数据的关键是必须适当地赋值和更新嵌套的每个级别。不要直接修改嵌套当中的数据,这样会导致更改到原来的state,导致副作用,而reducer也不是纯函数了

初始化State

现在初始化State主要有两种方法

  1. 在createStore当中,传入第二个参数 preloadState
  2. 在reducer当中,对state进行初始化

其初始化规则,详见reducer章节,主要的就是,如果createStore当中传了参数,则优先使用其作为state的值,如果没有传,则用reducer当中给state的默认值


常见问题

综合

问题:

一般而言,如果随着时间的推移,数据处于合理的变动之中、需要一个单一的数据源、在 React 顶层组件 state 中维护所有内容的办法已经无法满足需求,这个时候就需要使用 Redux 了。

它从设计之初就不是为了编写最短、最快的代码,他是为了解决 “当有确定的状态发生改变时,数据从哪里来” 这种可预测行为的问题的。

Redux能作为任何UI层的store,只是常与 React和RN搭配使用

Reducer

问题:

组织State

问题:

强烈推荐只在store中维护普通的可序列化对象、数组以及基本数据类型(undefined除外)。
非序列化项保存在store中是可行的,但会破坏store内容持久化和恢复能力,以及会干扰时间旅行。

创建Store

问题:

只推荐一个应用只有一个单一的store,不仅可以使用Redux DevTools,还能简化数据的持久化及深加工、精简订阅的逻辑处理

不能存在多个middleware链,next是将action传递给下一个middleware,而disptach则是重新开始处理。

Action

问题:

代码结构

问题:

性能

问题:

考虑到各种性能的问题,只有维护state的范式化和扁平化,才能有效地提升性能,不要有太多的嵌套。

React Redux

问题:

组件没有被重新渲染,最常见的原因,是对state进行了直接修改。


排错

常见的dipatch之后,数据并没有变化,很可能的原因就是因为直接修改了state,reducer不是返回一个新的state


词汇表

StateActionReducerdispatchAction CreatorMidllewareStoreStore CreatorStore enhancer


API文档

顶级暴露的方法

顶级方法的引入

  1. import { createStore } from 'redux'

Store API

compose,从右到左来组合多个函数。 需要合成的多个函数。预计每个函数都接收一个参数。它的返回值将作为一个参数提供给它左边的函数。compose(funcA, funcB, funcC) => compose(funcA(funcB(funcC())))

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注