复合组件通信:
- 父子通信「以及具备相同父亲的兄弟组件」:props属性 「或者基于ref」
- 祖先和后代「以及具备相同祖先的平行组件」:context上下文
redux/react-redux也是实现组件之间通信的技术「插件」,不管任何类型的组件,都可以基于这种方法,实现组件通信-------redux公共状态管理方案。
实际开发中,父子组件一般是基于props/ref/redux通信,其余组件的通信一般都是基于redux管理的。
Redux思想和基础操作
redux五步核心操作(操作思想):
除了上述核心五步骤外,还需同上下文对象配合:三个组件中都需要用到创建的store容器,在根组件中导入store并把其放在上下文中,后期其他组件需要,只要是根组件的后代组件,则直接获取使用即可。
redux基础操作
- 创建store,规划出reducer
dispatch派发和reducer的关系:
import { createStore } from 'redux'
/* 初始状态值 */
let initial = {
supNum: 10,
oppNum: 5
}
/* 管理员:修改STORE容器中的公共状态 */
const reducer = function reducer(state = initial, action) {
//state存储STORE容器中的公共状态【最开始没有的时候,赋值初始状态值initial】
//action:每一次基于dispatch派发的时候,传递进来的行为对象【要求必须具备type属性,存储派发的行为标识】
//为了接下来的操作中我们操作state,不会直接修改容器中的状态【要等到最后return的时候】,我们需要先克隆
state = { ...state }
//接下来我们需要基于派发的行为标识,修改STORE容器中的公共状态信息
switch (action.type) {
case 'VOTE_SUP':
state.supNum++
break
case 'VOTE_OPP':
state.oppNum++
break
default:
}
//return的内容,会整体替换STORE容器中的内容
return state
}
/* 创建STORE公共容器 */
const store = createStore(reducer)
export default store
浅克隆,只克隆第一层数据,使得reducer执行过程中对公共状态的修改与store容器中的公共状态值无关:
- 在入口中,基于上下文对象,把store放入到上下文中;需要用到store的组件,从上下文中获取
为了在各个组件中,都可以把创建的store获取到,我们可以基于上下文的方案:
- 在index.jsx中,基于ThemeContext.Provider把创建的store放在上下文中。
- 因为所有组件最后都是在index.jsx中渲染,所有组件都可以理解为index.jsx的后代组件,基于上下文方案,获取在上下文中存储的store就可以了。
src下创建ThemeContext.js上下文对象:
import React from "react";
const ThemeContext = React.createContext()
export default ThemeContext
根组件index.jsx中导入store和上下文对象,同时基于ThemeContext.Provider组件将store放入上下文中:
...
/* REDUX */
import store from './store';
import ThemeContext from './ThemeContext'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ConfigProvider locale={zhCN}>
<ThemeContext.Provider
value={{
store
}}>
<Vote />
</ThemeContext.Provider>
</ConfigProvider>
);
打印store,可以看到store提供的各种操作函数:
分别在Vote/VoteMain/VoteFooter中获取store公共容器:…
- 各组件中基于store提供的方法,完成公共状态的获取、向store的事件池中加入让组件更新的方法、任务的派发等操作
import React, { useContext, useState, useEffect } from "react";
import './Vote.less';
import VoteMain from './VoteMain';
import VoteFooter from './VoteFooter';
import ThemeContext from "../ThemeContext";
const Vote = function Vote() {
const { store } = useContext(ThemeContext)
// 获取容器中的公共状态
let { supNum, oppNum } = store.getState()
// 组件第一次渲染完毕后,把让组件更新的方法,基于store.subscribe(),放在store的事件池中
let [num, setNum] = useState(0)
const update = () => {
setNum(num + 1)
}
useEffect(() => {
// let unsubscribe = store.subscribe(让组件更新的方法)
// + 把让组件更新的方法放入store事件池中
// + 返回的unsubscribe方法执行,可以把刚才放入事件池中的方法移除掉
let unsubscribe = store.subscribe(update)
}, [])
return <div className="vote-box">
<div className="header">
<h2 className="title">React 52lkk</h2>
<span className="num">{supNum + oppNum}</span>
</div>
<VoteMain />
<VoteFooter />
</div>;
};
export default Vote;
类组件中使组件更新有更简单的方法:this.forceUpdate()
强制组件更新
import React from "react";
import ThemeContext from "../ThemeContext";
class VoteMain extends React.Component {
static contextType = ThemeContext
render() {
const { store } = this.context
// 获取容器中的公共状态
let { supNum, oppNum } = store.getState()
return <div className="main" >
<p>支持人数:{supNum}人</p>
<p>反对人数:{oppNum}人</p>
</div>;
}
// 组件第一次渲染完毕后,把让组件更新的方法,基于store.subscribe(),放在store的事件池中
componentDidMount() {
const { store } = this.context
store.subscribe(() => {
this.forceUpdate()
})
}
};
export default VoteMain;
传递的action行为对象中的type属性(行为对象标识),需要和reducer中进行判断的行为标识有对应
import React, { useContext } from "react";
import { Button } from 'antd';
import ThemeContext from "../ThemeContext";
const VoteFooter = function VoteFooter() {
const { store } = useContext(ThemeContext)
return <div className="footer">
// 点击按钮基于store.dispatch()方法派发行为对象,通知reducer执行
<Button type="primary" onClick={() => {
store.dispatch({
type: 'VOTE_SUP'
})
}}>支持</Button>
<Button type="primary" danger onClick={() => {
store.dispatch({
type: 'VOTE_OPP'
})
}}>反对</Button>
</div>;
};
export default VoteFooter;
此时:
出现问题:当点击支持/反对按钮各5次,支持反对人数正常增加,但总人数仅在第一次点击时增加1次,后续点击按钮总人数不再增加。
原因:函数组件渲染更新原理所致。函数组件的每一次更新,都是把函数组件重新执行,创建出新的闭包作用域,在其中将状态、函数等重新进行创建,内部作用域所引用的上级上下文来自新的闭包作用域。
当函数组件第一次执行,组件第一次渲染,创建闭包作用域及状态、函数,基于store.subscribe()将使组件更新的方法update添加到store事件池,此时使组件更新的方法所引用的状态始终是组件第一次渲染所创建的函数闭包作用域中的状态num=0。第一次点击按钮,修改store公共容器中的状态,立即通知store事件池中的方法执行,组件第一次更新,函数组件重新执行创建新的函数闭包作用域,此时内部num状态修改为1,创建新的update函数;后续再点击按钮,公共状态更改,通知执行的store事件池中的方法依然是引用组件第一次渲染产生闭包中状态num=0的函数update,此时始终执行setNum(0+1),触发useState()创建的修改状态的方法内部优化机制,(Vote)函数组件无法更新。
解决方法:每一次组件更新,都把最新创建的update方法放入事件池中,保证update的上级上下文是最新的闭包【num是最新的状态值】
let [num, setNum] = useState(0)
const update = () => {
setNum(num + 1)
}
useEffect(() => {
// let unsubscribe = store.subscribe(让组件更新的方法)
// + 把让组件更新的方法放入store事件池中
// + 返回的unsubscribe方法执行,可以把刚才放入事件池中的方法移除掉
let unsubscribe = store.subscribe(update)
// 在上一次组件释放的时候,把上一次放在事件池中的方法移除掉
return () => {
unsubscribe()
}
}, [num])
替代方法:还是只在第一次渲染完往事件池中加入一个方法,但必须保证这个方法执行,修改的状态值和之前的状态值不一样。
- 可以把状态修改为时间戳
- 也可以修改为随机数
let [_, setNum] = useState(0)
useEffect(() => {
store.subscribe(() => {
setNum(+new Date())
})
}, [])
总结:
redux具体的代码编写顺序
- 创建store,规划出reducer「当中的业务处理逻辑可以后续不断完善,但是最开始reducer的这个架子需要先搭建起来」
- 在入口中,基于上下文对象,把store放入到上下文中;需要用到store的组件,从上下文中获取
- 组件中基于store,完成公共状态的获取、和任务的派发
- 使用到公共状态的组件,必须向store的事件池中加入让组件更新的办法;只有这样,才可以确保,公共状态改变后立即通知事件池中的方法依次执行后,可以让组件更新,才可以获取最新的状态进行绑定
redux部分源码解析
src文件夹下新建myredux.js:
import _ from './assets/utils'
/* 实现redux的部分源码 */
export const createStore = function createStore(reducer) {
if (typeof reducer !== 'function') throw new Error('Expected the root reducer to be a function')
let state,// 存放公共状态
listeners = []// 事件池
/* 获取store公共容器状态 */
const getState = function getState() {
// 返回公共状态信息
return state
}
/* 将使组件更新的方法添加到事件池中 */
const subscribe = function subscribe(listener) {
// 规则校验
if (typeof listener !== 'function') throw new TypeError('Expected the listener to be a function')
// 把转入的方法(使组件更新的方法)添加到事件池中【需要做去重处理】
if (!listeners.includes(listener)) {
listeners.push(listener)
}
// 返回一个移除添加到事件池中方法的函数
return function unsubscribe() {
let index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
/* 派发任务通知reuducer执行 */
const dispatch = function dispatch(action) {
// 规则校验
if (!_.isPlainObject(action)) throw new TypeError('Actions must be plane objects')
if (typeof action.type === 'undefined') throw new TypeError('Actions may not have an undefined "type" property')
// 把reducer执行,传递公共状态、行为对象,接收执行的返回值,替换公共状态
state = reducer(state, action)
// 当状态更改,通知事件池中的方法执行
listeners.forEach(listener => {
listener()
})
return action
}
/* redux内部会默认进行一次dispatch派发,目的是给公共容器中的状态赋值初始值 */
const randomString = () => Math.random().toString(36).substring(8).split('').join('.')
dispatch({
// type: Symbol()
type: "@@redux/INIT" + randomString()
})
// 返回创建的store对象
return {
getState,
subscribe,
dispatch
}
}
入口文件index.js中从上述文件中导入createStore
import { createStore } from '../myredux'
结合redux五步核心步骤及注释理解源码。
遗留问题:为什么不能直接修改store容器中的公共状态,而需要通过reducer进行状态修改呢?
这其实是一种公共状态管理思想
redux工程化
真实的复杂项目中,总的store只有一个,但是派发的行为标识很多(比如几百个),reducer会十分臃肿;并且派发的行为标识由不止一个开发人员编写,如果都操作这一个文件容易出错,不利于团队协作开发。
因而在真正的项目中,我们一定会把状态和reducer的管理,按照模块化进行划分。
redux工程化的第一步: 按照模块,把reducer(管理的状态及修改状态的逻辑)进行单独管理,每个模块都有自己的reducer;最后,我们还要把所有的reducer进行合并,合并为一个,赋值给我们创建的store。(一个模块叫做vote ,一个模块叫做 personal ……)
src文件夹下创建reducers文件夹
voteReducer.js:
/* vote板块下的reducer */
import _ from '@/assets/utils'
const initial = {
supNum: 15,
oppNum: 5,
num: 0
}
export default function voteReducer(state = initial, action) {
state = _.clone(true, state)
switch (action.type) {
case 'VOTE_SUP':
state.supNum++
break
case 'VOTE_OPP':
state.oppNum++
break
default:
}
return state
}
personalReducer.js:
/* personal板块下的reducer */
import _ from '@/assets/utils'
const initial = {
num: 100,
info: null
}
export default function personalReducer(state = initial, action) {
state = _.clone(true, state)
switch (action.type) {
case 'PERSONAL_INFO':
state.info = action.payload
break
default:
}
return state
}
//模拟未来的业务操作:派发任务通知personalReducer执行
dispatchEvent({
type:'PERSONAL_INFO',
payload:{...}
})
index.js:
import { combineReducers } from 'redux'
import voteReducer from './voteReducer'
import personalReducer from './personalReducer'
const reducer = combineReducers({
vote: voteReducer,
personal: personalReducer
})
export default reducer
/* 合并各个模块的reducer,最后创建出一个总的reducer
const reducer = combineReducers({
vote: voteReducer,
personal: personalReducer
})
+ reducer就是最后合并的总的reducer
+ 此时容器中的公共状态,会按照我们设置的成员名字,分模块进行管理
state = {
vote: {
supNum: 15,
oppNum: 5,
num: 0
},
personal: {
num: 100,
info: null
}
}
*/
为模块reducer设置的什么名字,最后redux容器中的公共状态信息就会以这个名字作为“模块名”分别管理自己的状态信息。
这样处理完,即便模块之前的状态名一样了,最后也不会冲突。
以后基于store.getState()获取的状态是总的公共状态信息,要想获得各个模块的状态,需要基于设置的模块名获取,即store.getState().vote / store.getState().personal
把合并后的总的reducer,赋值给创建的store容器:
import { createStore } from 'redux'
import reducer from './reducers/index'
/* 创建STORE公共容器 */
const store = createStore(reducer)
export default store
此时redux容器中的状态已经变为:
state = {
vote: {
supNum: 15,
oppNum: 5,
num: 0
},
personal: {
num: 100,
info: null
}
}
各组件获取公共容器中的状态:
let { supNum, oppNum } = store.getState().vote
派发的操作不需要改动,每一次派发后,都会去所有reducer进行逐一匹配「用派发的行为标识,和每个模块reducer中判断的行为标识进行比较」;和谁匹配成功,就执行谁的逻辑。
redux工程化的第二步: 每一次dispatch派发的时候,都会去每个模块的reducer中找一遍,把所有和派发行为标识匹配的逻辑执行。此时可能存在问题:团队协作开发的时候,因为开发的人多,最后很可能派发的行为标识会有冲突。
所以我们一定要保证,不管哪个模块,哪个组件,我们派发的行为标识必须是唯一的。-----------------基于“宏管理(统一管理)”,让所有派发的行为标识具备唯一性。
store文件夹下新建action-types.js:
统一管理需要派发的行为标识:
- 为了保证不冲突,我们能一般都是这样命名:模块名_派发的行为标识【大写】
- 变量和存储的值是一致的
- 所有需要派发的行为标识,都在这里定义
export const VOTE_SUP = 'VOTE_SUP'
export const VOTE_OPP = 'VOTE_OPP'
export const PERSON_SUP = 'PERSON_SUP'
export const PERSON_INFO = 'PERSON_INFO'
voteReducer、personalReducer以及VoteFooter.jsx中,原本判断的行为标识以及派发的行为标识不需要自己单独写字符串了,而是把action-types中统一管理的标识拿来进行判断。保证不冲突,避免粗心大意和出错。
import * as TYPES from '../action-types'
...
case TYPES.VOTE_SUP:
...
case TYPES.VOTE_OPP:
...
case TYPES.PERSON_INFO:
...
store.dispatch({
type: TYPES.VOTE_SUP
})
...
store.dispatch({
type: TYPES.VOTE_OPP
})
redux工程化第三步: 把派发的行为对象,按照模块进行统一的管理。
store文件夹下新建actions文件夹
voteAction.js:
vote板块要派发的行为对象管理:voteActions包含好多方法,每一个方法执行,都返回要派发的行为对象
import * as TYPES from '../action-types'
const voteAction = {
support() {
return {
type: TYPES.VOTE_SUP
}
},
oppose() {
return {
type: TYPES.VOTE_OPP
}
}
}
export default voteAction
personalAction.js:
personal板块要派发的行为对象管理:personalActions包含好多方法,每一个方法执行,都返回要派发的行为对象
import * as TYPES from '../action-types'
const personalAction = {
/* info() {
return {
type: TYPES.PERSON_INFO
}
} */
//......
}
export default personalAction
index.js:
把各个板块的action合并为一个action即可
import voteAction from "./voteAction";
import personalAction from "./personalAction";
const action = {
vote: voteAction,
personal: personalAction
}
export default action
合并后的action={
vote:{
support() {
return {
type: TYPES.VOTE_SUP
}
},
oppose() {
return {
type: TYPES.VOTE_OPP
}
}
},
personal:{
//...
}
}
需要派发行为对象的地方(VoteFooter.jsx),导入action并使用相关方法派发
import action from "../store/actions";
const VoteFooter = function VoteFooter() {
const { store } = useContext(ThemeContext)
return <div className="footer">
<Button type="primary" onClick={() => {
// store.dispatch({
// type: TYPES.VOTE_SUP
// })
store.dispatch(action.vote.support())
}}>支持</Button>
<Button type="primary" danger onClick={() => {
// store.dispatch({
// type: TYPES.VOTE_OPP
// })
store.dispatch(action.vote.oppose())
}}>反对</Button>
</div>;
};
从目前来看,此工程化步骤,不仅没有啥好处,反而让之前的操作更麻烦了。之前每次派发,把派发的行为对象直接写出来即可;现在,还需要把派发的行为对象,放到store/actions的某个版块下,靠方法执行才能返回我们需要的行为对象。
此操作的意义,我们称之为创建actionCreator,在我们接下来处理react-redux的时候,会非常的有用。
combineReducer源码部分解析:
store目录下新建myredux-combineReducers.js
const combineReducers = function combineReducers(reducers) {
//reducers是一个对象,以键值对存储了:模块名&每个模块的reducer
let reducerskeys = Reflect.ownKeys(reducers)
//reducerskeys:['vote','personal']
/* 返回一个合并的reducer
+每一次dispatch派发,都是把这个reducer执行
+state就是redux容器中的公共状态
+action就是派发时候传递进来的行为对象
*/
return function reducer(state = {}, action) {
// 把reducers中的每一个小的reducer(每个模块的reducer)执行;把对应模块的状态/action行为对象传递进来;返回的值替换当前模块下的状态
let nextState = {}
reducerskeys.forEach(key => {
//key:'vote'/'personal'模块名
//reducer每个模块的reducer
let reducer = reducers[key]
nextState[key] = reducer(state[key], action)
})
return nextState
}
}
export default combineReducers
/* // 注意:第一次执行redux内部会默认进行一次派发,此时根据redux源码,给公共容器中的的状态(按模块)赋值初始值
// 后续派发
store.diapatch({
type:'VOTE_SUP',
...
}) */