组件通信
父子组件通信
vue中父子组件通信,父传子:props,子组件通知父组件:this.$emit
在react中,父子组件通信
父传子:使用props
属性传递
子组件通知父组件修改数据可以调用父组件传递的函数去执行修改。
父组件App.js
import React, { Component } from 'react'
import Child from './components/Child'
export default class App extends Component {
state = {
username: '张三'
}
updateName = (name) => {
this.setState({
username: name
})
}
render() {
return (
<div>
<Child username={this.state.username} updateName={this.updateName}></Child>
</div>
)
}
}
子组件Child.jsx
import React, { Component } from 'react'
export default class Child extends Component {
changeName = () => {
this.props.updateName('王五');
}
render() {
return (
<div>
<p>child子组件</p>
姓名:{this.props.username}
<button onClick={this.changeName}>修改姓名</button>
</div>
)
}
}
兄弟组件通信
通过将父组件作为中转站去通知兄弟组件修改数据,达到兄弟组件通信。
父组件App.jsx
import React, { Component } from 'react'
import Child from './components/Child'
import ChildTwo from './components/ChildTwo'
export default class App extends Component {
state = {
username: '张三'
}
childRef = React.createRef()
updateName = (name) => {
this.setState({
username: name
})
}
changeChildAge = (age) => {
this.childRef.current.changeAge(age);
}
render() {
return (
<div>
<Child ref={this.childRef} username={this.state.username} updateName={this.updateName}></Child>
<ChildTwo changeChildAge={this.changeChildAge}></ChildTwo>
</div>
)
}
}
子组件Child.jsx
import React, { Component } from 'react'
export default class Child extends Component {
state = {
age: 20
}
changeAge = (age) => {
this.setState({
age
})
}
changeName = () => {
this.props.updateName('王五');
}
render() {
return (
<div>
<p>child子组件</p>
姓名:{this.props.username}
<br />
年龄:{this.state.age}
<button onClick={this.changeName}>修改姓名</button>
</div>
)
}
}
子组件ChildTwo.jsx
import React, { Component } from 'react'
export default class ChildTwo extends Component {
changeChildAge = () => {
this.props.changeChildAge(30);
}
render() {
return (
<div>
<p>ChildTwo</p>
<button onClick={this.changeChildAge}>修改child中的名字</button>
</div>
)
}
}
这种方式可以实现兄弟组件通信,但是如果组件嵌套太深,导致通信需要一层一层网上传,然后一层一层往下传递,这个过程非常繁琐,以后维护不方便。
基于事件总线的通信
事件总线的方式eventBus
,是一种设计模式,叫做订阅发布,设计模式有23种,他们都是一种思想,是前辈们总结出来的一种经验和思想,能够帮助我们快速解决问题,比如:单例模式、策略模式、订阅发布等等。
react中本身没有提供发布订阅模式,我们需要安装第三方插件events
:
events API
- 监听事件:addListener(‘事件名称’,执行函数)
- 触发事件:emit(‘事件名称’,参数)
- 移除事件:removeListener(‘事件名称’, 执行函数)
-
安装
events
插件npm i events # or yarn add events
-
创建eventBus文件
utils/eventBus.js
import { EventEmitter } from 'events'; export default new EventEmitter(); // 生成一个eventBus
-
监听事件
Child.jsx
import React, { Component } from 'react' import eventBus from '../utils/eventBus' export default class Child extends Component { state = { age: 20 } componentDidMount() { eventBus.addListener('changeAge', this.changeAge) } changeAge = (age) => { console.log(age) this.setState({ age }) } }
-
触发事件
ChildTwo.jsx
import React, { Component } from 'react' import eventBus from '../utils/eventBus' export default class ChildTwo extends Component { changeChildAge = () => { eventBus.emit('changeAge', 18) } render() { return ( <div> <p>ChildTwo</p> <button onClick={this.changeChildAge}>修改child中的名字</button> </div> ) } }
-
移除事件
Child.jsx
import React, { Component } from 'react' import eventBus from '../utils/eventBus' export default class Child extends Component { componentWillUnmount() { eventBus.removeListener('changeAge', this.changeAge) } }
事件总线的方式能让组件之间直接通信,但是存在问题:
当项目中大面积使用事件总线,监听和触发事件非常分散,导致以后维护不好维护,不知道哪里在监听哪里在触发。
建议:事件总线可以少用,不要大面积使用。
最好使用状态管理库,在react中通常使用的是redux。
redux三大核心概念
react本身没有提供相关的仓库管理插件,react社区非常庞大,社区提供了一个仓库管理插件redux
,相当于vuex可以对数据进行管理。
在之前将的是redux
,@reduxjs/toolkit
,它包括了redux的用法。
为什么redux官网推荐使用@reduxjs/toolkit
,其中原因之一是因为redux在使用时非常繁琐,需要对文件进行拆分以及合并。
市面上关于状态管理的库,比如vuex(借鉴了redux的思想,根据vue语法特点去进行开发,方便vue使用者去使用)、redux(任何框架都可以使用,所以使用起来不是那么舒服)、mbox(语法采用装饰器语法)、zustand、@reduxjs/toolkit
(redux官方推荐使用)。
这是用了redux和不同redux的区别:
vuex图解
redux图解
store:redux仓库数据存放的地方,一般只会存在一个store仓库
action:作为动作描述期,是一个对象,描述了这个动作是干什么的,通知reducer去更新数据
reducer:相当于vuex中的mutations,去更新仓库数据的一个函数或对象。
store
redux仓库数据存放的地方,一般只会存在一个store仓库
-
安装
@reduxjs/toolkit
npm i @reduxjs/toolkit # or yarn add @reduxjs/toolkit
-
新建store/index.js文件
创建仓库
import { configureStore } from '@reduxjs/toolkit'; let store = configureStore({ reducer() { } }); console.log('store', store);
-
在index.js文件中引入仓库使用
import './store'
action
作为动作描述期,是一个对象,必须传递一个type属性,代表这个动作是什么,其他参数可以任意传递,通常其他参数会放在payload
这个属性中,描述了这个动作是干什么的,通知reducer去更新数据
可以通过disaptch
派发action通知reducer更新数据
定义action
const action = {
type: 'add', // 动作类型
num: 10
}
派发action通知reducer更新数据
store.dispatch(action);
reducer中会根据type属性去匹配执行相关操作。
reducer
相当于vuex中的mutations,去更新仓库数据的一个函数或对象。初始化数据。
初始化数据
import { configureStore } from '@reduxjs/toolkit';
let store = configureStore({
reducer(state = 0, action) {
console.log('reducer触发了', action);
switch (action.type) {
case 'add':
state += action.num;
return state;
case 'reduce':
state -= action.num
return state;
default:
return state;
}
}
});
console.log('store', store);
console.log('getState', store.getState());
const action = {
type: 'add', // 动作类型
num: 10
}
store.dispatch(action);
console.log('add', store.getState());
const reduceAction = {
type: 'reduce',
num: 1
}
store.dispatch(reduceAction)
console.log('reduce', store.getState());
在reducer函数中第一个参数去定义数据,达到初始化数据的效果,然后返回这个state数据。
redux对象特性
在@reduxjs/toolkit
中,像原来一样去使用redux仓数据时,修改数据需要返回一个新的对象,目前不能直接修改state中的值,否则会有错误提示。
store/index.js
import { configureStore } from '@reduxjs/toolkit';
let store = configureStore({
reducer(state = {count: 0}, action) {
let count = state.count
switch (action.type) {
case 'add':
// state.count += action.num
count += action.num;
return {
count
};
case 'reduce':
// state -= action.num
count -= action.num
return {
count
};
default:
return state;
}
}
});
export default store;
坑一:返回state数据不完整导致数据丢失
当state对象中有多个数据时,修改其中某些数据后,需要将全部数据返回,否则会导致数据丢失
import { configureStore } from '@reduxjs/toolkit';
let store = configureStore({
reducer(state = {count: 0, name: '阿旺', age: 18}, action) {
let count = state.count
switch (action.type) {
case 'add':
count += action.num;
return {
count
};
case 'reduce':
count -= action.num
return {
count
};
default:
return state;
}
}
});
export default store;
此时,更新数据后,会导致name
和age
属性丢失,因为返回时只返回了count
值。
正确处理方式:
将整个state对象拷贝一份,用修改后的数据覆盖掉原来的数据,数据不会丢失
import { configureStore } from '@reduxjs/toolkit';
let store = configureStore({
reducer(state = {count: 0, name: '阿旺', age: 18}, action) {
let count = state.count
switch (action.type) {
case 'add':
count += action.num;
return {
...state,
count
};
case 'reduce':
count -= action.num
return {
...state,
count
};
default:
return state;
}
}
});
export default store;
坑二:如果state中嵌套对象,修改这个对象后,需要返回一个新的对象地址,否则不能引起组件渲染更新
错误案例:
import { configureStore } from '@reduxjs/toolkit';
let store = configureStore({
reducer(state = {count: 0, name: '阿旺', age: 18, user: {address: '上海'}}, action) {
let user = state.user;
switch (action.type) {
case 'changeAdress':
user.address = '静安区';
return {
...state,
user
}
default:
return state;
}
}
});
export default store;
此时修改user地址后,不会引起组件渲染,因为user对象是地址引用,不是一个新的对象。
正确做法:
将user对象深拷贝:
1)结构扩展(不是深拷, 对一层对象有效)
import { configureStore } from '@reduxjs/toolkit';
let store = configureStore({
reducer(state = {count: 0, name: '阿旺', age: 18, user: {address: '北京'}}, action) {
let user = state.user;
switch (action.type) {
case 'changeAdress':
user.address = '朝阳区';
return {
...state,
user: {
...user
}
}
default:
return state;
}
}
});
export default store;
2)JSON.parse
和JSON.stringify
深拷、或者递归实现
import { configureStore } from '@reduxjs/toolkit';
let store = configureStore({
reducer(state = {count: 0, name: '阿旺', age: 18, user: {address: '广州'}}, action) {
let user = state.user;
switch (action.type) {
case 'changeAdress':
user.address = '天河区';
return {
...state,
user: JSON.parse(JSON.stringify(user))
}
default:
return state;
}
}
});
export default store;
3)一开就深拷对象
import { configureStore } from '@reduxjs/toolkit';
let store = configureStore({
reducer(state = {count: 0, name: '阿旺', age: 18, user: {address: '深圳'}}, action) {
// 一开就深拷对象
let user = {...state.user};
switch (action.type) {
case 'changeAdress':
console.log('changeAdress触发了');
user.address = '南山';
return {
...state,
user
}
default:
return state;
}
}
});
export default store;
重点:state必须是一个完全的新的地址对象,才能引起组件的渲染更新。
createSlice模块化
createSlice是@reduxjs/toolkit
提供的一个函数,通过它可以创建模块,相当于vuex中的modules
子模块功能。
初始化
1)新建store仓库
store/index.js
import { configureStore } from '@reduxjs/toolkit';
let store = configureStore({
reducer() {
}
});
export default store;
2)通过createSlice
创建模块
新建counter.js
文件
store/counter.js
import { createSlice } from '@reduxjs/toolkit';
// 创建模块
let counterSlice = createSlice({
name: 'counter', // 相当于vue子模块的命名空间
initialState: { // 初始化数据
count: 0
},
reducers: { // 相当于vuex中的mutations对象,用于同步修改数据
}
});
export default counterSlice;
createSlice
:创建模块后,会生成一个模块,这个模块包含reducer
、action creator
创建器
name
:相当于vue子模块的命名空间,这个名字必须是唯一的
initialState
:初始化数据
reducers
:相当于vuex中的mutations对象,用于同步修改数据
3)绑定reducer
在store/index.js中绑定reducer初始化数据
import { configureStore } from '@reduxjs/toolkit';
import counterSlice from './counter';
let store = configureStore({
reducer: {
counterRd: counterSlice.reducer
}
});
export default store;
reducer
:是一个对象,对象中的键就是划分模块,在访问数据时,需要安装模块名去访问数据
4)通过Provider
绑定store仓库
react的组件是UI组件,如果UI组件要使用redux的数据,需要变成容器组件
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
// react的组件是UI组件,如果UI组件要使用redux的数据,需要变成容器组件
import { Provider } from 'react-redux'
import store from './store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
将store仓库数据传入到Provider
中,后续的组件就有资格获取到redux的仓库数据。
5)通过connect
高阶组件去获取redux中的数据
function mapStateToProps(state) {
console.log('mapStateToProps', state);
return {
count: state.counterRd.count
}
}
export default connect(mapStateToProps)(App);
在mapStateToProps
中访问数据时,需要注意通过模块划分时的key
去访问数据。
修改数据
1)编写reducer
import { createSlice } from '@reduxjs/toolkit';
// 创建模块
let counterSlice = createSlice({
name: 'counter', // 相当于vue子模块的命名空间
initialState: { // 初始化数据
count: 0
},
reducers: { // 相当于vuex中的mutations对象,用于同步修改数据
addCountRd(state, action) {
// 这里的state数据proxy代理了
console.log(state, action);
state.count += action.num;
}
}
});
export default counterSlice;
注意:reducer中的state陪proxy代理过,直接修改state中的数据即可引起页面的渲染更新。
2)派发action通知reducer更新数据
派发action有两种方式:
-
派发一个对象
this.props.dispatch({ type: 'counter/addCountRd', num: 10 });
type
:是一个字符串,有两部分组成,第一部分是createSlice
创建模块时的命名空间name
的值,第二部分是对应的reducer
函数的名称。 -
通过派发模块生成的
action creator
创建器函数去生成action
对象this.props.dispatch(counterSlice.actions.addCountRd(10));
counterSlice.actions.addCountRd(10)
:就是模块化后生成的action creator
创建器函数,运行后会返回一个action
对象,对象中默认使用payload
作为参数载体来传递参数。
createAsyncThunk异步处理
目前学习的仓库知识中,要处理异步数据,可能会考虑这样写:
componentDidMount() {
getUserList().then(res => {
this.props.dispatch({})
})
}
这样写可以实现功能,但是不具备封装性,@reduxjs/toolkit
提供了一种方式让我们去封装异步的代码。
createAsyncThunk异步处理
类似于vuex中的actions封装异步请求。
Thunk
: 是一个插件,做异步处理来获取数据,目前@reduxjs/toolkit
集成了这个异步处理插件,我们直接用就可以了。
1)安装axios
npm i axios
2)封装api接口
import axios from "axios";
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50IjoieGlhb2ZlaSIsInJvbGUiOnsibWVudXMiOlsiL2hvbWUiLCIvdXNlciIsIi9yb2xlIiwiL3Byb2R1Y3QiLCIvcHJvZHVjdC9saXN0IiwiL3Byb2R1Y3QvY2F0ZWdvcnkiLCIvY2hhcnQiLCIvY2hhcnQvYmFyIiwiL2NoYXJ0L2xpbmUiLCIvY2hhcnQvcGllIl0sIl9pZCI6IjVmYzc4NDJkMjY0MjAwMDBkYzAwNTAzYyIsImF1dGhUaW1lIjoiMjAyMS0wMy0xN1QxMzowMDozMC41MDJaIiwiYXV0aFVzZXIiOiJ4aWFvZmVpIiwiY3JlYXRlRGF0ZSI6bnVsbCwiY3JlYXRlVGltZSI6IjIwMjAtMTItMDIiLCJuYW1lIjoi6LaF57qn566h55CG5ZGYIiwic3RhdGUiOjEsInVwZGF0ZURhdGUiOiIyMDIxLTAzLTE3VDEzOjAwOjMwLjUwOVoifSwiX2lkIjoiNWZiNjY1ZjEyMjViMDAwMDM2MDA0ZGY1IiwiZXhwIjoxNjc3ODk4MTA1LjU0MywiaWF0IjoxNjc3ODExNzA1fQ.B3BqHeY3sfKLjywFN34PaIbLumnCCanfdhePXGiUzHQ'
export function getAccountList() {
return axios.post('http://localhost:8002/users/getAccountList', {}, {
headers: {
token
}
})
}
3)创建异步thunk
通过createAsyncThunk
去创建异步thunk,进行异步请求封装。会返回一个thunk创建器函数,包含三个状态:pending
、fulfiled
、rejected
,我们需要监听这个三个状态去对应执行reducer函数操作。
store/user.js
// 是一个函数,生成thunk进行异步派发,一个thunk生成器,用于dispatch派发
export let getUserListThunk = createAsyncThunk('user/getUserList', async () => {
// 这里可以去调用接口做异步处理
let res = await getAccountList();
return res.data.data;
});
createAsyncThunk:需要将数据返回,不需要在这里去做数据修改操作。
4)通过extraReducers
配置额外的reducer
在这里可以监听异步thunk创建器函数的状态,对应处理reducer函数
第一种配置方式:
通过builder构建起的addCase
函数去监听状态。
let userSlice = createSlice({
name: 'user',
initialState: {
userList: []
},
reducers: {},
extraReducers(builder) {
// 监听调用接口的状态,执行对应的reducer函数去修改数据
builder.addCase(getUserListThunk.fulfilled, (state, action) => {
console.log('成功');
console.log(state, action);
state.userList = action.payload;
}).addCase(getUserListThunk.pending, () => {
console.log('挂起');
}).addCase(getUserListThunk.rejected, () => {
console.log('失败');
})
}
});
第二种配置方式:
使用对象的方式配置:
let userSlice = createSlice({
name: 'user',
initialState: {
userList: []
},
reducers: {},
extraReducers: {
[getUserListThunk.fulfilled](state, action) {
console.log('成功');
state.userList = action.payload;
},
[getUserListThunk.pending]() {
console.log('挂起');
},
[getUserListThunk.rejected]() {
console.log('失败');
}
}
});
解释:相当于对象中的key是一个变量的写法:
var k = 'name';
var obj = {
[k]: 'xxx'
}
obj[k] = 'xxx'
obj.name = 'xxx'
5)派发异步thunk执行异步接口调用
App.js
import { getUserListThunk } from './store/user'
this.props.dispatch(getUserListThunk());
getUserListThunk
:是一个thunk创建器,执行后会得到一个thunk函数,派发后会执行异步接口调用,获取到数据。自动触发extraReducers
监听对应的状态执行reducer函数。