目標
- 掌握 redux
- 掌握 react-redux 及其中間件
- 實現(xiàn) redux、react-redux 及其中間件
資源
起步

Redux
Redux 是 JavaScript 應用的狀態(tài)容器。它保證程序行為一致性且易于測試。
安裝 redux
npm i redux -S
redux 上手
redux 較難上手,是因為上來就有太多的概念需要學習,用一個累加器舉例
- 需要一個 store 來存儲數(shù)據(jù)
- store 里的 reducer 初始化 state 并定義 state 修改規(guī)則
- 通過 dispatch 一個 action 來提交對數(shù)據(jù)的修改
- action 提交到 reducer 函數(shù)里,根據(jù)傳入的 action 的 type,返回新的 state
// src/store/index.js
import { createStore } from 'redux'
const counterReducer = function(state = 0, action) {
switch (action.type) {
case 'add':
return state + 1
case 'minus':
return state - 1
default:
// 初始化
return state
}
}
const store = createStore(counterReducer)
export default store
// ReduxTest.js
import React, { Component } from 'react'
import store from '../store'
export default class ReduxTest extends Component {
componentDidMount() {
// 訂閱狀態(tài)變更
store.subscribe(() => {
this.forceUpdate()
})
}
render() {
return (
<div>
{store.getState()}
<div>
<button onClick={() => store.dispatch({ type: 'add' })}>+</button>
<button onClick={() => store.dispatch({ type: 'minus' })}>-</button>
</div>
</div>
)
}
}
redux 是單項非響應式的
1.createStore 創(chuàng)建 store
2.reducer 初始化、修改狀態(tài)函數(shù)
3.getState 獲取狀態(tài)值
4.dispatch 提交更新
5.subscribe 變更訂閱
react-redux
每次都重新調用 render 和 getState 太 low 了,想用更 react 的方式來寫,需要 react-redux 的支持
npm i react-redux -S
提供了兩個 api
- Provider 為后代組件提供 store
- connect 為組件提供數(shù)據(jù)和變更方法
// index.js 入口文件
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './store'
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App title="hello React" />
</Provider>,
document.getElementById('root')
)
// store/index.js
import { createStore } from 'redux'
const counterReducer = function(state = 0, action) {
const num = action.payload || 1
switch (action.type) {
case 'add':
return state + num
case 'minus':
return state - num
default:
// 初始化
return state
}
}
const store = createStore(counterReducer)
export default store
// ReduxTest.js
import React, { Component } from 'react'
// import store from '../store'
import { connect } from 'react-redux'
// 參數(shù)1:mapStateToProps = (state) => { return { num:state } }
// 參數(shù)2:mapDispatchToProps = dispatch => { return { add: () => { dispatch({ type: 'add' }) } } }
@connect(
state => ({ num: state }),
{
add: num => ({ type: 'add', payload: num }), // action creator
minus: () => ({ type: 'minus' }) // action creator
}
)
class ReduxTest extends Component {
// componentDidMount() {
// // 訂閱狀態(tài)變更
// store.subscribe(() => {
// this.forceUpdate()
// })
// }
render() {
return (
<div>
{/* {store.getState()} */}
{this.props.num}
<div>
{/* <button onClick={() => this.props.dispatch({ type: 'add' })}>
+
</button>
<button onClick={() => this.props.dispatch({ type: 'minus' })}>
-
</button> */}
<button onClick={() => this.props.add(2)}>+</button>
<button onClick={() => this.props.minus()}>-</button>
</div>
</div>
)
}
}
export default ReduxTest
重構水果列表案例為 redux 版
中間件
異步
react 默認只支持同步,實現(xiàn)異步任務比如延遲,網(wǎng)絡請求,需要中間件的支持,比如我們試用最簡單的 redux-thunk 和 redux-logger
npm i redux-thunk redux-logger -S

// store/index.js
import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
const counterReducer = function(state = 0, action) {
const num = action.payload || 1
switch (action.type) {
case 'add':
return state + num
case 'minus':
return state - num
default:
// 初始化
return state
}
}
// applyMiddleware 引入中間件
// 中間件之間有依賴,被依賴的放前面
const store = createStore(counterReducer, applyMiddleware(logger, thunk))
export default store
// ReduxTest.js
@connect(
state => ({ num: state }),
{
add: num => ({ type: 'add', payload: num }), // action creator
minus: () => ({ type: 'minus' }), // action creator
// 異步調用函數(shù)
asyncAdd: () => dispatch => {
setTimeout(() => {
// 異步
dispatch({ type: 'add' })
}, 1000)
}
}
)
<button onClick={() => this.props.asyncAdd()}>+</button>
代碼優(yōu)化
模塊化
抽離 reducer 和 action,創(chuàng)建 store/counter.js
export const add = num => ({ type: 'add', payload: num }) // action creator
export const minus = () => ({ type: 'minus' }) // action creator
// 異步返回的是函數(shù)
export const asyncAdd = () => dispatch => {
// 異步調用在這里
setTimeout(() => {
dispatch({ type: 'add' })
}, 1000)
}
export const counterReducer = function(state = 0, action) {
const num = action.payload || 1
switch (action.type) {
case 'add':
return state + num
case 'minus':
return state - num
default:
// 初始化
return state
}
}
store/index.js 修改后
import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import { counterReducer } from './counter'
// applyMiddleware 引入中間件
// 中間件之間有依賴,被依賴的放前面
const store = createStore(counterReducer, applyMiddleware(logger, thunk))
export default store
ReduxTest.js
import { add, minus, asyncAdd } from '../store/counter'
@connect(
state => ({ num: state }),
{
add,
minus,
asyncAdd
}
)
combineReducers
store/index.js
import { createStore, applyMiddleware, combineReducers } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import { counterReducer } from './counter'
// applyMiddleware 引入中間件
// 中間件之間有依賴,被依賴的放前面
const store = createStore(
combineReducers({ counter: counterReducer }),
applyMiddleware(logger, thunk)
)
export default store
ReduxTest.js
@connect(
state => ({ num: state.counter }),
{
add,
minus,
asyncAdd
}
)
Redux 原理
核心實現(xiàn)
- 存儲狀態(tài) state
- 獲取狀態(tài) getState
- 更新狀態(tài) dispatch
- 變更訂閱 subscribe
store/kredux.js
// 閉包
export function createStore(reducer) {
let currentState = undefined
const currentListeners = []
function getState() {
return currentState
}
function dispatch(action) {
// 修改
currentState = reducer(currentState, action)
// 變更通知
currentListeners.forEach(cb => cb())
return action
}
function subscribe(cb) {
currentListeners.push(cb)
}
// 初始化狀態(tài)
dispatch({ type: '@ArchieChiu' })
// 暴露
return {
getState,
dispatch,
subscribe
}
}
使用 kredux,KReduxTest.js
import React, { Component } from 'react'
import { createStore } from '../store/kredux'
const counterReducer = function(state = 0, action) {
const num = action.payload || 1
switch (action.type) {
case 'add':
return state + num
case 'minus':
return state - num
default:
// 初始化
return state
}
}
const store = createStore(counterReducer)
export default class KReduxTest extends Component {
componentDidMount() {
store.subscribe(() => this.forceUpdate())
}
render() {
return (
<div>
{store.getState()}
<div>
<button onClick={() => store.dispatch({ type: 'add' })}>+</button>
<button onClick={() => store.dispatch({ type: 'minus' })}>-</button>
</div>
</div>
)
}
}
實現(xiàn) applyMiddleware
kredux.js
export function applyMiddleware(...middlewares) {
return createStore => (...args) => {
// 先完成之前 createStore 工作
const store = createStore(...args)
let dispatch = store.dispatch
// 強化dispatch
const midApi = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// [fn1(dispatch), fn2(dispatch)] => fn(dispatch) {}
const chain = middlewares.map(mw => mw(midApi))
// 強化 dispatch,讓它可以按順序執(zhí)行中間件函數(shù)
dispatch = compose(...chain)(store.dispatch)
// 返回全新 store,更新強化過的 dispatch 函數(shù)
return {
...store,
dispatch
}
}
}
export function compose(...fns) {
if (fns.length === 0) {
return arg => arg
}
if (fns.length === 1) {
return fns[0]
}
// 將參數(shù) 函數(shù)數(shù)組 聚合成 一個函數(shù) [fn1, fn2] => fn2(fn1())
return fns.reduce((left, right) => (...args) => right(left(...args)))
}
自定義中間件
logger
KReduxTest.js
import { applyMiddleware } from '../store/kredux'
// 自定義中間件
// 有兩個參數(shù):getState,dispatch
function logger() {
// 返回真正的中間件任務執(zhí)行函數(shù)
return dispatch => action => {
// 執(zhí)行中間件任務
console.log(`${action.type} 執(zhí)行了?。?!`)
// 執(zhí)行下一個中間件
return dispatch(action)
}
}
//使用
const store = createStore(counterReducer, applyMiddleware(logger))
thunk
KReduxTest.js
const thunk = function({ getState }) {
return dispatch => action => {
// thunk邏輯:處理函數(shù)action
if (typeof action === 'function') {
return action(dispatch, getState)
}
// 不是函數(shù)直接跳過
return dispatch(action)
}
}
// 使用
const store = createStore(counterReducer, applyMiddleware(logger, thunk))
react-redux 原理
實現(xiàn) kreact-redux
核心任務:
- 實現(xiàn)一個高階函數(shù)工廠 connect,可以根據(jù)傳入狀態(tài)映射規(guī)則函數(shù)和派發(fā)器映射規(guī)則來映射需要的屬性,可以處理變更檢測和刷新任務
- 實現(xiàn)一個 Provider 組件可以傳遞 store
范例:kstore
kstore/kreact-redux.js
import React from 'react'
import PropTypes from 'prop-types'
import { bindActionCreators } from './kredux'
export const connect = (
mapStateToProps = state => state,
mapDispatchToProps = {}
) => WrapComponent => {
return class ConnectComponent extends React.Component {
static contextTypes = {
store: PropTypes.object
}
constructor(props, context) {
super(props, context)
this.state = {
props: {}
}
}
componentDidMount() {
const { store } = this.context
store.subscribe(() => this.update())
this.update()
}
update() {
const { store } = this.context
const stateProps = mapStateToProps(store.getState())
const dispatchProps = bindActionCreators(
mapDispatchToProps,
store.dispatch
)
this.setState({
props: {
...this.state.props,
...stateProps,
...dispatchProps
}
})
}
render() {
return <WrapComponent {...this.state.props} />
}
}
}
export class Provider extends React.Component {
static childContextTypes = {
store: PropTypes.object
}
getChildContext() {
return { store: this.store }
}
constructor(props, context) {
super(props, context)
this.store = props.store
}
render() {
return this.props.children
}
}
實現(xiàn) bindActionCreators
添加一個 bindActionCreators 能轉換 actionCreator 為派發(fā)函數(shù)
kstore/kredux.js
function bindActionCreator(creator, dispatch) {
return (...args) => dispatch(creator(...args))
}
export function bindActionCreators(creators, dispatch) {
return Object.keys(creators).reduce((ret, item) => {
ret[item] = bindActionCreator(creators[item], dispatch)
return ret
}, {})
}
kstore/index.js
import { createStore, applyMiddleware } from './kredux'
import logger from './kredux-logger'
import thunk from './kredux-thunk'
import { counterReducer } from './counter'
// applyMiddleware 引入中間件
// 中間件之間有依賴,被依賴的放前面
const store = createStore(counterReducer, applyMiddleware(logger, thunk))
export default store
kstore/counter.js
export const add = num => ({ type: 'add', payload: num }) // action creator
export const minus = num => ({ type: 'minus', payload: num }) // action creator
// 異步返回的是函數(shù)
export const asyncAdd = num => dispatch => {
// 異步調用在這里
setTimeout(() => {
dispatch({ type: 'add', payload: num })
}, 1000)
}
export const counterReducer = function(state = 0, action) {
const num = action.payload || 1
switch (action.type) {
case 'add':
return state + num
case 'minus':
return state - num
default:
// 初始化
return state
}
}
kstore/kredux-logger.js
// 自定義中間件
// 有兩個參數(shù):getState,dispatch
export default function() {
// 返回真正的中間件任務執(zhí)行函數(shù)
return dispatch => action => {
// 執(zhí)行中間件任務
console.log(`${action.type} 執(zhí)行了?。?!`)
// 執(zhí)行下一個中間件
return dispatch(action)
}
}
kstore/kredux-thunk.js
// 自定義中間件
// 有兩個參數(shù):getState,dispatch
export default function({ getState }) {
return dispatch => action => {
// thunk邏輯:處理函數(shù)action
if (typeof action === 'function') {
return action(dispatch, getState)
}
// 不是函數(shù)直接跳過
return dispatch(action)
}
}
使用
// KReactReduxTest.js
import React, { Component } from 'react'
import { connect } from '../kstore/kreact-redux'
import { add, minus, asyncAdd } from '../kstore/counter'
@connect(
state => ({ num: state }),
{
add,
minus,
asyncAdd
}
)
class KReactReduxTest extends Component {
render() {
return (
<div>
{this.props.num}
<div>
<button onClick={() => this.props.add(2)}>+</button>
<button onClick={() => this.props.minus()}>-</button>
<button onClick={() => this.props.asyncAdd()}>+</button>
</div>
</div>
)
}
}
export default KReactReduxTest
// index.js 入口
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
// import store from './store'
// import { Provider } from 'react-redux'
import store from './kstore'
import { Provider } from './kstore/kreact-redux'
ReactDOM.render(
<Provider store={store}>
<App title="hello React" />
</Provider>,
document.getElementById('root')
)