组件是独立且封闭的单元,默认情况下,只能使用组件自己的私有数据。在页面组件化过程中,我们将一个完整的功能拆分成多个组件,在这个过程中,多个组件之间需要共享某些数据,或者与外界沟通,这个过程就是组件通信。
组件之间通信方式大致分为四类:
- 父组件向子组件通信
- 子组件向父组件通信
- 跨级组件通信
- 非嵌套组件之间通信
父组件向子组件通信
通过props : React中主要通过props来实现组件之间数据的传输。
- 可以给组件传递任意类型的数据。
- props 是只读的对象,只能读取属性的值,无法修改对象。
- 所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
export default function Father() {
// 传值
const numCount = 1;
return (
<div>
<h2>父组建------ </h2>
<Son num={numCount}></Son>
</div>
)
}
function Son(props) {
return (
<div>
<h2>子组建------ </h2>
<p>接收到的数据为:{props.num}</p>
</div>
)
}
通过Refs:父组件通过 ref 获取到子组件的实例或者元素,调用子组件的方法进行数据传递。
在某些情况下,需要在典型数据流之外强制修改子组件,被修改的组件可能是一个React组件的实例或是Dom元素。react提供这个ref属性,表示对组件实例真正的引用。
所以 需要注意!! :不能在函数组件上使用 ref 属性,因为他们没有实例。
// 父组件
export default class FatherToSonRefs extends Component {
constructor() {
super();
this.msg = '1'
// 1、 通过 createRef() 生成ref
this.childComp = createRef()
}
componentDidMount() {
console.log('componentDidMount', this.childComp.current);
}
clickHandle = () => {
// 2 、 调用子组件的方法,并传递数据
this.childComp.current.childClickHandleName("传值给子组建");
}
render() {
return (
<div>
<h2>父组建------ </h2>
<button onClick={this.clickHandle}>按钮</button>
{/* 给子组件设置ref属性 */}
<Child ref={this.childComp}></Child>
</div>
)
}
}
// 子组件
class Child extends Component {
state = {
name: "",
}
// 3、子组件声明该方法
childClickHandleName = (v) => {
this.setState({
name: v
})
}
render() {
return (
<div>
<h2>子组建------ </h2>
<div>name:{this.state.name}</div>
</div>
)
}
}
子组件向父组件通信
回调函数方式:
子组件通过回调函数向父组件传递数据,父组件将自己的某个方法传递给子组件,
子组件通过props接收到父组件的方法进行调用。
// 父组件
export default function Father() {
const [name, setName] = useState('');
const [value, setValue] = useState('');
const handleAlert = (v) => {
setName(v.name);
setValue(v.value);
}
return (
<div>
<h2>父组建------ </h2>
<Son handleAlert={handleAlert.bind(this)}></Son>
<div>子组建内容{name}-{value}</div>
</div>
)
}
// 子组件
function Son(props) {
const obj = {
name: 'name',
value :'111'
}
return (
<div>
<h2>子组建------ </h2>
<button onClick={() => { props.handleAlert(obj)} }>子组件调用父组件方法</button>
</div>
)
}
嵌套关系通信方式
Context.Provider
1、创建一个context对象,每个 Context 对象都会返回一个 Provider React 组件。
const MyContext = createContext('context');
2、Provider 接收一个 value 属性,传递给消费组件。
<MyContext.Provider value={/* 某个值 */}>
3、一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
4、当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件的传播不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。
function GrandPa() {
// 通过createContext 创建 React 的 上下文(context)
const ThemeContext = createContext('light');
return (
<div>
<h2>根组建------ </h2>
{/*通过 Context.Provider 最外层包装组件,并且需要显示的通过 <MyContext.Provider value={{xx:xx}}> 的方式传入 value,指定 context 要对外暴露的信息*/} <ThemeContext.Provider value='dark'>
<Father />
</ThemeContext.Provider>
</div>
);
};
const Father = () => {
return (
<div>
<h2>父组建------ </h2>
<Son />
</div>
)
};
// 函数组件
const Son = () => {
// 子组件useContext 解析上下文
const data = useContext(ThemeContext);
return (
<div>
<h2>子组建------ </h2>
<div>{data}</div>
</div>
)
};
// 类组件
class SonWithClass extends React.Component {
static contextType = ThemeContext;
render() {
return (
<div>
<h2>子组建 class------ </h2>
<div>{this.context}</div>
</div>
)
}
};
Context.consumer
在函数式组件中订阅context
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
这个方法需要以一个函数作为子元素,这个函数接收当前context值,并返回一个react节点。
const FatherContext = createContext({
id: 1,
name: 'father'
})
const SonContext = createContext({
id: 2,
name: 'son'
})
function Father() {
return (
<div>
<h2>根组建------ </h2>
{/*函数参数接收当前的 FatherContext 值,返回一个 React 节点。*/}
<FatherContext.Consumer>
{
(prop) => {
return (
<div>
<h2>子组件------ </h2>
{prop.name}- {prop.id}
{/*函数参数接收当前的SonContext的值,返回一个 React 节点。*/}
<SonContext.Consumer>
{(status) => {
return <div>
<div>子组件</div>
{status.name} - {status.id}
<GrandSon name={prop.name} id={prop.id} />
</div>
}}
</SonContext.Consumer>
</div>
)
}
}
</FatherContext.Consumer>
</div>
);
};
const GrandSon = (props) => {
return (
<div>
<h2>孙子组件------ </h2>
<div>接收到的数据为:{props.name} - {props.id}</div>
</div>
)
}
缺点:
Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。
请谨慎使用,因为这会使得组件的复用性变差。如果用组件组合可以解决的问题,就不要使用 Context 。
Context + useReducer
hooks成为广泛使用的api后,可以通过使用useReducer+useContext实现redux全局状态共享
再简单介绍一下useReducer:
useReducer 接收一个形如 (state,action)=> newState的reducer,并返回当前state以及其配套的dispatch方法。
const [state, dispatch] = useReducer(reducer, initialArg, init);
示例如下:
function ContextWithReducer() {
// 1、创建context
const Context = createContext();
const initialState = { count: 0 };
// 2、调用useReducer,并返回state和dispatch方法
const [state, dispatch] = useReducer(reducer, initialState);
// 2、创建全局的reducer
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 , ...action};
case 'decrement':
return { count: state.count - 1 , ...action};
default:
throw new Error();
}
}
return (
<div>
<h2>根组建------ </h2>
// 4、通过context.provider 把值state和dispatch传入
<Context.Provider value={{ state, dispatch }}>
<div>创建 reducer </div>
<Father />
<Aunt />
</Context.Provider>
</div>
);
};
const Father = () => {
return (
<div>
<h2>父组建------ 什么都不做,只是传值 </h2>
<Son />
</div>
)
};
const Son = () => {
// 子组件useContext 解析上下文
const sonContext = useContext(Context);
const [value, setValue] = useState('');
useEffect(() => {
setValue(sonContext.state)
},[sonContext.state])
return (
<div>
<h2>子组建------ 调用dispatch,接收value </h2>
<button onClick={() => {
sonContext.dispatch({
type: 'increment',
payload: 'payload increment info',
})
}}>type:'increment'</button>
<button onClick={() => {
sonContext.dispatch({
type: 'decrement',
payload: 'payload decrement info',
})
}}>type:'decrement'</button>
<div>{value.count}</div>
</div>
)
};
const Aunt = () => {
const auntContext = useContext(Context);
const [values, setValue] = useState('');
useEffect(() => {
setValue(auntContext.state)
console.log('auntContext', auntContext, auntContext.state)
// eslint-disable-next-line
},[auntContext.state])
return (
<div>
<h2>aunt组建------ 接收到子组建的内容 </h2>
<div>{values.payload}</div>
</div>
)
};
非嵌套关系 - 订阅模式 EventEmitter
Events 基本介绍
Node.js的events模块对外提供了一个EventEmitter对象,用于对Node.js中的事件进行统一创建。
核心是:
事件监听(订阅)addListener方法
事件触发(发布)emit方法
事件删除(取消订阅)removeListener方法
此外务必注意在组件销毁的时候卸载订阅的事件调用,否则会造成内存泄漏。
使用方式:
1、安装events包
npm install events --save
2、提供events事件对象
import { EventEmitter } from "events";export default new EventEmitter();
我们在ev.js文件里引入eventEmitter,并抛出一个实例
3、开始订阅、触发、删除
// 引入Ev.js文件
import { EventEmitter } from "events";
export default new EventEmitter();
function Event() {
return (
<div>
<Foo />
<Boo />
</div>
);
}
export default class Foo extends Component{
constructor(props) {
super(props);
this.state = {
msg: '开始没有内容',
};
}
componentDidMount() {
// 声明一个自定义事件
// 在组件装载完成以后
this.eventEmitter = emitter.addListener("callMe", (msg) => {
this.setState({
msg
})
}
);
}
// 组件销毁前移除事件监听
componentWillUnmount() {
emitter.removeAllListeners(['callMe']);
};
render(){
return(
<div>
<h2>同级 Foo ----- { this.state.msg }</h2>
</div>
);
}
}
export default class Boo extends Component{
render(){
const cb = (msg) => {
return () => {
// 触发自定义事件
emitter.emit("callMe",msg);
}
}
return (
<div>
<h2>同级 Boo ----- </h2>
<button onClick={cb("Boo传递的内容")}>点击我</button>
</div>
);
}
}