React-自学之路(二)

一、Redux

当项目越来越大的时候,管理数据的事件或回调函数将越来越多,也将越来越不好管理,state 在什么时候,由于什么原因,如何变化已然不受控制。。React自带的state与props无法满足或者会混乱状态机时,才会需要Redux来进行数据流管理。

最早Facebook提出的管理流架构为Flux。而Flux 不像一个框架,更是一种组织代码的推荐思想,这里的Redux则是对Flux框架的一些简化。

Redux 主要分为三个部分 Action、Reducer、及 Store。
github
这张图也是看了很多遍了。

文章内容全部源于 redux 官方文档 (https://www.redux.org.cn/)

二、Redux基础库

简单例子

下面是一个简单的Redux通信代码

import { createStore } from 'redux';

// 可以理解为 Reducer
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}

// 总数据管理 store
let store = createStore(counter);

// 函数调用 发送Action
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });

这里介绍 Action、Reducer和store的基础使用。

三大核心

  • Action
    发送请求通知,来通知 Reducer 根据这个通知来处理数据。

  • Reducer
    Reducer 为了把 State 和 Action 产生关联而出现。他是根据 Action 来返回一个新的 State 的函数。为了降低耦合度,编写多个小函数来处理这些 Action,最后开发一个总的 Reducer 来管理这些小 Reduver。
    即是 Redux的思想核心部分

redux 提供了 combineReducers() 函数来管理我们拆分后细小的 reducer函数集合

  • store
    管理整个Redux数据中的内容,被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

三大核心之间的数据流过程

调用 store.dispatch(action)

通过调用 dispatch() 函数将封装的 action 对象发送给 store。

store 调用传入的 reducer 函数

将 dispatch 发送过来的 action 和当前 store 中存储的 state 发送给 reducer 函数。

reducer 就是 (state, action) => newState

我们定义好的 reducer 会根据不同的 action 处理 state,来生成新的 state,需要注意在这个过程中旧的 state 不能发生改变,他只是需要合成一个新的 state 返回就可以了

store 保存了根 reducer 返回的完整 state 树

获得了新的 state 后,会将所有注册监听的监听器都会调用,来完成后续工作。

Redux 核心思想

三大原则:

  • 整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
  • 唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
  • ction 如何改变 state tree ,需要编写 reducers。即拆分 reducer。

三、关联至 Component react-redux

核心思想

在绑定 Component 时,需要绑定的是容器组件,而不是展示组件。因为在 React 中鼓励组件化开发而需要确定的思想。
redux 是为了解决数据流的单项性,而重要数据流通常是需要展示到 UI 层的数据,在组件化分发的 React 上充分使用 Redux 的重要思想。

react-redux

为了将 redux 与 react 无缝结合,建议使用 react-redux

npm install --save react-redux

mapStateToProps

需要先定义 mapStateToProps 这个函数来指定如何把当前 Redux store state 映射到展示组件的 props 中。然后通过调用 connect 绑定监听。

const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
case 'SHOW_ALL':
default:
return todos
}
}

const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}

代码完成读取 state 数据的功能。

mapDispatchToProps

有了接收就有发送,这里就是完成 dispatch 的方法

const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}

connect

准备好上面两个功能后,就可以绑定注册。

import { connect } from 'react-redux'

const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)

export default VisibleTodoList

这样就完成了绑定工作。

Provider

所有容器组件都可以访问 Redux store,所以可以手动监听它。使用<Provider>组件封装完成。

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp)

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

四、异步数据流 redux-thunk

思路

redux使用中在没有添加中间件的情况下只支持同步数据流。可以使用 applyMiddleware() 来增强 createStore()。注意这个功能不是必须的,而是在需要的时候减少编码量来完成异步 action。
他会完成对 dispatch() 的二次封装,让我们在处理基础的 action 时还可以完成对 Promise 的处理。当我们使用 Promise 来处理 action 时,中间件就会完成对此次 dipatch 的封装处理,他会拦截 Promise ,然后对异步返回的 Promise 进行回调处理,每一个 Promise 都会返回 请求中请求后(成功或失败)的两个结果。得到结果后就会自动的 dispatch 异步结果。

Middlewares 中间件

它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。使用 redux 的一个优点就是让 state 数据变化透明化,可以使用 debugger 工具进行数据时光回溯 功能查看每次变化的 state 数据。而实现这个功能就是使用了中间件。

理清思路

以下过程只是用于介绍中间件的需求过程。

简单 Log 日志

在记录 dispatch 的时候我们可以这样简单记录:

let action = addTodo('Use Redux')

console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())

虽然效果不错,记录清晰,但是代码量巨大,工作繁琐。

封装 dispatch

这次我们将上面的代码封装到 dispatchAndLog 中,替换原有 dispatch。

function dispatchAndLog(store, action) {
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
}

但是功能依旧需要外部引入。

替换 store 中的 dispatch 函数

let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}

是的,直接替换为我们想要的 dispatch 功能,更简单直接。

新的问题

上面感觉很完美的完成了任务,但是注意,这只是一个任务,在实际开发中需要的插拔式功能,功能单一但是具有组合式,即模块化。
例如我们需要记录 dispatch 日志功能和 dispatch 异常的反馈功能。

function patchStoreToAddLogging(store) {
let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}

function patchStoreToAddCrashReporting(store) {
let next = store.dispatch
store.dispatch = function dispatchAndReportErrors(action) {
try {
return next(action)
} catch (err) {
console.error('捕获一个异常!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
}


patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)

就像上面代码具有组合式功能,但是写法不具有模块化。因此需要转换写法,采用链式调用的方式。

function logger(store) {
let next = store.dispatch

// 我们之前的做法:
// store.dispatch = function dispatchAndLog(action) {

return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}

让插件完成独立的链式模块,然后在 redux 中去完成链式调用即可。

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

// 在每一个 middleware 中变换 dispatch 方法。
middlewares.forEach(middleware =>
store.dispatch = middleware(store)
)
}

最后的调用方式就像下面这样:

applyMiddlewareByMonkeypatching(store, [ logger, crashReporter ])

是的,这样就完成了模块化中间件。

最后

为了保证在中间件模块中正确使用链式调用(保证获取到的 dispatch 为上一个中间件包装过的)。我们需要将这些功能提出来。

function logger(store) {
return function wrapDispatchToAddLogging(next) {
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
}

是的,函数现在只接收 dispatch,而不是 store。
而下面则是管理中间件的最终函数形式:
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 })
}

这与 Redux 中 applyMiddleware() 的实现已经很接近了,但是有三个重要的不同之处:

它只暴露一个 store API 的子集给 middleware:dispatch(action) 和 getState()。

它用了一个非常巧妙的方式,以确保如果你在 middleware 中调用的是 store.dispatch(action) 而不是 next(action),那么这个操作会再次遍历包含当前 middleware 在内的整个 middleware 链。这对异步的 middleware 非常有用。

为了保证你只能应用 middleware 一次,它作用在 createStore() 上而不是 store 本身。因此它的签名不是 (store, middlewares) => store, 而是 (…middlewares) => (createStore) => createStore。

但是现在已经对中间件的概念和流程整理清除了,下面是可以使用的中间件:

const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd(action.type)
return result
}

applyMiddlewares()

是 Redux 的原生方法,作用是将所有中间件组成一个数组,依次执行。

redux-thunk

该插件是一个中间件,不同于其他插件。

代码实例:

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

const configureStore = () => {
const store = createStore(rootReducer, applyMiddleware(thunk));
return store;
}

export default configureStore;

五、实现Action redux-act-reducer

思路

上述的 action 只有同步操作,调用 dispatch 时 state 会立即更新。而异步的 action 则有三种 action 的状态。

  • 发送 action 后立即执行
  • action 成功后的执行
  • action 失败后执行

这有点像网络请求时的三个状态,即请求中、请求成功和请求失败。但是只能接收其中两个:请求中请求结果

为了更方便的完成异步 action,这里使用 redux-act-reducer 来完成。

redux-act-reducer

基本使用

创建 Action

提供了两种创建方式

  • createAction 创建同步 action
  • createActionAsyn 创建异步 action

解析 Action

提供函数:
createReducer(handlers, defaultState, options)

handler:reducer纯函数中 action 的处理对象
defaultState:初始化时默认 state
options

autoAssign:boolean 类型,默认为 false。如果为 true,则默认使用 Object.assign(...)

使用方法:

import { createReducer } from 'redux-act-reducer';
import { SHOW_HELLO, SHOW_HI } from '../your/actions/path';

const defaultState = {
info: ''
};

const hello = createReducer({
[SHOW_HELLO](state, action) {
return Object.assign({}, state, { info: action.info });
},
[SHOW_HI](state, action) {
return Object.assign({}, state, {
arg1: action.arg1,
arg2: action.arg2
});
},

......

}, defaultState);

export default hello;

这里的时候方式与数据流走向基本与 redux 中的走向一致。

同步处理

createAction

函数如下:
createAction(type, [...args])

使用方法:

import { createAction  } from 'redux-act-reducer';

export const SHOW_HI = 'SHOW_HI';
export const showHi = createAction(SHOW_HI, 'arg1', 'arg2')

dispatch(showHi('one', 'two'))

异步处理

注意:与redux-thunk一起使用,否则redux无法提供异步功能

createActionAsyn

函数如下:
createActionAsync(type, api, options)

type:action 数据类型
api:向服务器发送网络请求的函数,返回Promise
options:字符串或对象,该功能是对默认模式的自定义配置项,可以省略。

name:异步名称。默认值:type
isCreateRequest:是否自动创建和调度REQUEST操作(默认值:true)
isCreateSuccess:是否自动创建和调度SUCCESS操作(默认值:true)
isCreateFailure:是否自动创建和调度FAILURE操作(默认值:true)
onRequest:请求后的功能:onRequest(dispatch,getState)
onSuccess:成功后的功能:onSuccess(dispatch,getState,res)
onFailure:失败后的功能:onFailure(dispatch,getState,err)

实例代码:

创建异步 action

import { createActionAsync } from 'redux-act-reducer';

export const SHOW_HELLO_ASYNC = 'SHOW_HELLO_ASYNC';
export const showHelloAsync = createActionAsync(SHOW_HELLO_ASYNC, api);

reducer中解析 action

import { createReducer } from 'redux-act-reducer';
import { SHOW_HELLO_ASYNC } from '../your/actions/path';

const defaultState = {};
const hello = createReducer({
[SHOW_HELLO_ASYNC](state, action) {
return {
'REQUEST'() {
return {
asyncStatus: {
hello: {
isFetching: true,
err: undefined,
}
}
};
},
'SUCCESS'() {
return {
asyncStatus: {
hello: {
isFetching: false,
err: undefined,
}
},
res: action.res
};
},
'FAILURE'() {
return {
asyncStatus: {
hello: {
isFetching: false,
err: action.err,
}
}
};
}
};
},

......

}, defaultState, { autoAssign: true });

export default hello;

代码量很多,但是逻辑很清晰,对于这个指定的异步 action,插件会对其异步的状态返回一个状态类型REQUESTSUCCESSFAILURE,具体的类型判断是根据异步 api 中返回的 Promise 来判断的。

六、redux-immutable

用来完成 redux 与 immutable 的关联插件

immutable

js在原生创建数据类型即是mutable,可变的。
Immutable数据就是一旦创建,就不能更改的数据。每当对Immutable对象进行修改的时候,就会返回一个新的Immutable对象,以此来保证数据的不可变。在数据某节点发送变化时只会更改此节点和父节点并返回新对象,减少性能损耗。
github

redux-immutable

思路

对我们在传递的数据的封装替换,使用 immutable 解决拷贝效率与性能问题。

代码

在创建 store中,使用 immutable 数据初始化 state。
并且在返回的 reducer 使用 immutable 进行组合 reducer。

import {
combineReducers
} from 'redux-immutable';

import {
createStore
} from 'redux';

const initialState = Immutable.Map();
const rootReducer = combineReducers({});
const store = createStore(rootReducer, initialState);

上面是对 store 接收的总 reducer进行保证处理,下面则是对子 reducer进行处理:
import Immutable from 'immutable';
import {
LOCATION_CHANGE
} from 'react-router-redux';

const initialState = Immutable.fromJS({
locationBeforeTransitions: null
});

const main = createReducer({
...
}, initialState);
export default main;

七、reselect

在web框架中都会用数据库做数据持久层,在查表的时候会为了效率做缓存,reselect是同样的目的。

问题

先来看看为什么需要他

state 数据关联性

import React, { Component } from 'react'
import { connect } from 'react-redux'

class UnusedComp extends Component {
render() {
const { a, b, fab } = this.props
return (
<div>
<h6>{a}</h6>
<h6>{b}</h6>
<h6>{fab}</h6>
</div>
)
}
}

function f(x, y) {
return a + b
}

上面的代码很标准,然后再通过 redux 完成数据管理:

store = {
a:1,
b:1,
fab: 2, // a + b
};

// reducer 中
switch(action.type) {
case 'changeA': {
return {
...state,
a: action.a,
fab: f(action.a, state.b),
}
}
case 'changeB': {
...
}
};

管理后发现需要维护的数据由于产生关联性,不得不对他们进行全部维护。这样只会复杂化。

数据关联性优化

上面问题熟悉 react 可以感觉到可以在 render 或者 componentWillReciveProps 中完成关联数据处理。而 state 中只使用基础数据。

store = {
a:1,
b:1,
}
...
switch(action.type) {
case 'changeA': {
return {
...state,
a: action.a
}
}
...
}

关联数据在其他地方处理:
render() {
const { a, b } = this.props
const fab = f(a, b)
return (
<div>
<h6>{a}</h6>
<h6>{b}</h6>
<h6>{fab}</h6>
</div>
)
}

// 或者
componentWillReciveProps(nextProps) {
const { a, b } = this.props
this.fab = f(a, b)
}

render() {
const { a, b } = this.props
return (
<div>
<h6>{a}</h6>
<h6>{b}</h6>
<h6>{this.fab}</h6>
</div>
)
}

问题虽然解决了,但是数据的处理离开了 redux。

重回 redux

class UnusedComp extends Component {
render() {
const { a, b, fab } = this.props
return (
<div>
<h6>{a}</h6>
<h6>{b}</h6>
<h6>{fab}</h6>
</div>
)
}
}
function mapStateToProps(state) {
const {a, b} = state
return {
a,
b,
fab: f(a,b),
}
}
UnusedComp = connect(mapStateToProps)(UnusedComp)

仿佛又回到刚刚的情况,在数据变化时,都会在 mapStateToProps 下调用 fab 函数,无论调用结果有没有变化。
但是又给了一点希望,就是可以在 mapStateToProps 中进行优化处理,判断后可以规避重复调用函数问题。

let memoizeState = null
function mapStateToProps(state) {
const {a, b, c} = state
if (!memoizeState) {
memoizeState = {
a,
b,
fab: f(a,b),
}
} else {
if (!(a === memoizeState.a && b === memoizeState.b) ) {
// f should invoke
memoizeState.fab = f(a, b)
}
memoizeState.a = a
memoizeState.b = b
}

return memoizeState
}

在内部进行一次缓存处理,来解决我们是否需要有意义的调用关联函数。

reselect

这就是解决我们上面的问题。他的解决方法和上面很像,通过缓存判断是否有必要进行调用关联函数。

import { createSelector } from 'reselect'

fSelector = createSelector(
a => state.a,
b => state.b,
(a, b) => f(a, b)
)

...

function mapStateToProps(state) {
const { a, b, c } = state
return {
a,
b,
c,
fab: fSelector(state),
}
}

提供了下面几个api:

  • createSelector(…inputSelectors|[inputSelectors],resultFunc)
    接受一个或者多个selectors,或者一个selectors数组,计算他们的值并且作为参数传递给resultFunc.

  • defaultMemoize(func, equalityCheck = defaultEqualityCheck)
    defaultMemoize能记住通过func传递的参数.这是createSelector使用的记忆函数。defaultMemoize 通过调用equalityCheck函数来决定一个参数是否已经发生改变(===)。

  • createSelectorCreator(memoize,…memoizeOptions)
    createSelectorCreator用来配置定制版本的createSelector.

    memoize参数是一个有记忆功能的函数,来代替defaultMemoize。
    …memoizeOption展开的参数是0或者更多的配置选项,这些参数传递给memoizeFunc。

    const customSelector = customSelectorCreator(
    input1,
    input2,
    resultFunc // resultFunc will be passed as first argument to customMemoize
    )

    customMemoize(resultFunc, option1, option2, option3)
  • createStructuredSelector({inputSelectors}, selectorCreator = createSelector)
    createStructuredSelector接受一个对象,这个对象的属性是input-selectors,函数返回一个结构性的selector.这个结构性的selector返回一个对象,对象的键和inputSelectors的参数是相同的,但是使用selectors代替了其中的值.

    const mySelectorA = state => state.a
    const mySelectorB = state => state.b

    const structuredSelector = createStructuredSelector({
    x: mySelectorA,
    y: mySelectorB
    })

    const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 }

    八、撤销历史 redux-undo


思路

希望 redux 可以帮我们管理 state 的状态历史。

{
counter: 10
}

设计结构

设计一个可以撤销和恢复功能的结构,那么就需要对所有数据进行缓存。图如下:
github

{
past: Array<T>,
present: T,
future: Array<T>
}

代码实现如上。这个结构很简单,一个很好的模型。
那么整个撤销和恢复的逻辑就可以梳理以下:

  • 撤销

    1.移除 past 中的最后一项
    2.将 present 项插入到 future 的最前列
    3.将移除的那一项赋值给 present

  • 恢复

    1.移除 future 中的第一项
    2.将当前 present 项插入到 past 的最后一项
    3.将移除的项 赋值给 present

redux-undo

安装他:

npm install --save redux-undo

提供可撤销功能的 reducer enhancer 的库。

使用

import undoable, { distinctState } from 'redux-undo'

/* ... */

const todos = (state = [], action) => {
/* ... */
}

const undoableTodos = undoable(todos, {
filter: distinctState()
})

export default undoableTodos

distinctState() 会过滤那些没有引起 state 变化的 actions。然后使用 undoable 来包装我们的 reducer。

完成 reducer 配置后:

import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'

const todoApp = combineReducers({
todos,
visibilityFilter
})

export default todoApp

这样就可以在 reducer 合并层次中的任何层级对一个或多个 reducer 执行 undoable。从而只关心 todos ,否则如果封装到顶层 reducers 则会影响到 visibilityFilter 层。

看看实例代码中的 state :

{
visibilityFilter: 'SHOW_ALL',
todos: {
past: [
[],
[ { text: 'Use Redux' } ],
[ { text: 'Use Redux', complete: true } ]
],
present: [ { text: 'Use Redux', complete: true }, { text: 'Implement Undo' } ],
future: [
[ { text: 'Use Redux', complete: true }, { text: 'Implement Undo', complete: true } ]
]
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos.present, state.visibilityFilter)
}
}

结构和上面一样清晰,我们需要注意的就是 present 中的数据处理。

最后的工作就是实现撤销/恢复按钮功能。

import { ActionCreators as UndoActionCreators } from 'redux-undo'
import { connect } from 'react-redux'

/* ... */

const mapStateToProps = (state) => {
return {
canUndo: state.todos.past.length > 0,
canRedo: state.todos.future.length > 0
}
}

const mapDispatchToProps = (dispatch) => {
return {
onUndo: () => dispatch(UndoActionCreators.undo()),
onRedo: () => dispatch(UndoActionCreators.redo())
}
}

UndoRedo = connect(
mapStateToProps,
mapDispatchToProps
)(UndoRedo)

export default UndoRedo

这里主要完成的就是 onUndo 和 onRedo 两个函数,他会完成我们需要的撤销/恢复功能。而 canUndo 和 canRedo 会判断是否还有撤销/恢复内容。