不管是在 vue 还是在 react 中,父组件是通过 props 向子组件传递数据,但 props 的特性是单向数据流,一般不能在子组件中直接修改 props 的值,那如果要修改呢?该如何处理呢?
vue 中组件的通信
在 vue 中,我们定义的父子组件如下
父组件
<template>
<div>
<div>父组件</div>
<div class="parent">
父组件中定义的信息: {{ msg }}
</div>
<Child :msg="msg" />
</div>
</template>
<script setup>
import {ref} from 'vue'
import Child from './Child.vue';
const msg = ref('msg at parent')
</script>
子组件
<template>
<div>
<div>子组件</div>
<div class="child">
<div>
来自父组件的信息: {{ msg }}
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
msg: String
})
</script>
子组件通过 :msg 属性接收父组件的数据
<Child :msg="msg" />
但是不能在子组件中直接修改接收到的值
props.msg = "new msg" ,无法通过这种方式修改,这种方式不会报错,但是修改之后不会生效,在控制台中有警告信息。那如果要修改接收到的值该如何处理?
定义一个本地变量接收者值
const msg1 = ref(props.msg)
可以修改 msg1 的值,但 props.msg 的值还是无法修改
通过 props 或 emit
在父组件中修改 msg 的值,对应的子组件的值也会跟着修改,那在子组件中如何修改父组件中的值呢?
1. 通过 `props` 传递一个 `Function` 方法\
在父组件中定义一个方法修改 `msg` 的值,子组件中用一个属性接收这个方法\
**父组件**
```vue
<Child :msg="msg" :chg-msg="chgMsg" />
const chgMsg = () => {
msg.value = "new msg"
}
```
在子组件中接收这个方法,并把这个方法绑定到一个 `button` 上\
**子组件**
```vue
const props = defineProps({
msg: String,
chgMsg: Function
})
<button @click="chgMsg">修改信息</button>
```
通过这种方式,相当于子组件调用父组件的方法修改 `msg` 的值,子组件的值也会跟着改变
2. 通过自定义事件
在子组件中定义一个事件\
**子组件**
```vue
const emit = defineEmits(['emitChg'])
const emitChg = () => {
emit('emitChg')
}
<button @click="emitChg">修改信息</button>
```
**父组件**
```vue
<Child :msg="msg" @emit-chg="chgMsg" />
```
**请注意这两种方式的区别**
```vue
<!--传递一个props-->
<Child :msg="msg" :chg-msg="chgMsg" />
<!--自定义一个事件-->
<Child :msg="msg" @emit-chg="chgMsg" />
```
react中组件的通信
在 react 中,只能通过在 props 传递方法的方式来处理,在react中不支持自定义事件的方式
父组件
const Parent = () => {
const [msg, setMsg] = useState('msg at parent')
const chgMsg = () => {
setMsg('new msg')
}
return (
<div>
<div>父组件</div>
<div>在父组件中定义的信息: {msg}</div>
<Child msg={msg} chgMsg={chgMsg} />
</div>
)
}
子组件
const Child = ({ msg, chgMsg }) => {
return (
<div>
<div>子组件</div>
<div>来自父组件的信息: {msg}</div>
<button onClick={chgMsg}>修改信息</button>
</div>
)
}
嵌套的组件之间也通过这种
props的方式进行通信,兄弟组件之间也可以通过这种方式,但需要通过他们的共同的父组件然后再通过props的方式进行通信
Prop 逐级透传问题
通常情况下,我们需要通过父组件将数据传递到子组件,如果嵌套很深,就需要把 props 像链条一样传递到最后的组件
vue中使用 provide 和 inject
provide 和 inject 可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
provide 和 inject 的基本的使用方式
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
<script setup>
import { inject } from 'vue'
const message = inject('message')
// message 的值是 hello
</script>
配合响应式数据,可以实现一个小型的响应式的数据仓库 store
- 定义
storestore.js 使用vue的响应式函数reactive
import { reactive, readonly } from "vue";
const initState = reactive({
count: 0,
})
export const store = {
state: readonly(initState),
increment: (v) => {
initState.count += v
},
decrement: (v) => {
initState.count -= v
}
}
- 提供依赖
providemain.js ,可以在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:
import {store} from './store'
const app = createApp(App)
app.provide('store', store)
app.mount('#app')
- 注入依赖
injectDeepChild.vue , 就可以在应用中的任何一个组件中使用
const {state, increment} = inject('store') // 可以使用 析构
// 或者
const store = inject('store')
// 在模板中使用
<div>{{ state.count }}</div>
<button @click="() => increment(2)">增加</button>
这种自定义 store 的形式,一般用在小型的项目中,如果是大型的项目请使用第三方库 pinia 或 vuex,以获取更好的性能
在 react 中也有同样的问题,在 react 中使用 Reducer 和 Context,来处理这种深度嵌套通信问题
react 使用 Reducer 和 Context
- 使用
useReducer提取数据操作逻辑
CounterReducer.js
export const initState = {
count: 0
}
export function counterReducer(state, action) {
switch(action.type) {
case 'increment':
return {
count: state.count + action.payload
}
case 'decrement':
return {
count: state.count - action.payload
}
}
}
父组件 Parent.jsx
import { initState, counterReducer } from './CounterReducer'
import { useReducer, useCallback } from 'react'
import Counter from './Counter'
<div>{state.count}</div>
<Counter
increment={() => dispatch({ type: 'increment', payload: 2 })}
decrement={() => dispatch({ type: 'decrement', payload: 1 })}
/>
子组件 Counter.jsx
const Counter = ({increment, decrement}) => {
return (
<div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
- 结合
Context,解决props透传问题
- 定义
ContextCounterContext.js
import { createContext } from "react";
export const CounterContext = createContext(null)
- 在
App.jsx中,提供数据
import { CounterContext } from './CouterContext'
import { initState, counterReducer } from './CounterReducer'
import { useCallback } from 'react'
import { useReducer } from 'react'
function App() {
const reducer = useCallback(counterReducer, [])
const [state, dispatch] = useReducer(reducer, initState)
return (
<CounterContext.Provider value={{state: state, dispatch: dispatch}}>
<Parent />
</CounterContext.Provider>
)
}
- 任意深度嵌套的组件中使用 DeepCounter.jsx
import React from 'react'
import { useContext } from 'react'
import {CounterContext} from './CouterContext'
const DeepCounter = () => {
const {state, dispatch} = useContext(CounterContext)
return (
<div>
<div>{state.count}</div>
<button onClick={() => dispatch({ type: 'increment', payload: 2})}>+</button>
</div>
)
}
使用组件实例访问数据
vue3 中使用 ref 和 defineExpose 访问组件暴露的属性和方法
Expose.vue,暴露组件的 increment 方法
<template>
<div>
expose: {{ count }}
</div>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0)
const increment = () => {
count.value += 1
}
defineExpose({
increment
})
</script>
在其他地方可以通过这个组件的实例调用这个方法
Parent.vue , 通过组件实例调用
<Expose ref="expose" />
const expose = ref(null)
const onIncrement = () => {
expose.value.increment() // 通过组件实例调用组件内部的方法
}
在 react 可以使用 ref 获取 DOM 节点的实例
ref配合useRef可以获取到DOM节点的实例,然后就可以调DOM节点的方法和属性
MyInput.jsx,
import { useRef } from 'react'
const MyInput = () => {
const inputRef = useRef(null)
const handleClick = () => {
inputRef.current.focus()
console.log(inputRef.current.value)
}
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={handleClick}>value</button>
</div>
)
}
- 配合
forwardRef可以获取其他组件的DOM节点的实例
MyInput.jsx 使用 forwardRef 封装一下, 然后在父组件中可以通过 useRef 获取到组件内部的 DOM 节点的实例,然后就获取 DOM 节点的方法和属性
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
在 父组件中 Parent.jsx
const inputRef = useRef(null)
const handleClick = () => {
inputRef.current.focus()
console.log(inputRef.current.value)
}
<MyInput ref={inputRef} />
<button onClick={handleClick}> focus</button>
其他的通信方式
这里只是列举了基本的通信方式,还其他的通信方式,如在 vue 中还可以通过 eventbus、 v-model slot的方式,还可以通过状态管理框架 pinia vuex,在 react 中也可以通过第三方的的状态管理框架 redux zustand等进行通信