Redux
理解JavaScript的纯函数
- 函数式编程中有一个非常重要的概念 纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念
- 在React开发中,纯函数被多次提及:
- React组件被要求像一个纯函数(为什么是像,因为还有类组件)
- Redux中有一个reducer的概念,同样是要求必须是一个纯函数
- 掌握纯函数对于理解很多框架的设计都是有帮助的
一个纯函数必然具备以下特征:
- 确定的输入一定产生确定的输出
- 函数的执行过程中,不能产生副作用
为什么需要Redux
- JS需要管理的状态越来越多,越来越复杂
- 状态不断发生变化之间又相互依赖,这要求视图层也能同步更新
- React提供了自动更新视图的方法,但状态仍需要手动管理
- Redux可以帮我们管理状态,提供了可预测的状态管理
- 框架无关,体积只有2KB大小
Redux的核心理念
Redux的核心理念 Store
- 定义一个统一的规范来操作数据,这样就可以做到对数据的跟踪
list.push()
list[0].age = 18
Redux的核心理念 Action
- Redux要求:要修改数据,必须通过Action来修改
- 所有数据的变化,必须通过派发(Patch)Action来更新
- Action是一个普通的JS对象,用来描述此次更新的type与content
const action = { type: 'ADD_ITEM', item: { name: 'Ziu', age: 18 } }
Redux的核心理念 Reducer
- 如何将Store和Action联系在一起?
- reducer是一个纯函数
- 完成的工作就是:将传入的state和action结合起来,生成一个新的state
patch
=>reducer
=>newState
=>Store
Redux Demo
下例中,通过createStore
创建了一个Store(已经不推荐了)
- initialState用于在调用
createStore
时作为默认值传入reducer
- 后续每次
store.dispatch
都会调用reducer
- 通过
reducer
更新state中的数据
在React中,可以通过store.subscribe
注册State变化的监听回调
- 当state发生变化时,通过调用
this.forceUpdate
触发组件的更新 - 一般情况下,我们在
componentDidMount
注册监听回调,在componentWillUnmount
解除监听
// App.jsx
import React, { PureComponent } from 'react'
import store from './store'
export default class App extends PureComponent {
componentDidMount() {
// Subscribe to the store
store.subscribe(() => {
console.log('subscribe', store.getState())
this.forceUpdate()
})
}
componentWillUnmount() {
store.unsubscribe()
}
render() {
return (
<div>
<h1>App</h1>
<p>Count: {store.getState().count}</p>
<p>Name: {store.getState().name}</p>
<button onClick={() => store.dispatch({ type: 'INCREMENT' })}> +1 </button>
<button onClick={() => store.dispatch({ type: 'DECREMENT' })}> -1 </button>
<button onClick={() => store.dispatch({ type: 'CHANGE_NAME', name: 'ZIU' })}>
{' '}
CHANGE_NAME{' '}
</button>
</div>
)
}
}
// store/index.js
import { createStore } from 'redux'
// The initial application state
// This is the same as the state argument we passed to the createStore function
const initialState = {
count: 0,
name: 'Ziu'
}
// Reducer: a pure function that takes the previous state and an action, and returns the next state.
// (previousState, action) => newState
function reducer(state = initialState, action) {
console.log('reducer', state, action)
switch (action.type) {
case 'INCREMENT':
// NOTE: Keep functions pure - do not mutate the original state.
// Desctructure the state object and return a **new object** with the updated count
// Instead of `return state.count++`
return {
...state,
count: state.count + 1
}
case 'DECREMENT':
return {
...state,
count: state.count - 1
}
case 'CHANGE_NAME':
return {
...state,
name: action.name
}
default:
return state
}
}
const store = createStore(reducer)
export default store
进一步封装
可以将耦合在一起的代码拆分到不同文件中
- 将
reducer
抽取出来reducer.js
,简化store/index.js
内容 - 将
action.type
抽取为常量constants.js
,使用时做导入,以保证一致性 - 将
action
抽取出来actionFactory.js
,用于外部dispatch时规范类型
// store/index.js
import { createStore } from 'redux'
import reducer from './reducer'
const store = createStore(reducer)
export default store
// constants.js
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'
export const CHANGE_NAME = 'CHANGE_NAME'
// reducer.js
import * as actionType from './constants'
const initialState = {
count: 0,
name: 'Ziu'
}
export default function reducer(state = initialState, action) {
switch (action.type) {
case actionType.INCREMENT:
return {
...state,
count: state.count + 1
}
case actionType.DECREMENT:
return {
...state,
count: state.count - 1
}
case actionType.CHANGE_NAME:
return {
...state,
name: action.name
}
default:
return state
}
}
// actionFactory.js
import * as actionType from './constants'
export const increment = () => ({
type: actionType.INCREMENT
})
export const decrement = () => ({
type: actionType.DECREMENT
})
export const changeName = (name) => ({
type: actionType.CHANGE_NAME,
name
})
// App.jsx
import React, { PureComponent } from 'react'
import store from './store'
import { increment, decrement, changeName } from './store/actionFactory'
export default class App extends PureComponent {
componentDidMount() {
store.subscribe(() => this.forceUpdate())
}
componentWillUnmount() {
store.unsubscribe()
}
render() {
return (
<div>
<h1>App</h1>
<p>Count: {store.getState().count}</p>
<p>Name: {store.getState().name}</p>
<button onClick={() => store.dispatch(increment())}> +1 </button>
<button onClick={() => store.dispatch(decrement())}> -1 </button>
<button onClick={() => store.dispatch(changeName('ZIU'))}>CHANGE_NAME</button>
</div>
)
}
}
Redux的三大原则
单一数据源
- 整个应用程序的状态都被存储在一棵Object Tree上
- 且这个Object Tree只存储在一个Store中
- 但Redux并不强制限制创建多Store,不利于数据维护
- 单一数据源有利于整个应用程序的维护、追踪、修改
State属性是只读的
- 允许修改State的方法只有patch action,不要直接修改State
- 确保了View或网络请求都不能修改State
- 保证所有的修改都能被追踪、按照严格的顺序执行,不用担心竞态(race condition)的问题
使用纯函数来执行修改
- 通过reducer将旧State与新State联系在一起,并且返回一个新的State
- 随着应用程序复杂程度增加,可以将reducer拆分为多个小的reducer,分别用于操作不同State Tree的某一部分
- 所有的reducer都应该是纯函数,不能产生任何的副作用
优化重复代码
当编写了一些案例的时候会发现,React结合Redux时会编写很多重复的代码
在每个需要用到Redux中状态的组件中,都需要在不同生命周期做添加订阅/解除订阅的处理,组件初始化时还要从store中取最新的状态
针对重复代码的问题,可以使用之前学到的高阶组件来做优化
Redux官方提供的库react-redux
,可以让我们更方便的在React中使用Redux
npm i react-redux
在Profile组件中,通过高阶函数connect
实现的
将store中需要的状态通过mapStoreToProps
转为props,并将需要使用store中状态的组件传入调用connect返回的函数中
在Profile
组件中就可以从props中获取到store中的状态
// App.jsx
import React, { PureComponent } from 'react'
import { Provider } from 'react-redux'
import store from './store'
import Profile from './Profile'
export default class App extends PureComponent {
render() {
return (
<Provider store={store}>
<div>
<h2>App</h2>
<Profile></Profile>
</div>
</Provider>
)
}
}
// Profile.jsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
// mapStateToProps is a function that
// takes the state of the store as an argument
// and returns an object with the data that the component needs from the store.
// component will receive the data as props.
const mapStateToProps = (state) => ({
count: state.count
})
export default connect(mapStateToProps)(
class Profile extends Component {
render() {
return (
<div>
<h2>Profile</h2>
<p>Count: {this.props.count}</p>
</div>
)
}
}
)
我们刚刚只是完成了对State的映射,将Store中保存的全局状态state映射到了Profile组件的props中
connect还可以传入第二个参数,用于将action也映射到props中:
// Profile.jsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { INCREMENT, DECREMENT } from './store/constants'
const mapStateToProps = (state) => ({
count: state.count
})
const mapDispatchToProps = (dispatch) => ({
increment: () => dispatch({ type: INCREMENT }),
decrement: () => dispatch({ type: DECREMENT })
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(
class Profile extends Component {
render() {
return (
<div>
<h2>Profile</h2>
<p>Count: {this.props.count}</p>
<button onClick={this.props.increment}> +1 </button>
<button onClick={this.props.decrement}> -1 </button>
</div>
)
}
}
)
本质上是connect
内部对操作进行了封装,把逻辑隐藏起来了:
- 调用
connect
这个高阶函数,返回一个高阶组件 - 为高阶组件传入映射目标组件,最后高阶组件返回一个新组件
- 新组件的props包含了来自Store中状态/dispatch的映射
异步Action
有些场景下,我们希望组件能够直接调用Store中的action来触发网络请求,并且获取到数据
但是dispatch只允许派发对象类型的Action,不能通过dispatch派发函数
可以通过中间件redux-thunk
来对Redux做增强,让dispatch能够对函数进行派发
npm i redux-thunk
通过applyMiddleware
引入redux-thunk
这个中间件:
// store/index.js
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
const store = createStore(reducer, applyMiddleware(thunk))
export default store
// actionFactory.js
export const fetchPostList = () => {
return (dispatch, getState) => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then((res) => res.json())
.then((res) => {
dispatch({
type: actionType.FETCH_POST_LIST,
list: res
})
})
}
}
// list.jsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { fetchPostList } from './store/actionFactory'
const mapStateToProps = (state) => ({
list: state.list
})
const mapDispatchToProps = (dispatch) => ({
fetchList: () => dispatch(fetchPostList())
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(
class Profile extends Component {
render() {
return (
<div>
<h2>List</h2>
<button onClick={() => this.props.fetchList()}>Fetch List</button>
{this.props.list.length && (
<ul>
{this.props.list.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
)}
</div>
)
}
}
)
- 这样就可以将网络请求的具体逻辑代码隐藏到Redux中
- 将网络请求归于状态管理的一部分
- 而不是书写在组件内,不利于维护,耦合度太高
redux-thunk
是如何做到可以让我们发送异步请求的?
- 默认情况下
dispatch(action)
的action必须为一个JS对象 redux-thunk
允许我们传入一个函数作为action
- 函数会被调用,并且将
dispatch
函数和getState
函数作为入参传递给这个函数actiondispatch
允许我们在这之后再次派发action
getState
允许我们之后的一些操作依赖原来的状态,可以获取到之前的状态
下图展示了从组件调用方法,触发patch到Redux接收patch、发送网络请求、更新state的全过程:
拆分Store
拆分Store带来的益处很多,便于多人协作、不同业务逻辑解耦等
在Redux中,拆分Store的本质是拆分不同的reducer
函数,之前在使用createStore
时,传入的就是reducer
函数
之前的Store写法与用法:
// store/index.js
import { createStore } from 'redux'
import reducer from './reducer'
const store = createStore(reducer)
// App.jsx
store.getState().count
store.getState().list
拆分Store后的写法与用法:
// store/index.js
import { createStore, combineReducers } from 'redux'
import counterReducer from './counter'
import postListReducer from './postList'
const reducer = combineReducers({
counter: counterReducer,
postList: postListReducer
})
const store = createStore(reducer)
// App.jsx
store.getState().counter.count
store.getState().postList.count
拆分为多个Reducer之后,需要首先getState()
获取到整个状态树,随后指定获取到不同的模块中的状态
拆分后,不同模块下的文件是保持一致的:
- store/ # Store根目录
- index.js # 导出 store 位置
- counter/ # Counter模块
- actionFactory.js
- constants.js
- index.js # 统一导出
- reducer.js
- postList/ # PostList模块
- actionFactory.js
- constants.js
- index.js
- reducer.js
- ...
combineReducer函数
前面拆分Store时用到了combineReducer
函数,将多个模块reducer组合到一起,函数内部是如何处理的?
- 将传入的reducers合并到一个对象中,最终返回一个
combination
函数(相当于未拆分时传给createStore
的reducer
函数) - 在执行
combination
函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state - 新state会触发订阅者发生对应更新,而旧state可以有效地组织订阅者发生刷新
下面简单写了一下combineReducer
的实现原理
// 使用
combineReducer({
counter: combineReducer,
postList: postListReducer
})
// 创建一个新的reducer
function reducer(state = {}, action) {
// 返回一个对象 是Store的state
return {
counter: counterReducer(state.counter, action),
postList: postListReducer(state.postList, action)
}
}
ReduxToolkit
- ReduxToolkit重构
- ReduxToolkit异步
- connect高阶组件
- 中间件的实现原理
- React状态管理选择
认识ReduxToolkit
之前在使用createStore
创建Store时会出现deprecated标识,推荐我们使用@reduxjs/toolkit
包中的configureStore
函数
Redux Toolkit是官方推荐编写Redux逻辑的方法
- 在前面学习Redux时已经发现,Redux的逻辑编写过于繁琐、麻烦
- 代码分拆在不同模块中,存在大量重复代码
- Redux Toolkit旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题
- 这个包常被称为:RTK
使用ReduxToolkit重写Store
Redux Toolkit依赖于react-redux包,所以需要同时安装这二者
npm i @reduxjs/toolkit react-redux
Redux Toolkit的核心API主要是下述几个:
configureStore
包装createStore以提供简化的配置选项和良好的默认值- 可以自动组合你的slice reducer 添加你提供的任何Redux中间件
- 默认包含redux-thunk,并启用Redux DevTools Extension
createSlice
创建切片 片段- 接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有actions
createAsyncThunk
- 接受一个动作类型字符串和一个返回Promise的函数
- 并生成一个
pending / fullfilled / rejected
基于该承诺分派动作类型的thunk
写一个Demo:
// store/index.js
import { configureStore } from '@reduxjs/toolkit'
import counterSlice from './features/counter'
const store = configureStore({
reducer: {
counter: counterSlice
}
})
export default store
// store/features/counter.js
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: {
count: 0
},
reducers: {
addCount(state, action) {
const { payload } = action
state.count += payload
},
subCount(state, action) {
const { payload } = action
state.count -= payload
}
}
})
const { actions, reducer } = counterSlice
export const { addCount, subCount } = actions
export default reducer
// Counter.jsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { addCount, subCount } from '../store/features/counter'
const mapStateToProps = (state) => ({
count: state.counter.count
})
const mapDispatchToProps = (dispatch) => ({
increment: (count) => {
const action = addCount(count)
return dispatch(action)
},
decrement: (count) => {
const action = subCount(count)
return dispatch(action)
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(
class Counter extends Component {
render() {
const { count } = this.props
return (
<div>
<h2>Counter</h2>
<div>count: {count}</div>
<button onClick={() => this.props.increment(1)}>+1</button>
<button onClick={() => this.props.decrement(1)}>-1</button>
</div>
)
}
}
)
createSlice
函数参数解读
name
标记Slice 展示在dev-tool中initialState
初始化状态reducers
对象 对应之前的reducer函数- 返回值: 一个对象 包含所有actions
configureStore
解读
reducer
将slice中的reducer组成一个对象,传入此参数middleware
额外的中间件- RTK已经为我们集成了
redux-thunk
和redux-devtool
两个中间件
- RTK已经为我们集成了
devTools
布尔值 是否启用开发者工具
使用RTK执行异步dispatch
实际场景中都是在组件中发起网络请求,并且将状态更新到Store中
之前的开发中,我们通过redux-thunk
这个中间件,让dispatch中可以进行异步操作
ReduxToolkit默认已经给我们集成了Thunk相关的功能:createAsyncThunk
下面我们使用RTK实现一下这个场景:在Profile中请求postList数据并保存在Store中,并展示出来
// store/features/postList.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
export const fetchPostList = createAsyncThunk('fetch/postList', async () => {
const url = 'https://jsonplaceholder.typicode.com/posts'
const data = await fetch(url).then((res) => res.json())
return data
})
const postListSlice = createSlice({
name: 'postList',
initialState: {
postList: []
},
reducers: {
setPostList(state, { payload }) {
state.postList = payload
}
},
extraReducers: {
[fetchPostList.fulfilled]: (state, { payload }) => {
console.log('payload', payload)
state.postList = payload
},
[fetchPostList.pending]: (state, { payload }) => {
console.log('fetchPostList.pending', payload)
},
[fetchPostList.rejected]: (state, { payload }) => {
console.log('fetchPostList.rejected', payload)
}
}
})
export const { setPostList } = postListSlice.actions
export default postListSlice.reducer
// store/index.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counter'
import postListReducer from './features/postList'
export default configureStore({
reducer: {
counter: counterReducer,
postList: postListReducer
}
})
// Profile.jsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { fetchPostList } from '../store/features/postList'
const mapStateToProps = (state) => ({
postList: state.postList.postList
})
const mapDispatchToProps = (dispatch) => ({
fetchPostList: () => dispatch(fetchPostList())
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(
class Profile extends Component {
render() {
return (
<div>
Profile
<button onClick={() => this.props.fetchPostList()}>Fetch Data</button>
<ul>
{this.props.postList.map((item, index) => (
<li key={index}>{item.title}</li>
))}
</ul>
</div>
)
}
}
)
当createAsyncThunk
创建出来的action被dispatch时,会存在三种状态:
- pending: action被发出,但是还没有最终的结果
- fulfilled: 获取到最终的结果(有返回值的结果)
- rejected: 执行过程中又错误或者抛出了异常
我们可以在createSlice
的entraReducer
中监听这些结果,根据派发action后的状态添加不同的逻辑进行处理
除了上述的写法,还可以为extraReducer
传入一个函数,函数接收一个builder
作为参数,在函数体内添加不同的case来监听异步操作的结果:
// postList.js
...
extraReducers: (builder) => {
builder.addCase(fetchPostList.fulfilled, (state, { payload }) => {
state.postList = payload
})
builder.addCase(fetchPostList.pending, (state, { payload }) => {
console.log('fetchPostList.pending', payload)
})
builder.addCase(fetchPostList.rejected, (state, { payload }) => {
console.log('fetchPostList.rejected', payload)
})
}
...
在之前的代码中,我们都是通过触发action后置的回调来更新state,那么有没有可能在请求完毕时确定性地更新store中的state?
可以当请求有结果了,在请求成功的回调中直接dispatch设置state的action
当我们通过dispatch触发异步action时可以传递额外的参数,这些参数可以在传入createAsyncThunk的回调函数的参数中获取到,同时也可以从函数的参数中获取到dispatch
与getState
函数,这样就可以在请求到数据后直接通过派发action的方式更新store中的state,下面是修改后的例子:
// postList.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
export const fetchPostList = createAsyncThunk(
'fetch/postList',
async (extraInfo, { dispatch, getState }) => {
const url = 'https://jsonplaceholder.typicode.com/posts'
const data = await fetch(url).then((res) => res.json())
dispatch(setPostList(data))
}
)
const postListSlice = createSlice({
name: 'postList',
initialState: {
postList: []
},
reducers: {
setPostList(state, { payload }) {
state.postList = payload
}
}
})
export const { setPostList } = postListSlice.actions
export default postListSlice.reducer
当然,此时异步action的状态已经不那么重要了,也就不必再return data
了,除非你需要对异常状态做额外处理,仍然可以在extraReducers
中添加异常处理回调
Redux Toolkit的数据不可变性
Redux Toolkit本质是对之前繁琐的操作进行的一次封装
我们注意到:在之前reducer对state进行更新时,必须返回一个新的state才能触发修改state = { ...state, count: count + 1 }
,但是经过Redux Toolkit的封装,我们只需要state.count += 1
,直接对状态进行赋值就可以完成状态的更新
这是因为在RTK内部使用了immutable.js
,数据不可变性
- 在React开发中,我们总是强调数据的不可变性
- 无论是类组件中的state还是redux中管理的state
- JS的编码过程里,数据的不可变性都是非常重要的
- 所以在之前我们更新state时都是通过浅拷贝来完成的
- 但是浅拷贝也存在它的缺陷:
- 当对象过大时,进行浅拷贝会造成性能的浪费
- 浅拷贝后的新对象,其深层属性仍然是旧对象的引用
Redux Toolkit底层使用了immerjs
库来保证数据的不可变性
immutablejs库的底层原理和使用方法:React系列十八 - Redux(四)state如何管理
为了节约内存,出现了新的算法Persistent Data Structure
持久化数据结构/一致性数据结构
- 用一种数据结构来保存数据
- 当数据被修改时,会返回一个新的对象,但是新的对象会尽可能复用之前的数据结构而不会对内存进行浪费
- 比如有一棵引用层级较深的树,当我们对其深层某个节点进行修改时,不会完全拷贝整棵树,而是在尽可能复用旧树结构的同时创建一棵新的树
一图胜千言:
connect的实现原理
connect函数是react-redux
提供的一个高阶函数,它返回一个高阶组件,用于将store中的state/dispatch映射为组件的props
下面一步一步手写一个connect函数,实现和库提供的connect一样的映射功能:
首先完成基本的代码搭建,connect函数接收两个参数mapStateToProps
mapDispatchToProps
返回一个高阶组件
所谓高阶组件,就是传入一个类组件,返回一个增强后的新的类组件:
// connect.js
import { PureComponent } from 'react'
export default function connect(mapStateToProps, mapDispatchToProps) {
return (WrapperComponent) =>
class InnerComponent extends PureComponent {
render() {
return <WrapperComponent {...this.props} />
}
}
}
其中,mapStateToProps
和mapDispatchToProps
都是函数,函数入参是state
与dispatch
,返回一个对象,键值对对应prop <=> state/dispatch调用
我们导入store,并且从store中获取到state和dispatch传入mapStateToProps
和mapDispatchToProps
,随后将得到的键值对以props形式传递给WrapperComponent
这样新组件就可以拿到这些状态与dispatch方法,我们可以在componentDidMount
中监听整个store,当store中的状态发生改变时,强制执行re-render
// connect.js
import { PureComponent } from 'react'
import store from '../store'
export default function connect(mapStateToProps, mapDispatchToProps) {
return (WrapperComponent) =>
class InnerComponent extends PureComponent {
componentDidMount() {
store.subscribe(() => this.forceUpdate())
}
render() {
const state = mapStateToProps(store.getState())
const dispatch = mapDispatchToProps(store.dispatch)
return <WrapperComponent {...this.props} {...state} {...dispatch} />
}
}
}
上述代码能够正常工作,但是显然每次store内state发生改变都re-render是不明智的,因为组件可能只用到了store中的某些状态
那些组件没有用到的其他状态发生改变时,组件不应该也跟着re-render,这里可以做一些优化
// connect.js
import { PureComponent } from 'react'
import store from '../store'
export default function connect(mapStateToProps, mapDispatchToProps) {
return (WrapperComponent) =>
class InnerComponent extends PureComponent {
constructor(props) {
super(props)
this.state = mapStateToProps(store.getState())
}
componentDidMount() {
store.subscribe(() => {
this.setState(mapStateToProps(store.getState()))
})
}
render() {
const state = mapStateToProps(store.getState())
const dispatch = mapDispatchToProps(store.dispatch)
return <WrapperComponent {...this.props} {...state} {...dispatch} />
}
}
}
经过优化后,每次store.state发生变化会触发setState,由React内部的机制来决定组件是否应当重新渲染
如果组件依赖的state发生变化了,那么React会替我们执行re-render,而不是每次都强制执行re-render
进一步地,我们可以补充更多细节:
- 当组件卸载时解除监听
store.subscribe
会返回一个unsubscribe
函数 用于解除监听
- 解除与业务代码store的耦合
- 目前的store来自业务代码 更优的做法是从context中动态获取到store
- 应当提供一个context Provider供用户使用
- 就像
react-redux
一样,使用connect前需要将App用Provider包裹并传入store
至此就基本完成了一个connect函数
// connect.js
import { PureComponent } from 'react'
import { StoreContext } from './storeContext'
export function connect(mapStateToProps, mapDispatchToProps) {
return (WrapperComponent) => {
class InnerComponent extends PureComponent {
constructor(props, context) {
super(props)
this.state = mapStateToProps(context.getState())
}
componentDidMount() {
this.unsubscribe = this.context.subscribe(() => {
this.setState(mapStateToProps(this.context.getState()))
})
}
componentWillUnmount() {
this.unsubscribe()
}
render() {
const state = mapStateToProps(this.context.getState())
const dispatch = mapDispatchToProps(this.context.dispatch)
return <WrapperComponent {...this.props} {...state} {...dispatch} />
}
}
InnerComponent.contextType = StoreContext
return InnerComponent
}
}
// storeContext.js
import { createContext } from 'react'
export const StoreContext = createContext(null)
export const Provider = StoreContext.Provider
// App.jsx
import React, { Component } from 'react'
import store from './store'
import Counter from './cpns/Counter'
import { Provider } from './hoc'
export default class App extends Component {
render() {
return (
<Provider value={store}>
<div>
<h1>React Redux</h1>
<Counter />
</div>
</Provider>
)
}
}
实现日志中间件logger
设想现在有需求:设计一个Redux中间件,当我们每次通过dispatch派发action时都能够在控制台输出:派发了哪个action,传递的数据是怎样的
最终实现:拦截dispatch,并且在控制台打印派发的action
// logger.js
function logger(store) {
const next = store.dispatch // 保留原始的dispatch函数
function dispatchWithLog(action) {
console.group(action.type)
console.log('dispatching', action)
const res = next.dispatch(action)
console.log('next state', store.getState())
console.groupEnd()
return res
}
store.dispatch = dispatchWithLog
}
logger(store) // 应用中间件
通过monkey patch
对原始dispatch函数进行了修改,为函数添加额外的副作用
实现redux-thunk
redux-thunk
这个库帮我们提供了派发异步函数的功能
回顾一下redux-thunk
的功能:
- 默认情况下dispatch(action)的action必须为一个JS对象
- redux-thunk允许我们传入一个函数作为action
- 函数会被调用,并且将dispatch函数和getState函数作为入参传递给这个函数action
- dispatch 允许我们在这之后再次派发action
- getState 允许我们之后的一些操作依赖原来的状态,可以获取到之前的状态
// thunk.js
function thunk(store) {
const next = store.dispatch
function dispatchWithThunk(action) {
if (typeof action === 'function') {
// pass dispatch and getState to the thunk
return action(store.dispatch, store.getState)
}
return next(action)
}
return dispatchWithThunk
}
thunk(store) // 应用中间件
需要注意的是,传递给函数action的第一个参数是经过更新后的新的dispatch
函数,这是从细节考虑:如果在函数中又派发了函数
实现applyMiddleware
当我们需要同时应用多个中间件时,可以用applyMiddleware
来对多个中间件进行组合,统一进行注册
// applyMiddleware.js
function applyMiddleware(store, ...fns) {
fns.forEach(fn => fn(store))
}
applyMiddleware(store, logger, thunk) // 使用applyMiddleware