redux & react-redux

一、前置知识

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props

Context的使用基于生产者消费者模式,如果要Context发挥作用,需要用到两种组件

  • 一个是Context生产者(Provider),通常是一个父节点

  • 另外是一个Context的消费者(Consumer),通常是一个或者多个子节点

1.1 Context

创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值

创建context

1
const ThemeContext = React.createContext('light');

生产组件

Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染

1
2
3
4
5
6
7
8
9
10
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}

中间的组件

1
2
3
4
5
6
7
function Toolbar() {
return (
<div>
<MyClass />
</div>
);
}

消费组件或者contextType

  • contextType: class上的contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近Context上的那个值

  • 在react-redux中, 经过高阶组件connect统一处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class MyClass extends React.Component {

static contextType = ThemeContext

constructor () {
super()
this.state = {
ss: 33
}
}
componentDidMount() {
let value = this.context;
console.log(value, 'componentDidMount')
setTimeout(() => {
this.setState({
ss: 222
})
}, 2000);
}
componentDidUpdate() {
let value = this.context;
console.log(value, 'componentDidUpdate')
}
render() {
let value = this.context;
const { ss } = this.state
console.log(value, 'render')
return <div>{ss}</div>
}
}
  • Context.Consumer
1
2
3
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value是离这个context最近的Provider提供的 value 值。如果没有,取createContext()的 defaultValue

Context.displayName

  • context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容

  • react-redux 源码中有做配置

1
2
3
4
5
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中

1.2 柯里化函数(非必须)

把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数

作用:

  • 参数复用,固定不变的参数,实现参数复用是 Currying 的主要用途之一

  • 降低耦合度和代码冗余,便于复用

通过递归降解的思路,简化参数,实现多参函数

1
2
3
4
5
6
7
8
9
10
11
12
var add = function(x) {
return function(y) {
return x + y;
};
};

var increment = add(1);
var addTen = add(10);

increment(2); // 3

addTen(2); // 12

组合

1
2
3
4
5
const compose = (f, g) => {
return (x) => {
return f(g(x));
};
};

1.3 展示组件和容器组件

展示组件

  • 关注页面的展示效果(外观)

  • 内部可以包含展示组件和容器组件,通常会包含一些自己的DOM标记和样式(style)

  • 通常允许通过this.props.children方式来包含其他组件

  • 对应用程序的其他部分没有依赖关系,例如Flux操作或store

  • 不用关心数据是怎么加载和变动的

  • 只能通过props的方式接收数据和进行回调(callback)操作

  • 很少拥有自己的状态,即使有也是用于展示UI状态的

  • 会被写成函数式组件除非该组件需要自己的状态,生命周期或者做一些性能优化

容器组件

  • 关注应用的是如何工作的

  • 内部可以包含容器组件和展示组件,但通常没有任何自己的DOM标记,并且从不具有任何样式

  • 提供数据和行为给其他的展示组件或容器组件

  • 调用Flux操作并将它们作为回调函数提供给展示组件

  • 往往是有状态的,因为它们倾向于作为数据源

  • 通常使用高阶组件生成,例如React Redux的connect

redux 和 react-redux

  • redux 本身不依赖框架, 其主要功能, 是让我们的数据变更变得可控, 修改数据只能通过 dispatch, 展现数据只能通过 getState 获取的 objec tree

  • react-redux 是基于 redux 功能, 为 react 开发的, 其主要目的是利用 redux 提供的功能, 通过生产组件和消费组件, 将顶层注入的数据通信到各个组件, 以便使用和展现

二、回到redux

特点

  • redux 以 reducer 为拆分维度,然后 combineReducers 合成一个总 reducer 来创建 store,然后传给 React context,全局访问

  • redux 把 reducer 生产的 state 闭包在内部,无法直接访问,只能 dispatch action 驱动 reducer 函数来改变 state,逻辑集中在 reducer 内,可预测维护

基本思想

  • Web 应用是一个状态机,视图与状态是一一对应的

  • 所有的状态,保存在一个对象里面

原则

  • 单一数据源, 整个应用的 state 被储存在唯一一个 store 的 object tree

  • state 是只读的, 唯一改变 state 的方法就是触发 action

  • 使用纯函数 reducers 来执行修改, 把 action 和 state 串起来,接收 state 和 action,并返回新的 state

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// store/home.js
export const home = (state = { count: 1 }, action) => {
if (action.type === 'add') {
return { ...state, count: state.count + 1 }
}
return state;
}

// store/detail.js
export const detail = (state = { detail: false }, action) => {
if (action.type === 'toggle') {
return { ...state, detail: !state.detail }
}
return state
}

// store/index.js
import { createStore, combineReducers } from 'redux';
import { home } from './home';
import { detail } from './detail';
export const store = createStore(combineReducers({
home,
detail
}))

// index.js
import { store } from './store/index';

console.log(store.getState()) // { home: { count: 1}, detail: { detail: false } }

store.dispatch({
type: 'add'
})

console.log(store.getState()) // { home: { count: 2}, detail: { detail: false } }

分析源码

从上面的使用再结合源码,我们来分析分析,首先createStore之后生成一个对象 store(其实这个并不是object tree), 这个 store 包含如下 { dispatch, subscribe, getState, replaceReducer,[$$observable]: observable }; 通过store.getState()的方法获得对象,则是单一数据源 object tree,下面我们从 createStore 返回, 我用到的方法来分析一下

实现getState

1
2
3
function getState() {
return currentState
}

实现dispatch

1
2
3
4
function dispatch(action) {
currentState = currentReducer(currentState, action)
return action
}

原则三说使用纯函数 reducers 来执行修改state, 并把 action 和 state 串起来, 那我们也结合起来看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 写个reducer
const detail = (state = { detail: false }, action) => {
if (action.type === 'toggle') {
return { ...state, detail: !state.detail }
}
return state
}

// 然后执行
let store = createStore(detail)

console.log(store.getState())


// createStore 实现
function createStore(reducer, preloadedState) {

let currentReducer = reducer
let currentState = preloadedState

// 实现 dispatch
function dispatch (action) {
currentState = currentReducer(currentState, action)
return action
}

// 实现 getState
function getState() {
return currentState
}

// 调用一次 , ActionTypes 无法匹配返回默认值
dispatch({ type: ActionTypes.INIT })

// 返回
return {
dispatch,
getState
}
}

看了上面的代码是不是一下清楚多了, currentState 单一数据源(object tree), 用dispatch 触发 action , 修改 state 状态, 另外 redux 把 reducer 生产的 state 闭包在内部,无法直接访问

上面我们把redux的原则串起来了, 但是还有一些其他的,下面我们看看 subscribe 和 replaceReducer

实现subscribe

  • store.subscribe(handleChange) 添加一个监听

  • store.subscribe(handleChange) 返回值是销毁监听器的函数 unsubscribe

  • 当 dispatch 时会触发 subscribe 的回调

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// index.js 

import { store } from './store/index';

const handleChange = () => {
let currentValue = store.getState()
console.log(currentValue)
}

const unsubscribe = store.subscribe(handleChange)

store.dispatch({
type: 'add'
})

subscribe 的基本实现如下, currentListeners 维护当前的监听队列, subscribe 每执行一次都会返回一个销毁监听的函数, 用来找到当前的监听并销毁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let currentListeners = []             // 监听队列
let nextListeners = currentListeners // 保证两个变量存统一分值

function subscribe(listener) {

// 订阅标记
let isSubscribed = true

// 保存订阅快照, nextListeners 拷贝一份 currentListeners ,并解除引用关系
ensureCanMutateNextListeners()
// 添加一个订阅函数
nextListeners.push(listener)

// unsubscribe 销毁订阅的方法
return function unsubscribe() {
if (!isSubscribed) {
return
}
isSubscribed = false
ensureCanMutateNextListeners()

// 找到并删除当前的listener
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}

//
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}

上面实现了 subscribe ,但是还差一点, 在dispatch时,触发监听的回调, 此时需要修改一下, diapatch 方法

1
2
3
4
5
6
7
8
9
10
11
12
function dispatch (action) {
currentState = currentReducer(currentState, action)

// 遍历监听队列, 逐个执行
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}

return action
}

实现replaceReducer

  • 替换 store 当前用来计算 state 的 reducer

  • 需要实现代码分隔,而且需要立即加载一些 reducer 的时候才可能会用到

  • 在实现 Redux 热加载机制的时候也可能会用到

没什么好说的, 就是换掉了 currentReducer, 也没有在业务场景中使用过, 上面是官方给的可能用的场景

1
2
3
4
function replaceReducer(nextReducer) {
currentReducer = nextReducer
dispatch({ type: ActionTypes.REPLACE })
}

实现combineReducers

使用方式

1
2
3
4
const Reducers = combineReducers({
home,
detail
})
  • 生成一个函数,这个函数来调用你的一系列 reducer

  • 每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理

  • 然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
export default function combineReducers(reducers) {

const reducerKeys = Object.keys(reducers)
const finalReducers = {}

// 生成一个入参reducers的副本 finalReducers = { home, detail }
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}

const finalReducerKeys = Object.keys(finalReducers)

// 返回函数, 作为 createstore 中的reducer入参
// createStore 执行时触发一次 dispatch({ type: ActionTypes.INIT })
// 因此会调用一次currentReducer, 即combination,

return function combination(state = {}, action) {
let hasChanged = false
const nextState = {}

// 遍历全部的Reducers
for (let i = 0; i < finalReducerKeys.length; i++) {

const key = finalReducerKeys[i]
const reducer = finalReducers[key]

// dispatch 初次执行 state[key] 不存在, 则直接返回 我们 reducers 的state 默认值
const previousStateForKey = state[key]

// 执行reducer,这完全就是我们自己的函数home和detail函数
const nextStateForKey = reducer(previousStateForKey, action)

// 新的state放在nextState对应的key里, 这里注意, 使用 combineReducers 时, 会根据每个 reducers 的名称增加一层变量命名,即 state= {home: {count: 1}, detail: {detail: false}}
nextState[key] = nextStateForKey

// 判断新的state是不是同一引用, 是就没有更改state, 无需更新
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}

return hasChanged ? nextState : state
}
}

bindActionCreators

使用

使用 bindActionCreators 对 action creator 绑定 dispatch 方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// TodoActionCreators.js
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}

export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
}
}

// component.js
import { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'

import * as TodoActionCreators from './TodoActionCreators'

class TodoListContainer extends Component {
constructor(props) {
super(props)
const { dispatch } = props

this.boundActionCreators = bindActionCreators(TodoActionCreators, dispatch)
console.log(this.boundActionCreators);
// { addTodo: Function, removeTodo: Function }
}

componentDidMount() {
let { dispatch } = this.props
// 调用了创建 action 的方法, 必须要同时 dispatch action
let action = TodoActionCreators.addTodo('Use Redux')
dispatch(action)
}

render() {
let { todos } = this.props
return <TodoList todos={todos} {...this.boundActionCreators} />
}
}

export default connect(state => ({ todos: state.todos }))(TodoListContainer)

源码,结合上面使用, 在阅读下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
//
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}

const boundActionCreators = {}
for (const key in actionCreators) {
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}

react-redux

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';

// 创建容器组件和reducer
const reducer = (state = { count: 1 }, action) => {
if (action.type === 'add') {
return {
...state,
count: state.count + 1,
}
}
return state;
};
const store = createStore(reducer);

// 展示组件内触发
const mapStateToProps = (state) => {
return ({
count: state.count,
});
};

const mapDispatchToProps = dispatch => ({
add: () => dispatch({ type: 'add' }),
});
class App extends React.Component {
render() {
return (
<div>
<p>{this.props.count}</p>
<button onClick={this.props.add}>点击+1</button>
</div>
)
}
}
const AppContainer = connect(
mapStateToProps,
mapDispatchToProps
)(App);

// 创建的store数据提供给提供者的store
ReactDOM.render(
<Provider store={store}>
<AppContainer />
</Provider>,
document.getElementById('root'));

分析 Provider 和 connect

Provider

  • Provider 就是我们在前置知识里提到的生产组件

  • 从上面 redux 我们知道 dispatch 之后, object tree 会被重新赋值, 可以通过 store.getState() 取到最新的

  • 我们想它自动更新, 直接供我们使用, 可以利用 subscribe 方法监听,当 dispatch 发生后, 我们就给生产组件重新赋新值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import React, { Component } from 'react'

// 创建ReactReduxContext
const ReactReduxContext = React.createContext(null)

class Provider extends Component {
constructor(props) {
super(props)

// 获取 redux 通过 createStore 生成的 store
const { store } = props
this.state = {
// object tree
storeState: store.getState(),
store
}
}

componentDidMount() {
this.subscribe()
}

componentWillUnmount() {
if (this.unsubscribe) this.unsubscribe()
}

subscribe() {
const { store } = this.props

this.unsubscribe = store.subscribe(() => {
const newStoreState = store.getState()
this.setState({ storeState: newStoreState })
})
}

render() {
const Context = ReactReduxContext

return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}
}

export default Provider

connect

connect 是一个高阶组件, 相对比较复杂, 下面就让我们就着源码, 来分析一下 connect, 首先看一下 connect 使用,从调用来看 connect()() 本身是一个函数, 接收参数 mapStateToProps 和 mapDispatchToProps, 并且函数执行后又返回一个函数, 新的函数接收我们类组件

1
2
3
4
connect(
mapStateToProps,
mapDispatchToProps
)(App);

结合源码, 我们简化一个 connect 实现

1
2
3
4
5
6
function connect (mapStateToProps, mapDispatchToProps) {
// 接收我们的类组件
return function connectHOC(WrappedComponent) {

}
}

上面实现了最基础的函数结构, 接下来我们往下实现, connectHOC 是一个高阶组件, 接收我们传入的组件, 经过selectDerivedProps包装处理,将最新的数据注入到组件并返回新的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { ReactReduxContext } from './Context'

function connectHOC(WrappedComponent) {
class Connect extends Component {
constructor(props) {
super(props)
}
renderWrappedComponent(value) {
const { storeState, store } = value
const sourceSelector = selectorFactory(store.dispatch, selectorFactoryOptions)
const nextProps = sourceSelector(storeState, this.props)
return <WrappedComponent {...nextProps}/>
}

render() {
const ContextToUse = Context
return (
// <ContextToUse.Consumer> 的执行函数能够直接拿到提供者Provider注入的所有的值
<ContextToUse.Consumer>
{this.renderWrappedComponent}
</ContextToUse.Consumer>
)
}
}
return Connect
}

上面看完应该会明朗很多了, 但是还有个地方未处理,那就是selectorFactory 函数还未实现, 接下来我们来实现它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let state
let ownProps
let stateProps
let dispatchProps
let mergedProps

function selectorFactory(dispatch) {
return function (nextState, nextOwnProps) {
return handleFirstCall(nextState, nextOwnProps)
}
}

// 参数 firstState 则是我们消费组件的 storeState, 参数二则是 props , 该方法是在HOC里调用触发
function handleFirstCall(firstState, firstOwnProps) {
state = firstState
ownProps = firstOwnProps
// mapStateToProps 和 mapDispatchToProps 我们自己传入的
stateProps = mapStateToProps(state)
dispatchProps = mapDispatchToProps(dispatch)
mergedProps = mergeProps(stateProps, dispatchProps)
return mergedProps
}

// 合并 state 和 dispatch, 一般我们不传递, 会有个默认处理方式, 则是下面代码
const mergeProps = function (stateProps, dispatchProps) {
return {...stateProps, ...dispatchProps }
}

最后放一张redux和react-redux 工作流程图.

demo

返回
顶部