这两天被临时抽调到别的项目组去做一个小项目的迭代。这个项目前端是用React,只是个小型项目所以并没有使用Redux等状态管理的库。刚好遇到了一个小问题:两个不太相关的组件到底该怎么进行通信。我觉得这个问题还挺有趣的,所以把我的思考过程写下来,大家也可以一起讨论讨论。
虽然重点是要讲两个不相关的组件间的通信,但我还是从最常见的父子组件通信讲起,大家就当温故而知新了。先把完整的总结列出来,然后再详细展开。
组件间通信方式总结
父组件 => 子组件:
- Props
- Instance Methods
子组件 => 父组件:
- Callback Functions
- Event Bubbling
兄弟组件之间:
- Parent Component
不太相关的组件之间:
- Context
- Portals
- Global Variables
- Event Bus
- Redux/Flux
1. Props
这是最常见的react组件之间传递信息的方法了吧,父组件通过props把数据传给子组件,子组件通过this.props
去使用相应的数据
const Child = ({ name }) => {
{name}
}
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {
name: 'zach'
}
}
render() {
return (
)
}
}
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 (
{
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 I'm a modal
}
}
class Parent extends React.Component {
componentDidMount() {
if(// some condition) {
this.modal.show()
}
}
render() {
return (
{
this.modal = el
}}
/>
)
}
}
3. Callback Functions
讲完了父组件给子组件传递信息的两种方式,我们再来讲子组件给父组件传递信息的方法。回调函数这个方法也是react最常见的一种方式,子组件通过调用父组件传来的回调函数,从而将数据传给父组件。
const Child = ({ onClick }) => {
onClick('zach')}>Click Me
}
class Parent extends React.Component {
handleClick = (data) => {
console.log("Parent received value from child: " + data)
}
render() {
return (
)
}
}
4. Event Bubbling
这种方法其实跟react本身没有关系,我们利用的是原生dom元素的事件冒泡机制。
class Parent extends React.Component {
render() {
return (
);
}
handleClick = () => {
console.log('clicked')
}
}
function Child {
return (
);
}
巧妙的利用下事件冒泡机制,我们就可以很方便的在父组件的元素上接收到来自子组件元素的点击事件
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 (
);
}
}
6. Context
通常一个前端应用会有一些"全局"性质的数据,比如当前登陆的用户信息、ui主题、用户选择的语言等等。这些全局数据,很多组件可能都会用到,当组件层级很深时,用我们之前的方法,就得通过props一层一层传递下去,这显然太麻烦了,看下面的示例:
class App extends React.Component {
render() {
return ;
}
}
function Toolbar(props) {
return (
);
}
class ThemedButton extends React.Component {
render() {
return ;
}
}
上面的例子,为了让我们的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 (
);
}
}
function Toolbar() {
return (
);
}
class ThemedButton extends React.Component {
static contextType = ThemeContext;
render() {
return ;
}
}
简单的解析一下:
React.createContext
创建了一个Context对象,假如某个组件订阅了这个对象,当react去渲染这个组件时,会从离这个组件最近的一个Provider
组件中读取当前的context值Context.Provider
: 每一个Context对象都有一个Provider
属性,这个属性是一个react组件。在Provider组件以内的所有组件都可以通过它订阅context值的变动。具体来说,Provider组件有一个叫value
的prop传递给所有内部组件,每当value
的值发生变化时,Provider内部的组件都会根据新value值重新渲染那内部的组件该怎么使用这个context对象里的东西呢?
a. 假如内部组件是用class声明的有状态组件:我们可以把Context对象赋值给这个类的属性contextType
,如上面所示的ThemedButton组件class ThemedButton extends React.Component { static contextType = ThemeContext; render() { const value = this.context return ; } }
b. 假如内部组件是用function创建的无状态组件:我们可以使用
Context.Consumer
,这也是Context对象直接提供给我们的组件,这个组件接受一个函数作为自己的child,这个函数的入参就是context的value,并返回一个react组件。可以将上面的ThemedButton改写下:function ThemedButton { return (
{value => } ) }
最后提一句,context对于解决react组件层级很深的props传递很有效,但也不应该被滥用。只有像theme、language等这种全局属性(很多组件都有可能依赖它们)时,才考虑用context。如果只是单纯为了解决层级很深的props传递,可以直接用component composition
7. Portals
8. Global Variables
9. Event Bus
10. Redux/Flux
先发布下,明天继续更新