Search K
Appearance
Appearance
什么是副作用?
在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;
副作用往往是产生bug的 “温床”。
因为你可以安心的编写和安心的使用(不会产生额外的副作用);
你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;(解耦)
你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
React
中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:
在Redux
中Reducer
也被要求是一个纯函数
redux
核心理念包括:store
、action
、reducer
store
store
:可以理解为仓库
的意思,建立store
的目的是使用统一的规范来管理、操作数据数据,以达到跟踪数据变化的目的。action
reducer
简单概括:
store
是统一的数据仓库,可以向其中添加多个数据(state)
,可以通过store.getState
来获取当前的state
我们只能通过action
(修改的动作)来修改store
的数据,这样可以记录数据变化,通常action
中都会有type
属性,也可以携带其他的数据,通过dispatch
来派发(触发)action
创建store
时必须创建reducer
,reducer
是一个纯函数,接收参数是state
和action
,通过reducer
将旧state
和actions
联系在一起,并且返回一个新的State
完成数据更新
代码示例:
redux
工具函数
const {createStore} = require("redux")
// 初始化数据
const initialState = {
name: "why",
count: 100
}
// 定义reducer函数,要求是一个纯函数
// 接收两个参数
// 参数一:store中保存的state
// 参数二:更次需要更新的action(dispatch传入的action)
// 返回值:它的返回值会作为store之后存储的state
// 每次数据变化的时候都会重新执行一次reducer
const reducer = (state = initialState, action) => {
if (action.type === 'change_name') {
// 这里很关键,这里赋值用到了浅拷贝,创建了一个新的对象
// 并没有直接修改state,避免了副作用,符合纯函数的要求
return {...state, name: action.name}
} else if (action.type === 'add_count') {
return {...state, count: state.count + action.count}
}
return state
}
// 创建store
const store = createStore(reducer)
module.exports = store
使用示例:
const store = require("../index")
console.log("store", store.getState()) // store { name: 'why', count: 100 }
// 修改name的action
const action = {type: "change_name", name: "zhangsan"}
store.dispatch(action)
// 修改count的action
const action2 = {type: "add_count", count: 10}
store.dispatch(action2)
console.log("更新后的state", store.getState()) //更新后的state { name: 'zhangsan', count: 110 }
通过subscribe
可以订阅store
中的数据变化
订阅数据变化
const store = require("../index")
store.subscribe(() => {
console.log("subscribe state", store.getState())
})
取消订阅数据变化
const store = require("../index")
// store.subscribe的返回值就是取消订阅函数
const unsubscribe= store.subscribe(() => {
console.log("subscribe state", store.getState())
})
unsubscribe()
在React
中一般是在componentDidMount
中订阅数据变化,然后在componentWillUnmount
中取消订阅
上面已经简单体验了Redux
,但在实际项目中,我们会对Redux
进行模块化划分
接下来结合实际开发对代码进行优化
1、将action
的生成过程统一放到actionCreator.js
单独的文件中,dispatch
调用时只需要传入值即可
2、将actionCreator
和reducer
对应的字符串是一致的可以抽离到常量中,避免编写时不一致
3、将reducer
和initialState
放到一个独立文件夹reducer.js
中
actionCreator.js
const {ADD_NUMBER, CHANGE_NAME} = require("./constants");
const changeNameAction = (name) => ({
type: CHANGE_NAME,
name
})
const addNumberAction = (count) => ({
type: ADD_NUMBER,
count
})
module.exports = {changeNameAction, addNumberAction}
constants.js
const ADD_NUMBER = 'add_count';
const CHANGE_NAME = 'change_name';
module.exports = {
ADD_NUMBER,
CHANGE_NAME,
}
reducer.js
// 初始化数据
const {CHANGE_NAME, ADD_NUMBER} = require("./constants");
// 初始化state
const initialState = {
name: "why",
count: 100
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case CHANGE_NAME:
return {...state, name: action.name}
case ADD_NUMBER:
return {...state, count: state.count + action.count}
default:
return state
}
}
module.exports = reducer
index.js
const {createStore} = require("redux")
const reducer = require("./reducer");
// 创建store
const store = createStore(reducer)
module.exports = store
使用示例:
const store = require("../src/01_使用state中的数据")
const {addNumberAction, changeNameAction} = require("./01_使用state中的数据/actionCreator");
console.log("store", store.getState())
store.dispatch(changeNameAction("zhangsan"))
store.dispatch(addNumberAction(10))
console.log("更新后的state", store.getState())
首先封装store
相关函数 store/index.js
import {createStore} from "redux"
import reducer from "./reducer";
const store = createStore(reducer)
export default store
store/actionCreator.js
const {CHANGE_COUNT} = require("./constants");
export const changeCount = (count) => ({
type: CHANGE_COUNT,
count
})
store/constants.js
export const CHANGE_COUNT = 'change_count';
store/reducer.js
import {CHANGE_COUNT} from "./constants";
const initialState = {
count: 100
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case CHANGE_COUNT:
return {...state, count: action.count}
default:
return state
}
}
export default reducer
使用示例: App.jsx
import React from "react"
import Home from "./pages/home"
import Profile from "./pages/profile"
import store from "./store"
export default class App extends React.PureComponent {
constructor() {
super()
this.state = {
count: store.getState().count,
}
}
componentDidMount() {
store.subscribe(() => {
const state = store.getState()
this.setState({
count: state.count
})
})
}
render() {
const {count} = this.state
return (
<div>
<h2>
App Counter: {count}
</h2>
<div style={{border: '1px solid red'}}>
<Home/>
<Profile/>
</div>
</div>
)
}
}
home.jsx
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import store from "../store";
import {changeCount} from "../store/actionCreator";
let unsubscribe = null
class Home extends Component {
constructor() {
super()
this.state = {
count: store.getState().count,
}
}
componentDidMount() {
unsubscribe = store.subscribe(() => {
console.log("变化:", store.getState())
const state = store.getState()
this.setState({
count: state.count
})
})
}
componentWillUnmount() {
unsubscribe()
}
changeCount(count) {
store.dispatch(changeCount(count))
}
render() {
return (
<div>
<h2>home</h2>
<span>当前计数:{this.state.count}</span>
<button onClick={() => {
this.changeCount(this.state.count + 1)
}}>+1
</button>
<button onClick={() => {
this.changeCount(this.state.count + 5)
}}>+5
</button>
</div>
);
}
}
Home.propTypes = {};
export default Home;
安装:
yarn add react-redux
index.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import {Provider} from "react-redux"
import store from "./store";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App/>
</Provider>
</React.StrictMode>
);
about.jsx
import React, {Component} from 'react';
import {connect} from "react-redux";
import {changeCount} from "../store/actionCreator";
class About extends Component {
render() {
return (
<div>
<h2>About</h2>
<span>About:{this.props.count}</span>
<button onClick={() => {
this.props.changeCount(this.props.count + 10)
}}>+10
</button>
<button onClick={() => {
this.props.changeCount(this.props.count - 10)
}}>-10
</button>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
count: state.count,
})
const mapDispatchToProps = (dispatch, ownProps) => ({
changeCount: (count) => dispatch(changeCount(count))
})
export default connect(mapStateToProps, mapDispatchToProps)(About);
redux-thunk
对redux
进行了扩展,使得dispatch
可以接收并执行一个函数,之前的dispatch
只能接受对象,原理是利用了中间件
下面以一个实际项目场景作为例子,例如我们需要在Redux
请求一个获取bannerList
的接口,然后将数据存到Store
中
代码示例:
store/index.jsx
import {applyMiddleware, createStore} from "redux"
import reducer from "./reducer";
import {thunk} from "redux-thunk";
// 扩展中间件
const store = createStore(reducer, applyMiddleware(thunk))
export default store
store/actionCreator.js
export const changeBanner = (banner) => ({
type: CHANGE_BANNER,
banner
})
export const fetchBannerDataAction = (params) => {
return (dispatch, getState) => {
const banner = [
{
id: 1,
title: 'banner title1'
},
{
id: 2,
title: 'banner title2'
},
{
id: 3,
title: 'banner title3'
},
{
id: 4,
title: 'banner title4'
}
]
console.log("参数", params)
console.log("执行异步网络请求")
// 延迟模拟网络请求
setTimeout(() => {
dispatch(changeBanner(banner))
}, 2000)
}
}
store/constants.js
export const CHANGE_BANNER = 'change_banner';
store/reducer.js
import * as Constans from "./constants";
const initialState = {
count: 100,
banner: []
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case Constans.CHANGE_COUNT:
return {...state, count: action.count}
case Constans.CHANGE_BANNER:
console.log("更新CHANGE_BANNER")
return {...state, banner: action.banner}
default:
return state
}
}
export default reducer
代码中使用 about.jsx
import React, {Component} from 'react';
import {connect} from "react-redux";
import {changeCount, fetchBannerDataAction} from "../store/actionCreator";
class About extends Component {
componentDidMount() {
// 可以直接在props中使用
this.props.fetchBannerData()
}
render() {
const element = (
(this.props.banner.length) && this.props.banner.map((item) => {
return (
<li key={item.id}>title: {item.title}</li>
)
})
)
return (
<div>
<h2>About</h2>
<span>About:{this.props.count}</span>
<button onClick={() => {
this.props.changeCount(this.props.count + 10)
}}>+10
</button>
<button onClick={() => {
this.props.changeCount(this.props.count - 10)
}}>-10
</button>
<h2>banner</h2>
<ul>
{element}
</ul>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
count: state.count,
banner: state.banner
})
const mapDispatchToProps = (dispatch, ownProps) => ({
changeCount: (count) => dispatch(changeCount(count)),
fetchBannerData: () => {
// redux-thunk进行了扩展,使得dispatch可以接收并执行一个函数,之前的dispatch只能接受对象
dispatch(fetchBannerDataAction({page: 1}))
}
})
export default connect(mapStateToProps, mapDispatchToProps)(About);
redux-devtools
react-developer-tools 第二个工具是react-developer-tools
,类似vue-developer-tools
,可以在谷歌商店下载
在实际开发的过程中,如果我们所有需要管理的数据都放在同一个文件夹代码会比较混乱,正确的做法是根据业务或者功能进行模块化拆分,下面假设我们有两个功能模块banner
和counter
,接下来对其进行模块化拆分
拆分后的模块大概长这样:
banner
模块: store/banner/actionCreator.js
import * as Constants from "./constants";
export const changeBanner = (banner) => ({
type: Constants.CHANGE_BANNER,
banner
})
export const fetchBannerDataAction = (params) => {
return (dispatch) => {
const banner = [
{
id: 1,
title: 'banner title1'
},
{
id: 2,
title: 'banner title2'
},
{
id: 3,
title: 'banner title3'
},
{
id: 4,
title: 'banner title4'
}
]
console.log("参数", params)
console.log("执行异步网络请求")
setTimeout(() => {
dispatch(changeBanner(banner))
}, 2000)
}
}
store/banner/constants.js
export const CHANGE_BANNER = 'change_banner';
store/banner/reducer.js
import * as Constans from "./constants";
const initialState = {
banner: []
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case Constans.CHANGE_BANNER:
console.log("更新CHANGE_BANNER")
return {...state, banner: action.banner}
default:
return state
}
}
export default reducer
store/banner/index.js
import reducer from './reducer';
export default reducer
export * from "./actionCreator"
counter
模块: store/counter/actionCreator.js
import * as Constans from "./constants";
export const changeCount = (count) => ({
type: Constans.CHANGE_COUNT,
count
})
store/counter/constants.js
export const CHANGE_COUNT = 'change_count';
store/counter/reducer.js
import * as Constans from "./constants";
const initialState = {
count: 100
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case Constans.CHANGE_COUNT:
return {...state, count: action.count}
default:
return state
}
}
export default reducer
store/counter/index.js
import reducer from './reducer';
export default reducer
export * from "./actionCreator"
关键点主要在store/index
文件,这里使用了redux/combineReducers
函数对多个reducer
进行一个合并
import {applyMiddleware, createStore, compose, combineReducers} from "redux"
import {thunk} from "redux-thunk";
import Counter from "./counter";
import Banner from "./banner"
// 扩展redux-devtools中间件
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 扩展thunk中间件
let thunkEnhancers = applyMiddleware(thunk);
// 组合各个模块的reducer
const reducer = combineReducers({
counter: Counter,
banner: Banner
})
const store = createStore(reducer, composeEnhancers(thunkEnhancers))
export default store
combineReducers
原理是对各个模块的reducer
进行一个组合,它做的事情跟以下代码类似:
// combineReducers函数原理
function myCombineReducers(state = {}, action) {
return {
counter: counterReducer(state.counter, action),
banner: bannerReducer(state.banner, action)
}
}
上面的示例代码我们使用自己编写的combineReducers
函数后运行代码也能得到预期结果 store/index
import {applyMiddleware, createStore, compose, combineReducers} from "redux"
import {thunk} from "redux-thunk";
import Counter from "./counter";
import Banner from "./banner"
import counterReducer from "./counter";
import bannerReducer from "./banner";
// 扩展redux-devtools中间件
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 扩展thunk中间件
let thunkEnhancers = applyMiddleware(thunk);
// 组合各个模块的reducer
// const reducer = combineReducers({
// counter: Counter,
// banner: Banner
// })
// combineReducers函数原理
function myCombineReducers(state = {}, action) {
return {
counter: counterReducer(state.counter, action),
banner: bannerReducer(state.banner, action)
}
}
const store = createStore(myCombineReducers, composeEnhancers(thunkEnhancers))
export default store
connect
接手两个参数:mapStateToPropsFn
和mapDispatchToPropsFn
,本质上connect
的执行结果是一个高阶组件,参数接收了一个组件,返回值也是一个组件
import store from "../store";
import {PureComponent} from "react"
/**
* 调用示例
* const mapStateToProps = (state) => ({
* count: state.counter.count
* })
*
* const mapDispatchToProps = (dispatch) => ({
* addNumber(count) {
* dispatch(addNumber(count))
* },
* // 两种写法都可以
* // addNumber: (count) => dispatch(addNumber(count))
* })
*
* export default connect(mapStateToProps, mapDispatchToProps)(Home);
*/
/**
* 函数返回结果是一个函数fn,fn接收参数是一个
* react组件,目标是将mapStateToPropsFn、mapDispatchToPropsFn返回
* 结果的对象作为参数传入到目标组件中
* @param mapStateToPropsFn
* @param mapDispatchToPropsFn
*/
export function myConnect(mapStateToPropsFn, mapDispatchToPropsFn) {
let unsubscribe = null
const dispatch = mapDispatchToPropsFn(store.dispatch)
/**
* 创建一个高阶函数,将props混入到WrapperCpn
* @param WrapperCpn 待混入props的组件
*/
const enhanceProps = (WrapperCpn) => {
return class extends PureComponent {
constructor(props) {
super(props);
this.state = {
// 将state声明在函数内部以方便在生命周期订阅store变化
storeState: mapStateToPropsFn(store.getState())
}
}
componentDidMount() {
unsubscribe = store.subscribe(() => {
this.setState({
storeState: mapStateToPropsFn(store.getState())
})
})
}
componentWillUnmount() {
unsubscribe()
}
render() {
return <WrapperCpn {...this.props} {...this.state.storeState} {...dispatch} />
}
}
}
return enhanceProps
}
使用示例:
import React, {PureComponent} from 'react';
import {addNumber} from "../store/feature/counter";
import {myConnect} from "../utils/myConnect";
class Home extends PureComponent {
render() {
return (
<div>
<h2>
Home counter: {this.props.count}
</h2>
<div>
<button onClick={() => {
this.props.addNumber(1)
}}>+1
</button>
<button onClick={() => {
this.props.addNumber(5)
}}>+5
</button>
</div>
</div>
);
}
}
const mapStateToProps = (state) => ({
count: state.counter.count
})
const mapDispatchToProps = (dispatch) => ({
// 这种方式还不支持
// addNumber(count) {
// dispatch(addNumber(count))
// },
addNumber: (count) => dispatch(addNumber(count))
})
export default myConnect(mapStateToProps, mapDispatchToProps)(Home);
现在myConnect
还有一些缺陷,就是依赖于项目中的store
,当然也可以扩展myConnect
第三个参数,从外部传入store
进来,但这样会多写很多代码,我们可以参考react-redux
的做法,借助React
的Context
解决这个问题,这样我们的connect
函数就不需要依赖项目中的store
重构后的代码结构:
src/hoc/StoreContext.js
import React from 'react'
export const StoreContext = React.createContext()
src/hoc/myConnect.js
import {PureComponent} from "react"
import {StoreContext} from "./StoreContext";
/**
* 调用示例
* const mapStateToProps = (state) => ({
* count: state.counter.count
* })
*
* const mapDispatchToProps = (dispatch) => ({
* addNumber(count) {
* dispatch(addNumber(count))
* },
* // 两种写法都可以
* // addNumber: (count) => dispatch(addNumber(count))
* })
*
* export default connect(mapStateToProps, mapDispatchToProps)(Home);
*/
/**
* 函数返回结果是一个函数fn,fn接收参数是一个
* react组件,目标是将mapStateToPropsFn、mapDispatchToPropsFn返回
* 结果的对象作为参数传入到目标组件中
* @param mapStateToPropsFn
* @param mapDispatchToPropsFn
*/
export function myConnect(mapStateToPropsFn, mapDispatchToPropsFn) {
/**
* 创建一个高阶函数,将props混入到WrapperCpn
* @param WrapperCpn 待混入props的组件
*/
const enhanceProps = (WrapperCpn) => {
class ConnectCpn extends PureComponent {
constructor(props, context) {
super(props);
this.state = {
// 将state声明在函数内部以方便在生命周期订阅store变化
storeState: mapStateToPropsFn(context.getState())
}
}
componentDidMount() {
this.unsubscribe = this.context.subscribe(() => {
this.setState({
storeState: mapStateToPropsFn(this.context.getState())
})
})
}
componentWillUnmount() {
this.unsubscribe()
}
render() {
const dispatchs = mapDispatchToPropsFn(this.context.dispatch)
return <WrapperCpn {...this.props} {...this.state.storeState} {...dispatchs} />
}
}
ConnectCpn.contextType = StoreContext
return ConnectCpn
}
return enhanceProps
}
src/hoc/index.js
export { myConnect } from './myConnect';
export { StoreContext } from './StoreContext';
入口文件
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import {Provider} from "react-redux";
import store from './store';
import {StoreContext} from "./hoc/index";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<StoreContext.Provider value={store}>
<App/>
</StoreContext.Provider>
</Provider>
);
使用示例:
import React, {PureComponent} from 'react';
import {addNumber} from "../store/feature/counter";
import {myConnect} from "../hoc/index";
class Home extends PureComponent {
render() {
return (
<div>
<h2>
Home counter: {this.props.count}
</h2>
<div>
<button onClick={() => {
this.props.addNumber(1)
}}>+1
</button>
</div>
</div>
);
}
}
const mapStateToProps = (state) => ({
count: state.counter.count
})
const mapDispatchToProps = (dispatch) => ({
addNumber: (count) => dispatch(addNumber(count))
})
export default myConnect(mapStateToProps, mapDispatchToProps)(Home);
代码如下,我们只需要在抛出store
之前,编写一个函数对store.dispatch
进行修改即可 store/index
/**
* 传入store,当调用store.dispatch的时候打印日志
* @param store
*/
function patchLog(store){
const next = store.dispatch
function dispatchAndLog(action){
console.log("执行dispatch")
next(action)
console.log("dispatch执行完毕")
}
store.dispatch = dispatchAndLog
return store
}
export default patchLog(store)
store/index.js
/**
* 实现thunk中间件支持执行函数功能
* @param store
*/
function patchThunk(store){
const next = store.dispatch
function dispatchAndThunk(action){
if(typeof(action) === "function"){
// 传入的是函数
action(next,store.getState)
}else{
// 传入的是对象
next(action)
}
}
store.dispatch = dispatchAndThunk;
}
const store = createStore(reducer)
patchThunk(store)
export default store
/**
* 实现thunk中间件支持执行函数功能
* @param store
*/
function patchThunk(store){
const next = store.dispatch
function dispatchAndThunk(action){
if(typeof(action) === "function"){
// 传入的是函数
action(next,store.getState)
}else{
// 传入的是对象
next(action)
}
}
store.dispatch = dispatchAndThunk;
}
/**
* 传入store,当调用store.dispatch的时候打印日志
* @param store
*/
function patchLog(store){
const next = store.dispatch
function dispatchAndLog(action){
console.log("执行dispatch")
next(action)
console.log("dispatch执行完毕")
}
store.dispatch = dispatchAndLog
}
/**
* 传入多个中间件进行合并
* @param store
* @param middlewares
*/
function applyMiddleware(store,...middlewares){
middlewares.forEach(fn => {
fn(store)
})
}
const store = createStore(reducer)
applyMiddleware(store,patchLog,patchThunk)
export default store