React组件通信方法

React组件间通信方式总结

  • 父组件 => 子组件:
    1、Props
    2、Instance Methods
  • 子组件 => 父组件:
    3、Callback Functions
    4、Event Bubbling
  • 兄弟组件:
    5、Parent Component
  • 不相关组件:
    6、Context
    7、Component Composition
    8、Portals
    9、Observer Pattern
    10、Redux等

对于React,我也是刚刚开始学习,所以总结中难免会出现错误,欢迎指出问题,大家共同交流讨论!


1、Props

父组件通过props将数据传给子组件:

const Child = ({ name }) => {
	<div>{ name }</div>
}

class Parent extends React.Component {
	constructor(props) {
	super(props);
	this.state = {
		name: 'zhangsan'
	}
}
render() {
	return (
		<Child name={this.state.name}/>
	)}
}


// 改为函数组件的写法
function Parent(props) {
	const [name, setName] = useState('zhangsan');
	return (
		<Child name={name}/>
	)
}

2、Instance Methods - 实例方法

原理是:父组件通过使用refs来直接调用子组件实例的方法
该方法最常见的一种使用场景:比如子组件是一个modal弹窗组件,子组件中有显示/隐藏这个modal弹窗的各种方法,我们可以通过使用这个方法,直接在父组件上调用子组件实例的这些方法来操控子组件的显示/隐藏。这种方法比传递一个控制modal显示/隐藏的props给子组件要美观多了。

class Modal extends React.Component {
	show = () => {//codes to show the modal};
	hide = () => {//codes to hide the modal};
	render() {
		return <div>I'm a modal.</div>
	}
}
class Parent extends React.Component {
	componentDidMount() {
		if(//some condition) {
			this.modal.show()
		}
	}
	render() {
		return(
			<Modal ref={el => {this.modal = el}}/>
		)
	}
}

3、Callback Functions

子组件通过调用父组件传来的回调函数,从而将数据传给父组件。

const Child = ({ onClick }) => {
	<div onClick={() => onClick('zhangsan')}>Click me</div>
}

class Parent extends React.Component {
	handleClick = (data) => {
		console.log("Parent received value from child:" + data)
	}
	render() {
		return (
			<Child onClick={this.handleClick}/>
		)
	}
}

4、Event Bubbling - 事件冒泡

利用事件冒泡机制,可以在父组件的元素上接收到子组件元素的点击事件。

function Parent() {
	const handleClick = useCallback(() => console.log('click'),[])
	return (
		<div onClick={handleClick}>
		<Child />
		</div>
	)
}
function Child() {
	return <button>Click</button>
}

5、Parent Component - 利用父组件通信

将父组件作为中间层来实现数据互通。

function Parent() {
	const [count, setCount] = useState(0);
	const handleClick = useCallback(() => setCount(count + 1) ,[count]);
	return (
		<div>
			<SiblingA count={count}/>
			<SiblingB onClick={handleClick}/>
		</div>
	)
}

6、Context

通常一个项目中,会存在一些全局性质的数据,很多组件都会用到,比如当前登陆的用户信息、ui主题、用户选择的语言等。当组件层级很深时,通过props传递就有些麻烦,此时可以使用context。
比如:下面的例子中,为了让Button元素拿到主题色,我们必须将theme作为props,从App传到Toolbar,再从Toolbar传到ThemeButton,最后Button从父组件ThemeButton的props里拿到theme,很麻烦。

function App() {
	return <Toolbar theme="dark"/>
}
function Toolbar(props) {
	return (
		<div>
			<ThemeButton theme={props.theme}/>
		</div>
	)
}
function ThemeButton(props) {
	return <Button theme={props.theme}/>
}

使用Context进行改写

const ThemeContext = React.createContext('light')
function App() {
	return (
		<ThemeContext.Provider value="dark">
			<Toolbar>
		</ThemeContext.Provider>
	)
}
function Toolbar() {
	return (
		<div>
			<ThemeButton/>
		</div>
	)
}
function ThemeButton() {
	return (
		<ThemeContext.Consumer>
			{value => <Button theme={value}}/>
		</ThemeContext.Consumer>
	)
}
  • 先用React.createContext创建一个Context对象,假如某个组件订阅了这个对象,当react渲染该组件时,会从离该组件最近的一个Provider组件中读取当前context值。
  • Context.Provider:每个Context对象都有一个Provider属性,该属性是一个React组件。在Provider组件内的所有组件都可以通过它订阅context值的变动。具体来说就是Provider组件有一个叫value的prop传递给所有内部组件,每当value值发生变化的时候,Provider内部的组件都会根据新value重新渲染。
  • 内部组件用Context.Consumer组件来使用context对象中的东西,该组件接受一个函数作为自己的child,函数的入参就是context的value,并返回一个React组件。
  • 如果内部组件是一个类组件,可以将Context对象赋值给这个类的属性contextType,如下:
class ThemeButton extends React.Component {
	const contextType = ThemeContext;
	render() {
		return (
			<Button theme={this.context}/>
		)
	}
}

总结:context对于解决react组件层级很深的props传递很有效,但也不应该被滥用。只有像theme、language等这种全局属性,才考虑context。因为这样会使得组件的复用性变差。
如果只是单纯为了解决层级很深的props传递,可以直接用component composition。

7、Component Composition - 组合组件

比如Page组件层层向下传递user和avatarSize属性,从而让深度嵌套的Link和Avatar组件读取到这些属性。

<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

如果最后只有Avatar组件真的需要user和avatarSize,那么层层传递所使用到的两个props就显得非常冗余。而且一旦Avatar组件需要更多来自顶层组件的props,还需要在中间层一个一个加上去,很麻烦。
一种无需使用context的解决方法是将Avatar组件自身传递下去,因为中间组件无需知道user或者avatarSize等props:

function Page(props) {
	const user = props.user;
	const userLink = (
		<Link href={user.permalink}>
		<Avatar user={user} size={props.avatarSize}>
		</Link>
	);
	return <PageLayout userLink={userLink}/>;
}

// 现在,我们有这样的组件:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout userLink={...} />
// ... 渲染出 ...
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}

这样,只有最顶层的Page组件需要知道Link和Avatar组件是如何使用user和avatarSize的

  • 这种对组件的控制反转减少了应用中需要传递的props的数量,很多场景下会使得代码更干净,对根组件有更多的把控。
  • 但是,这种做法将逻辑提升到组件树到更高层级来处理,会使得这些高层组件更加复杂,且会强行将底层组件适应这样的形式。

有时候可能需要传递多个子组件,甚至会为这些子组件children封装多个单独的接口slots:

function Page(props) {
	const user = props.user;
	const content = <Feed user={user}/>;
	const topBar = (
		<NavigationBar>
	      <Link href={user.permalink}>
	        <Avatar user={user} size={props.avatarSize} />
	      </Link>
	    </NavigationBar>
	);
	return (
		<PageLayout
			content={content}
			topBar={topBar}
		/>
	);
}

8、Portals

Portals是react提供的新特性,虽然不是用来解决组件通信的,但也涉及到了组件通信的问题。
其主要应用场景为:两个组件在react项目中是父子组件的关系,但在HTML DOM中我们不想让它们成为父子元素的关系
比如,有一个父组件Parent,里面包含一个子组件Tooltip,虽然在react层级上它们是父子关系,但是我们希望子组件Tooltip渲染的元素在DOM中直接挂载到body节点里,而不是挂载到父组件的元素里。这样可以避免父组件的一些样式(如overflow: hidden、z-index、position等)导致子组件无法渲染成我们想要的样式。
首先,修改html文件,给portals增加一个节点:

<html>
	<body>
		<div id="react-root">div>
		<div id="portal-root">div>
	body>
html>

然后创建一个可以复用的portal容器:

import { useEffect } from 'react';
import { createPortal } from 'react-dom';

const Portal = ({children}) => {
const mount = document.getElementById("portal-root");
const el = document.getElementById("div");

useEffect(() => {
mount.appendChild(el);
return () => mount.removeChild(el);
}, [el, mount]);

return createPortal(children, el)
};
export default Portal;

最后在父组件中使用portal容器组件,并将Tooltip作为children传给portal容器组件:

const Parent = () => {
	const [coords, setCoords] = useState({});
	return (
		<div style={{overflow: "hidden"}}>
			<Button>Hover me</Button>
			<Portal>
				<Tooltip coords={coords}>
					Awesome content that is never cut off by its parent container!
				</Tooltip>
			</Portal>
		</div>
	)
}

这样,虽然父组件是overflow: hidden,但Tooltip不会被截断,它渲染到body节点下的

中去了。
使用场景一般有:Tooltip、Modal、Popup、Dropdown等

9、Observer Pattern - 观察者模式

两个不相干组件需要通信时,可以使用观察者模式,其中一个组件负责订阅某个消息,而另一个组件负责发送消息。js提供了现成的API来发送自定义事件:CustomEvent,可以直接利用。
首先,在componentA中,接受这个自定义事件:

function ComponentA() {
	useEffect(() => {
		document.addEventListener('myEvent', handleEvent);
		return () => document.removeEventListener('myEvent', handleEvent);
	}, []);
	handleEvent = useCallback(e => console.log(e.detail.log), []);
}

然后,在componentB中,在合适的时候发送自定义事件:

function ComponentB() {
	sendEvent = useCallback(() => {
		document.dispatchEvent(new CustomEvent('myEvent', {
		detail: {
			log: "I'm zhangsan"
		}
	}))
}, []);
return <button onClick={sendEvent}>Send</button>
}

这样我们就用观察者模式实现了两个不相关组件之间的通信。当然现在的实现有个小问题,我们的事件都绑定在了document上,这样实现起来方便,但很容易导致一些冲突的出现,所以我们可以小小的改良下,独立一个小模块EventBus专门这件事:

class EventBus {
    constructor() {
        this.bus = document.createElement('fakeelement');
    }

    addEventListener(event, callback) {
        this.bus.addEventListener(event, callback);
    }

    removeEventListener(event, callback) {
        this.bus.removeEventListener(event, callback);
    }

    dispatchEvent(event, detail = {}){
        this.bus.dispatchEvent(new CustomEvent(event, { detail }));
    }
}

export default new EventBus

然后我们就可以愉快的使用它了,这样就避免了把所有事件都绑定在document上的问题:

import EventBus from './EventBus'
class ComponentA extends React.Component {
    componentDidMount() {
        EventBus.addEventListener('myEvent', this.handleEvent)
    }
    componentWillUnmount() {
        EventBus.removeEventListener('myEvent', this.handleEvent)
    }
    
    handleEvent = (e) => {
        console.log(e.detail.log)  //i'm zach
    }
}
class ComponentB extends React.Component {
    sendEvent = () => {
        EventBus.dispatchEvent('myEvent', {log: "i'm zach"}))
    }
    
    render() {
        return <button onClick={this.sendEvent}>Send</button>
    }
}

最后我们也可以不依赖浏览器提供的api,手动实现一个观察者模式,或者叫pub/sub,或者就叫EventBus。

function EventBus() {
  const subscriptions = {};
  this.subscribe = (eventType, callback) => {
    const id = Symbol('id');
    if (!subscriptions[eventType]) subscriptions[eventType] = {};
    subscriptions[eventType][id] = callback;
    return {
      unsubscribe: function unsubscribe() {
        delete subscriptions[eventType][id];
        if (Object.getOwnPropertySymbols(subscriptions[eventType]).length === 0) {
          delete subscriptions[eventType];
        }
      },
    };
  };

  this.publish = (eventType, arg) => {
    if (!subscriptions[eventType]) return;

    Object.getOwnPropertySymbols(subscriptions[eventType])
      .forEach(key => subscriptions[eventType][key](arg));
  };
}
export default EventBus;

10、Redux
一般是项目较大时,才考虑使用Redux这种状态管理库。


对于React,我也是刚刚开始学习,所以总结中难免会出现错误,欢迎指出问题,大家共同交流讨论!

参考原文档:https://segmentfault.com/a/1190000023585646

你可能感兴趣的:(前端,react.js,javascript,前端)