组件间通信方式总结
- 父组件 => 子组件:
- Props
- Instance Methods
- 子组件 => 父组件:
- Callback Functions
- Event Bubbling
- 兄弟组件之间:
- Parent Component
- 不太相关的组件之间:
- Context
- Portals
- Global Variables
- Observer Pattern
- Redux等
1、Props
这是最常见的react组件之间传递信息的方法了吧,父组件通过props把数据传给子组件,子组件通过this.props去使用相应的数据。
const Child = ({ name }) => {
<div>{name}</div>
}
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {
name: 'zach'
}
}
render() {
return (
<Child name={this.state.name} />
)
}
}
2、Instance Methods
第二种父组件向子组件传递信息的方式有些同学可能会比较陌生,但这种方式非常有用,请务必掌握。原理就是:父组件可以通过使用refs来直接调用子组件实例的方法,看下面的例子:
class Child extends React.Component {
myFunc() {
return "hello"
}
}
class Parent extends React.Component {
componentDidMount() {
var x = this.foo.myFunc() // x is now 'hello'
}
render() {
return (
<Child
ref={foo => {
this.foo = foo
}}
/>
)
}
}
大致的过程:
- 首先子组件有一个方法myFunc。
- 父组件给子组件传递一个ref属性,并且采用callback-refs的形式。这个callback函数接收react组件实例/原生dom元素作为它的参数。当父组件挂载时,react会去执行这个ref回调函数,并将子组件实例作为参数传给回调函数,然后我们把子组件实例赋值给this.foo。
- 最后我们在父组件当中就可以使用this.foo来调用子组件的方法咯.
了解了这个方法的原理后,我们要考虑的问题就是为啥我们要用这种方法,它的使用场景是什么?最常见的一种使用场景:比如子组件是一个modal弹窗组件,子组件里有显示/隐藏这个modal弹窗的各种方法,我们就可以通过使用这个方法,直接在父组件上调用子组件实例的这些方法来操控子组件的显示/隐藏。
这种方法比起你传递一个控制modal显示/隐藏的props给子组件要美观多了。
class Modal extends React.Component {
show = () => {// do something to show the modal}
hide = () => {// do something 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
讲完了父组件给子组件传递信息的两种方式,我们再来讲子组件给父组件传递信息的方法。
回调函数这个方法也是react最常见的一种方式,子组件通过调用父组件传来的回调函数,从而将数据传给父组件。
const Child = ({ onClick }) => {
<div onClick={() => onClick('zach')}>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
这种方法其实跟react本身没有关系,我们利用的是原生dom元素的事件冒泡机制。
class Parent extends React.Component {
render() {
return (
<div onClick={this.handleClick}>
<Child />
</div>
);
}
handleClick = () => {
console.log('clicked')
}
}
function Child {
return (
<button>Click</button>
);
}
巧妙的利用下事件冒泡机制,我们就可以很方便的在父组件的元素上接收到来自子组件元素的点击事件.
5、Parent Component
讲完了父子组件间的通信,再来看非父子组件之间的通信方法。一般来说,两个非父子组件想要通信,首先我们可以看看它们是否是兄弟组件,即它们是否在同一个父组件下。
如果不是的话,考虑下用一个组件把它们包裹起来从而变成兄弟组件是否合适。这样一来,它们就可以通过父组件作为中间层来实现数据互通了。
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {count: 0}
}
setCount = () => {
this.setState({count: this.state.count + 1})
}
render() {
return (
<div>
<SiblingA
count={this.state.count}
/>
<SiblingB
onClick={this.setCount}
/>
</div>
);
}
}
6、Context
通常一个前端应用会有一些"全局"性质的数据,比如当前登陆的用户信息、ui主题、用户选择的语言等等。
这些全局数据,很多组件可能都会用到,当组件层级很深时,用我们之前的方法,就得通过props一层一层传递下去,这显然太麻烦了,看下面的示例:
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}
上面的例子,为了让我们的Button
元素拿到主题色,我们必须把theme
作为props
,从App
传到Toolbar
,再从Toolbar传到ThemedButton
,最后Button
从父组件ThemedButton
的props
里终于拿到了主题theme
。
假如我们不同组件里都有用到Button
,就得把theme
向这个例子一样到处层层传递,麻烦至极。
因此React
为我们提供了一个新api:Context
,我们用Context
改写下上例:
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
简单的解析一下:
1、React.createContext
创建了一个Context
对象,假如某个组件订阅了这个对象,当React
去渲染这个组件时,会从离这个组件最近的一个Provider
组件中读取当前的context
值
2、Context.Provider
: 每一个Context
对象都有一个Provider
属性,这个属性是一个react组件。
3、在Provider
组件以内的所有组件都可以通过它订阅context
值的变动。具体来说,Provider
组件有一个叫value
的props
传递给所有内部组件,每当value
的值发生变化时,Provider
内部的组件都会根据新value
值重新渲染。
最后提一句,context
对于解决react
组件层级很深的props
传.递很有效,但也不应该被滥用。只有像theme、language
等这种全局属性(很多组件都有可能依赖它们)时,才考虑用context
。如果只是单纯为了解决层级很深的props
传递,可以直接用component composition
。
7、Portals
Portals
也是react提供的新特性,虽然它并不是用来解决组件通信问题的,但因为它也涉及到了组件通信的问题,所以我也把它列在我们的十种方法里面。
Portals
的主要应用场景是:当两个组件在react项目中是父子组件的关系,但在HTML DOM里并不想是父子元素的关系。
举个例子,有一个父组件Parent
,它里面包含了一个子组件Tooltip
,虽然在react层级上它们是父子关系,但我们希望子组件Tooltip
渲染的元素在DOM中直接挂载在body节点里,而不是挂载在父组件的元素里。这样就可以避免父组件的一些样式(如overflow:hidden
、z-index
、position
等)导致子组件无法渲染成我们想要的样式。
如下图所示,父组件是这个红色框的范围,并且设置了overflow:hidden
,这时候我们的Tooltip
元素超出了红色框的范围就被截断了。
怎么用portals解决呢? 首先,修改html文件,给portals增加一个节点。
<html>
<body>
<div id="react-root"></div>
<div id="portal-root"></div>
</body>
</html>
然后我们创建一个可复用的portal
容器,这里使用了react hooks
的语法,看不懂的先过去看下我另外一篇讲解react hooks
的文章:30分钟精通React今年最劲爆的新特性——React Hooks
import { useEffect } from "react";
import { createPortal } from "react-dom";
const Portal = ({children}) => {
const mount = document.getElementById("portal-root");
const el = document.createElement("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>
}
这样就ok啦,虽然父组件仍然是overflow: hidden,但我们的Tooltip再也不会被截断了,因为它直接超脱了,它渲染到body节点下的
里去了。总结下适用的场景: Tooltip、Modal、Popup、Dropdown等等.
8、Global Variables
哈哈,这也不失为一个可行的办法啊。当然你最好别用这种方法。
class ComponentA extends React.Component {
handleClick = () => window.a = 'test'
...
}
class ComponentB extends React.Component {
render() {
return <div>{window.a}</div>
}
}
9、Observer Pattern
观察者模式是软件设计模式里很常见的一种,它提供了一个订阅模型,假如一个对象订阅了某个事件,当那个事件发生的时候,这个对象将收到通知。
这种模式对于我们前端开发者来说是最不陌生的了,因为我们经常会给某些元素添加绑定事件,会写很多的event handlers,比如给某个元素添加一个点击的响应事件elm.addEventListener('click', handleClickEvent),每当elm元素被点击时,这个点击事件会通知elm元素,然后我们的回调函数handleClickEvent会被执行。
这个过程其实就是一个观察者模式的实现过程。
那这种模式跟我们讨论的react组件通信有什么关系呢?当我们有两个完全不相关的组件想要通信时,就可以利用这种模式,其中一个组件负责订阅某个消息,而另一个元素则负责发送这个消息。
javascript提供了现成的api来发送自定义事件: CustomEvent,我们可以直接利用起来。
首先,在ComponentA中,我们负责接受这个自定义事件:
class ComponentA extends React.Component {
componentDidMount() {
document.addEventListener('myEvent', this.handleEvent)
}
componentWillUnmount() {
document.removeEventListener('myEvent', this.handleEvent)
}
handleEvent = (e) => {
console.log(e.detail.log) //i'm zach
}
}
然后,ComponentB中,负责在合适的时候发送该自定义事件:
class ComponentB extends React.Component {
sendEvent = () => {
document.dispatchEvent(new CustomEvent('myEvent', {
detail: {
log: "i'm zach"
}
}))
}
render() {
return <button onClick={this.sendEvent}>Send</button>
}
}
10、Redux等
最后终于来到了大家喜闻乐见的Redux
等状态管理库,当大家的项目比较大,前面讲的9种方法已经不能很好满足项目需求时,才考虑下使用redux
这种状态管理库。这里就先不展开讲解redux
了...否则我花这么大力气讲解前面9种方法的意义是什么???
总结
十种方法,每种方法都有对应的适合它的场景,大家在设计自己的组件前,一定要好好考虑清楚采用哪种方式来解决通信问题。
文初提到的那个小问题,最后我采用方案9,因为既然是小迭代项目,又是改别人的代码,当然最好避免对别人的代码进行太大幅度的改造。
而pub/sub这种方式就挺小巧精致的,既不需要对别人的代码结构进行大改动,又可以满足产品需求。