每一种新语言,新框架或者工具的出现都是为了解决项目中存在的问题。
动机
随着 Javascript 单页应用的开发越来越复杂,管理不断变化的 state 变得非常困难。这些状态有很多:UI 状态,服务器数据,缓存数据等。而组件,页面之间状态共享也是一个问题。
在 React 中,数据在组件中是单向流动的,数据从父组件传递给子组件是通过 props,由于这个特征,两个非父子关系的组件(或叫兄弟组件)之间的通信就比较麻烦。
Redux 的出现正式是为了解决 state 数据的管理问题。
设计思想
Redux 将整个应用的状态都存储在一个地方,就是 Store(容器),里面保存着应用的 state(状态树),组件可以通过 dispach(派发)action(行为)给 store(状态容器),而不是直接通知其他组件。其他组件可以通过订阅 store 中的 state(状态)来更新视图。
三大原则
写 Redux 需要遵循三个原则,有制约,有规则,状态才会可预测。因此 Redux 需要遵循以下三个原则:
单一数据源
整个应用的 state 被储存在一棵 Object tree 中,并且这个 Object tree 只存在于唯一一个 store 中;
State 是只读
State 是只读的,惟一改变 state 的方法就是dispatch(触发) action(动作);
使用纯函数来执行修改
通过 disptch action 来改变 state,action 是一个用于描述已发生事件的普通对象,使用纯函数(没有副作用的函数:函数的输出完全取决函数的参数;副作用:获取网络请求数据,修改 dom 标题等都属于副作用)来执行修改,为了描述 action 如何改变 state tree ,需要编写 reducer (处理器)。
单一数据源的设计让组件之间的通信更加方便,同时也便于状态的统一管理。
基本使用
通过一个 React 计数器组件来使用一下,新建一个项目,删除多余的文件,启动:
npx create-react-app redux-family
cd redux-family
npm install redux -S
npm start
编写一个 Counter 组件,应用 redux :
通过使用createStore
来创建一个srore
仓库,参数就是reducer
和初始 state
,通过 store.getState()
方法获取状态,store.dispatch
来派发动作,store. subscribe
订阅更新,store.unsubscribe
取消订阅,这些基本就是 redux 的全部了:
// src/components/Counter1.js
import React, { Component } from 'react';
import { createStore} from 'redux';
const INCREMENT = 'ADD';
const DECREMENT = 'MINUS';
const reducer = (state = initState, action) => {
switch (action.type) {
case INCREMENT:
return { number: state.number + 1 };
case DECREMENT:
return { number: state.number - 1 };
default:
return state;
}
}
let initState = { number: 0 };
const store = createStore(reducer, initState);
export default class Counter extends Component {
unsubscribe;
constructor(props) {
super(props);
this.state = { number: 0 };
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => this.setState({ number: store.getState().number }));
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={() => store.dispatch({ type: 'ADD' })}>+</button>
<button onClick={() => store.dispatch({ type: 'MINUS' })}>-</button>
<button onClick={
() => {
setTimeout(() => {
store.dispatch({ type: 'ADD' });
}, 1000);
}
}>1秒后加1</button>
</div>
)
}
}
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Counter1 from './components/Counter1';
ReactDOM.render(
<Counter1 />,
document.getElementById('root')
);
结合 React 框架 react-redux
react-redux
用来整合 react 状态和数据订阅,使其使用起来更简便。提供 Provider
使所有组件都可以获取状态,connect
联接状态和组件,获取state
状态,dispatch
派发动作:
npm install react-redux -S
npm start
// src/components/Counter2.js
import React, { Component } from 'react';
import { connect } from 'react-redux'
class Counter extends Component {
render() {
return (
<div>
<p>{this.props.number}</p>
<button onClick={() => this.props.add()}>+</button>
<button onClick={() => this.props.minus()}>-</button>
<button onClick={
() => {
setTimeout(() => {
this.props.add({ type: 'ADD' });
}, 1000);
}
}>1秒后加1</button>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
number: state.number
}
}
const mapDispatchToProps = (dispatch) => {
return {
add: () => dispatch({type: 'ADD'}),
minus: () => dispatch({type: 'MINUS'})
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore} from 'redux';
import { Provider } from 'react-redux'
import Counter2 from './components/Counter2'
const INCREMENT = 'ADD';
const DECREMENT = 'MINUS';
const reducer = (state = initState, action) => {
switch (action.type) {
case INCREMENT:
return { number: state.number + 1 };
case DECREMENT:
return { number: state.number - 1 };
default:
return state;
}
}
let initState = { number: 0 };
const store = createStore(reducer, initState);
ReactDOM.render(
<Provider store={store}>
<Counter2 />
</Provider>,
document.getElementById('root')
);
不仅仅使用在 React 上
Redux 是一个状态数据流管理方案,不是为了 react 单独实现的,可以使用在任何框架以及原生 Javascript 中,这更体现了 Redux 的广阔的视角和先进的思想。
下面在原生 Javascript 应用中使用如下:
需要修改下 index.html
,增加显示数据的 dom,以及操作按钮的 dom:
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React App</title>
</head>
<body>
<div id="root"></div>
<div id="counter">
<p id="number">0</p>
<button id="add">+</button>
<button id="minus">-</button>
</div>
</body>
</html>
index.js
文件中通过订阅渲染函数来做数据的更新:
// src/index.js
import { createStore} from 'redux';
let counterValue = document.getElementById('number');
let incrementBtn = document.getElementById('add');
let decrementBtn = document.getElementById('minus');
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
let initState = { number: 0 };
const reducer = (state = initState, action) => {
switch (action.type) {
case INCREMENT:
return { number: state.number + 1 };
case DECREMENT:
return { number: state.number - 1 };
default:
return state;
}
}
let store = createStore(reducer);
function render() {
counterValue.innerHTML = store.getState().number;
}
store.subscribe(render);
render();
incrementBtn.addEventListener('click', function () {
store.dispatch({ type: INCREMENT });
});
decrementBtn.addEventListener('click', function () {
store.dispatch({ type: DECREMENT });
});
以上就是Redux
的基本应用,学会使用一个框架很快,但是不能仅限于使用,还要知道其内部的实现原理,下面就一起来写一个具有简单功能的模仿的 redux
。
脱离框架的简单实现(源码)
createStore 创建仓库
从使用中可以看出,redux
需要返回一个createStore
方法,这个方法返回一个对象,对象上有getState(),dispatch,subscribe
方法,getState()
返回最新的state
,dispatch
需要派发action
,subscribe
需要订阅更新函数并返回一个取消订阅的函数
。redux内部
需要先派发一次INIT动作
来填充初始状态
。如此分析下来,这样一个redux壳
就有了,先来写壳,再往里面填充功能:
redux/index.js
import createStore from './createStore.js'
export {
createStore
}
redux/utils/ActionType.js
/**
* These are private action types reserved(保留的) by Redux.
* For any unknown actions, you must return the current state.
* (所有未知的action都必须返回当前的state)
* If the current state is undefined, you must return the initial state.
* (如果当前state是undefined,必须返回和一个初始的state)
* Do not reference these action types directly in your code.
* (不要在代码中直接引入这些action type)
*/
const randomString = () => {
return Math.random().toString(36).substring(7).split('').join('.')
}
const ActionTypes = {
INIT: `@@redux/INIT${randomString()}`
}
export default ActionTypes
redux/createStore.js
import ActionType from './utils/ActionType'
function createStore(reducer, preloadedState) {
let currentState = preloadedState || {}
let currentReducer = reducer
let currentListeners = []
const getState = () => {
return currentState
}
const dispatch = (action) => {
}
const subscribe = (listener) => {
return function unsubscribe() {
}
}
// 内部需要先派发一次动作
// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates(填充)
// the initial state tree.
dispatch({type: ActionType.INIT})
return {
getState,
dispatch,
subscribe,
}
}
export default createStore
这样的壳子就写好了,现在填充功能,订阅函数需要收集所有的订阅事件
存储到数组
中,并返回一个取消订阅
的函数;
dispatch
派发action
给reducer
获取最新的状态
,最后通知所有的订阅组件
(这里是更新函数);
const subscribe = (listener) => {
currentListeners.push(listener)
// 返回取消订阅的函数
return function unsubscribe() {
const index = currentListeners.indexOf(listener)
// 取消订阅就是将这个订阅事件从数组中删除
currentListeners.splice(index, 1)
}
}
const dispatch = (action) => {
// reducer:传入旧的状态和action,返回新的state
currentState = currentReducer(currentState, action)
const listeners = currentListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
// 执行每一个订阅函数
listener()
}
return action
}
这样基本的redux功能
就实现完了,将原生 Javascript 应用改成自己实现的redux,功能依然正常。
bindActionCreator
使用action creator
直接派发动作,不直接调用 dispatch,这是一种更便捷的方法。bindActionCreator
将action creator
转换为带有相同key
,但是每个函数都包含dispatch
调用。
将之前直接在 react 中使用 redux 的例子重新改写一下,使用 bindActionCreator
重写一下实现:
import React, { Component } from 'react';
import { createStore, bindActionCreators} from 'redux';
const INCREMENT = 'ADD';
const DECREMENT = 'MINUS';
const reducer = (state = initState, action) => {
switch (action.type) {
case INCREMENT:
return { number: state.number + 1 };
case DECREMENT:
return { number: state.number - 1 };
default:
return state;
}
}
+ function Add() {
+ return { type: INCREMENT }
+}
+function Minus() {
+ return { type: DECREMENT }
+}
let initState = { number: 0 };
const store = createStore(reducer, initState);
+ const actions = { Add, Minus }
+ const boundActions = bindActionCreators(actions, store.dispatch)
export default class Counter extends Component {
unsubscribe;
constructor(props) {
super(props);
this.state = { number: 0 };
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => this.setState({ number: store.getState().number }));
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return (
<div>
<p>{this.state.number}</p>
+ <button onClick={boundActions.Add}>+</button>
+ <button onClick={boundActions.Minus}>-</button>
<button onClick={
() => {
setTimeout(() => {
+ boundActions.Add();
}, 1000);
}
}>1秒后加1</button>
</div>
)
}
}
可以看到所有 action 都由 action creator
派发,不再显式的调用store.dispatch,bindActionCreator
内部就是返回了 dispatch 函数的调用,下面我们来写一下这个 bindActionCreator 函数实现:
思路:就是给每一个action creator 都返回 dispatch 方法调用,如果传入的是函数,也就是单个 action creator,直接调用返回 type,将参数传给调用 dispatch 的函数,如果是一个对象,就给这个对象上的每一个 action creator 都加上 dispatch 方法调用,所以代码的核心就是bindActionCreator
function bindActionCreator(actionCreator, dispatch) {
// 这里派发 action,执行 actionCreator 函数,返回一个 type 作为 dispatch 的参数调用
return function(...args) {
dispatch(actionCreator.apply(this, args))
}
}
完整的实现代码如下:
/**
* function Add() {
return { type: INCREMENT }
}
function Minus() {
return { type: DECREMENT }
}
* const actions = { Add, Minus }
* const boundActions = bindActionCreators(actions, store.dispatch)
*/
function bindActionCreator(actionCreator, dispatch) {
// 这里派发 action,执行 actionCreator 函数,返回一个 type 作为 dispatch 的参数调用的函数
return function(...args) {
dispatch(actionCreator.apply(this, args))
}
}
function bindActionCreators(actionCreators, dispatch) {
// 如果是一个函数,表示就是一个 actionCreator,直接返回包装了调用dispatch的函数
if (actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
// 如果是一个对象,给每个 actionCreator 都包装返回调用dispatch的函数
if (typeof actionCreators === 'object' && actionCreators !== null) {
const bindActionCreators = {}
for(const key in actionCreators) {
const actionCreator = actionCreators[key]
bindActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
return bindActionCreators
}
}
export default bindActionCreators
combineReducers
通常组件不会只有一个 reducer,每个组件都可以有自己的 reducer 和 state,便于模块化管理。但是 redux 规定,一个应用只能有一个 store,一个 reducer,一个 state,根据程序模块化,分而治之的管理模式,这个时候就需要合并多个 reducer,多个状态为一个 reducer,一个state,combineReducers 就是做合并 reducer
操作的,下面先来看看用法,然后再实现一个。
这里只贴重要的代码,其他细致的可以到 github 仓库看实战。
src/components/Counter5.js
import React, { Component } from 'react';
import { bindActionCreators} from '../redux';
import * as actionType from '../actionTypes'
// import { bindActionCreators } from '../redux'
import store from '../store'
const actions = {
Add5: actionType.Add5,
Minus5: actionType.Minus5
}
const boundActions = bindActionCreators(actions, store.dispatch)
export default class Counter5 extends Component {
constructor(props) {
super(props)
this.state = { number : 5}
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => this.setState({ number: store.getState().Counter5.number }));
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={boundActions.Add5}>+</button>
<button onClick={boundActions.Minus5}>-</button>
<button onClick={
() => {
setTimeout(() => {
boundActions.Add5();
}, 1000);
}
}>1秒后加5</button>
</div>
)
}
}
新建 reducers 和 action types
src/reducers/Counter5.js
,src/actionTypes
// src/reducers/Counter5.js
import * as actionType from '../actionTypes'
export let Counter5State = { number: 5 };
const reducer = (state = Counter5State, action) => {
switch (action.type) {
case actionType.INCREMENT5:
return { number: state.number + 5 };
case actionType.DECREMENT5:
return { number: state.number - 5 };
default:
return state;
}
}
export default reducer
// src/actionTypes.js
export const INCREMENT5 = 'ADD5';
export const DECREMENT5 = 'MINUS5';
export const INCREMENT6 = 'ADD6';
export const DECREMENT6 = 'MINUS6';
export function Add5() {
return { type: INCREMENT5 }
}
export function Minus5() {
return { type: DECREMENT5 }
}
export function Add6() {
return { type: INCREMENT6 }
}
export function Minus6() {
return { type: DECREMENT6 }
}
src/store.js
import { createStore } from 'redux'
import reducers from './reducers'
const store = createStore(reducers);
export default store
src/reducers/index.js
import { combineReducers } from 'redux'
import Counter5 from '../reducers/Counter5'
import Counter6 from '../reducers/Counter6'
const reducers = {
Counter5,
Counter6
}
const combinedReducers = combineReducers(reducers)
export default combinedReducers
其他的不贴了,效果就是组件的状态互不干扰:
combineReducers
就是将多个组件的 reducer 合并为一个 reducer,内部实现是过滤 reducers 对象上的函数属性,然后返回一个combination
函数,这个combination
函数内部会遍历过滤之后的对象,解析出来每一个 reducer,拿到对应的状态,最后再根据 reducer 的 key,返回一个新的状态对象。根据 reducer 解析出来的 state 是最重要的一段,const reducer = finalReducers[key]
,const nextStateForKey = reducer(previousStateForKey, action)
。这里面有一个优化操作:定义一个 hasChanged
的标志,新状态和老状态不一致就返回新状态,否则返回老的状态。
具体代码实现如下,跟源码的实现是一致的,代码有备注解释:
src/redux/combineReducers
export default function combineReducers (reducers) {
// 1. 过滤 reducers 对象上的函数属性
const reducerKeys = Object.keys(reducers) // ["counter1", "counter2"]
const finalReducers = {}
for(let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i] // "counter1"
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key] // finalReducers: {counter1: counter1, counter2: counter2}
}
}
// 2. 返回一个函数 遍历finalReducers,生成state
const finalReducerKeys = Object.keys(finalReducers) // ['counter1', 'counter2']
return function combination (state = {}, action) {
let hasChanged = false // hasChanged是作性能优化,没有改变就返回原来的state
const nextState = {}
for(let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i] // "counter1"
const reducer = finalReducers[key] // reducer counter1 函数
const previousStateForKey = state[key] // 取出reducer对应的state
// 这步最重要:给reducer传入老的state和action,返回新的state
const nextStateForKey = reducer(previousStateForKey, action)
// 将新的state的key 拼到总的state上,组件通过 state.counter1.number获取
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey // 前后状态不一样就是改变了
}
hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length // reducers的key不一样也是变了
return hasChanged ? nextState : state
}
以上就是 combineReducers 的实现。
react-redux 实现
从上面可以 Counter5,Counter6 组件可以看出来,这两个组件其实有很多的相似代码,比如订阅更新,获取状态等,这些都可以抽离出来,减少冗余代码,将 store 绑定到 context 上,至上而下的传递,这就是 react-redux 做的事情。
先来看一下 react-redux 的应用,实现一个 Counter7 组件,对应的 actions 和 reducer 和上面一样,就不赘述了。Counter7 使用 react-redux 的 connect 返回一个高阶组件
:
src/components/Counter7.js
import React, { Component } from 'react'
import * as actionType from '../actionTypes'
import { connect } from 'react-redux'
const actions = {
add: actionType.Add7,
minus: actionType.Minus7
}
const mapStateToProps = (state) => {
console.log(state)
return {
number: state.Counter7.number
}
}
const mapDispatchToProps = actions
class Counter7 extends Component {
render() {
const { number, add, minus } = this.props
return (
<div>
<p>{number}</p>
<button onClick={add}>+</button>
<button onClick={minus}>-</button>
<button onClick={
() => {
setTimeout(() => {
add()
}, 1000)
}
}>1秒后加5</button>
</div>
)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter7)
src/index.js
使用 Provider
共享 store:
import React from 'react';
import ReactDOM from 'react-dom';
import Counter7 from './components/Counter7'
import store from './store'
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<Counter7 />
</Provider>
,
document.getElementById('root')
)
可以看到 Counter7 使用 connect 连接,mapStateToProps 将 store 上的state 拼到 props 上
,mapDispatchToProps 将派发函数拼接到 props
。
下面来看看 Provider 和 connect 实现,根据 react-redux 官方的实现,我们也采用函数和 hooks 的方式实现。
Provider 是利用 react 的 context 创建上下文容器,value 可以设置成共享的数据,provider 提供数据,consumer 消费这些数据。
const { Provider, Consumer } = React.createContext(defaultValue);
新建一个 react-redux 文件目录,里面来写代码实现:
src/react-redux/index.js
import Provider from './Provider'
import connect from './connect'
export {
Provider,
connect
}
src/react-redux/ReactReduxContext.js
import React from 'react'
export const ReactReduxContext = React.createContext(null)
export default ReactReduxContext
src/react-redux/Provider.js
import React from 'react'
import ReactReduxContext from './ReactReduxContext'
export default function Provider (props) {
return (
<ReactReduxContext.Provider value={{ store: props.store }}>
{props.children}
</ReactReduxContext.Provider>
)
}
最复杂的是 connnect 的实现:
connnect 是一个函数,返回一个被包装过后的组件,里面做的事情就是将 state,dispatch 派发函数作为 props 拼到被包装的组件上,然后在渲染的时候订阅更新函数,更新状态。
mapStateToProps 映射 state 到 props 上const stateProps = useMemo(() => mapStateToProps(preState), [preState])
const mapStateToProps = (state) => {
return {
number: state.Counter7.number
}
}
mapDispatchToProps 就是映射 dispatch 函数到 props,这里分三种情况,如果参数是对象就要做 bindActionCreators,分别包装返回具有 dispatch能力的函数对象,像这样:{add: ƒ, minus: ƒ}
;如果参数是函数,就直接拼mapDispatchToProps(props, dispatch)
,否则就直接返回一个包含 dispatch 的对象;
const dispatchProps = useMemo(() => {
let dispatchProps
if (typeof mapDispatchToProps === 'object') {
dispatchProps = bindActionCreators(mapDispatchToProps, dispatch)
} else if (typeof mapDispatchToProps === 'function') {
dispatchProps = mapDispatchToProps(props, dispatch)
} else {
dispatchProps = { dispatch }
}
return dispatchProps
}, [dispatch, props])
然后订阅更新函数,更新状态:
const [, forceUpdate] = useReducer(x => x + 1, 0)
useLayoutEffect(() => subscribe(forceUpdate), [subscribe])
useLayoutEffect 会在 dom 更新完之后,浏览器绘制之前执行。
最后返回包装之后的组件:
return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />
完整的 connect 的实现代码如下:
import React, { useContext, useMemo, useReducer, useLayoutEffect } from 'react'
import ReactReduxContext from './ReactReduxContext'
import { bindActionCreators } from '../redux'
export default function connect (mapStateToProps, mapDispatchToProps) {
return function (WrappedComponent) {
return function (props) {
const { store } = useContext(ReactReduxContext)
const { getState, dispatch, subscribe} = store
const preState = getState()
// 映射状态到 props
const stateProps = useMemo(() => mapStateToProps(preState), [preState])
// 映射 dispatch 到 props
const dispatchProps = useMemo(() => {
let dispatchProps
if (typeof mapDispatchToProps === 'object') {
dispatchProps = bindActionCreators(mapDispatchToProps, dispatch)
} else if (typeof mapDispatchToProps === 'function') {
dispatchProps = mapDispatchToProps(props, dispatch)
} else {
dispatchProps = { dispatch }
}
return dispatchProps
}, [dispatch, props])
// 订阅更新函数,更新状态
const [, forceUpdate] = useReducer(x => x + 1, 0)
useLayoutEffect(() => subscribe(forceUpdate), [subscribe])
// 返回被包装的组件
return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />
}
}
}
到目前为止,redux 和 react-redux 的基本核心都实现了,参照的是 github 上的源码。还剩下一个 中间件
的功能,这个准备在下一章单独去写,分析一下时常用的几个 redux 中间件用法和实现,以及连接中间件的实现原理,到 react 生态的整合
,比如 redux-saga,dva,umi
等。目前项目用到的生态就是这些,所以想单独开一篇专门写这个(希望不要开天窗),加深理解,温故知新。
参考:
https://github.com/reduxjs/redux
https://github.com/reduxjs/react-redux