React 组件之间的通信
引入
本来我是没想过总结这些东西的,会感觉太入门了。但是之前同学去腾讯面试问到了这个问题(react或vue的组件通信),我帮他整理,顺便写demo的过程中,会有一些新的体会,多总结还是有利于进步的呀。另外本次的代码都放在 https://github.com/sunyongjian/rc-communication-demo , 可以 done 下来加深理解。
父子组件
父 → 子
parent 组件传给 child 组件,符合 react 的单向数据流理念,自上到下传递 props。
// 父组件 class Parent extends Component { constructor() { super(); this.state = { value: '', } } handleChange = e => { this.value = e.target.value; } handleClick = () => { this.setState({ value: this.value, }) } render() { return ( <div> 我是parent <input onChange={this.handleChange} /> <div className="button" onClick={this.handleClick}>通知</div> <div> <Child value={this.state.value} /> </div> </div> ); } }
// 子组件 class Child extends Component { render() { const { value } = this.props; return ( <div> 我是Child,得到传下来的值:{value} </div> ); } }
父组件做的就是定义好 state ,定义好事件函数,input onChange 的时候,去缓存 value 值,然后点击 button 的时候,改变 state , 子组件只负责展示 value 。
子 → 父
child 组件通知 parent 组件, 主要是依靠 parent 传下来的 callback 函数执行,改变 parent 组件的状态,或者把 child 自己的 state 通知 parent 。分两种情况:
state 定义在 parent 组件
// parent class Parent extends Component { constructor() { super(); this.state = { value: '', } } setValue = value => { this.setState({ value, }) } render() { return ( <div> <div>我是parent, Value是:{this.state.value}</div> <Child setValue={this.setValue} /> </div> ); } }
class Child extends Component { handleChange = e => { this.value = e.target.value; } handleClick = () => { const { setValue } = this.props; setValue(this.value); } render() { return ( <div> 我是Child <div className="card"> state 定义在 parent <input onChange={this.handleChange} /> <div className="button" onClick={this.handleClick}>通知</div> </div> </div> ); } }
parent 组件把改变 state 的 setValue 函数传给 child ,child 组件自己处理内部的状态(这里是表单的value值),当 child 组件分发消息的时候, 执行 parent 的 setValue 函数,从而改变了 parent 的 state,state发生变化, parent 组件执行 re-render 。
state 定义在 child 组件
// parent class Parent extends Component { onChange = value => { console.log(value, '来自 child 的 value 变化'); } render() { return ( <div> <div>我是parent <Child onChange={this.onChange} /> </div> ); } }
class Child extends Component { constructor() { super(); this.state = { childValue: '' } } childValChange = e => { this.childVal = e.target.value; } childValDispatch = () => { const { onChange } = this.props; this.setState({ childValue: this.childVal, }, () => { onChange(this.state.childValue) }) } render() { return ( <div> 我是Child <div className="card"> state 定义在 child <input onChange={this.childValChange} /> <div className="button" onClick={this.childValDispatch}>通知</div> </div> </div> ); } }
有时候 state 是需要定义在 child 组件的,比如弹窗, CheckBox 这种开关性质的,逻辑是重复的,state 定义在组件内部更好维护, 复用性更好。但是 child 的 state 是需要告知我的 parent 组件的, 同样还是执行 parent 传下来的 change 函数。
兄弟组件
有时候可能出现页面中的某两部分通信,比如省市的级联选择,点击 button 改变颜色等等,组件并不是父子级,没有嵌套关系的时候。这种时候通常是依赖共有的顶级 Container 处理或者第三方的状态管理器。其实原理都是相通的,兄弟 A 的 value 发生变化,分发的时候把 value 值告诉一个中间者 C ,C 会自动告知 B,实现 B 的自动render 。
利用共有的 Container
// container class Container extends Component { constructor() { super(); this.state = { value: '', } } setValue = value => { this.setState({ value, }) } render() { return ( <div> <A setValue={this.setValue}/> <B value={this.state.value} /> </div> ); } }
// 兄弟A class A extends Component { handleChange = (e) => { this.value = e.target.value; } handleClick = () => { const { setValue } = this.props; setValue(this.value); } render() { return ( <div className="card"> 我是Brother A, <input onChange={this.handleChange} /> <div className="button" onClick={this.handleClick}>通知</div> </div> ) } }
// 兄弟B const B = props => ( <div className="card"> 我是Brother B, value是: {props.value} </div> ); export default B;
组件 A 中的表单 value 值,告知了父级 Container 组件(通过 setValue 函数改变 state),组件 B 依赖于 Container 传下来的 state,会做出同步更新。这里的中间者是 Container。
利用 Context
上面的方式,如果嵌套少还可以,如果嵌套特别多,比如一级导航栏下的二级导航栏下的某个按钮,要改变页面中 content 区域的 table 里的某个列的值...他们同属于一个 page 。这样传递 props 就会很痛苦,每一层组件都要传递一次。
// 顶级公共组件 class Context extends Component { constructor() { super(); this.state = { value: '', }; } setValue = value => { this.setState({ value, }) } getChildContext() { // 必需 return { value: this.state.value, setValue: this.setValue, }; } render() { return ( <div> <AParent /> <BParent /> </div> ); } } // 必需 Context.childContextTypes = { value: PropTypes.string, setValue: PropTypes.func, };
// A 的 parent class AParent extends Component { render() { return ( <div className="card"> <A /> </div> ); } } // A class A extends Component { handleChange = (e) => { this.value = e.target.value; } handleClick = () => { const { setValue } = this.context; setValue(this.value); } render() { return ( <div> 我是parentA 下的 A, <input onChange={this.handleChange} /> <div className="button" onClick={this.handleClick}>通知</div> </div> ); } } // 必需 A.contextTypes = { setValue: PropTypes.func, };
// B 的 parent class BParent extends Component { render() { return ( <div className="card"> <B /> </div> ); } } // B class B extends Component { render() { return ( <div> 我是parentB 下的 B, value是: {this.context.value} </div> ); } } B.contextTypes = { value: PropTypes.string, };
组件 A 仍是 消息的发送者,组件 B 是接收者, 中间者是 Context 公有 Container 组件。context是官方文档的一个 API ,通过 getChildContext 函数定义 context 中的值,并且还要求 childContextTypes 是必需的。这样属于这个 Container 组件的子组件,通过 this.context
就可以取到定义的值,并且起到跟 state 同样的效果。中间者其实还是 Container,只不过利用了上下文这样的 API ,省去了 props 的传递。另外:这个功能是实验性的,未来可能会有所改动。
Context 也存在自己的问题。如果你用过 context,你可能会发现一个问题,当 context 发生改变的时候,比如数据流向是从 Container(context 定义) -> A -> B -> C(接收 context),组件 A, B 也会发生 render,这样 C 组件才能拿到更新后的 context。万一你在 A, B 使用 shouldComponentUpdate: false 拦截了,或者某个组件是 PureComponent,context 发生变化,C 没有重新渲染,故拿不到最新的 context。
针对这种情况,我们要做的不是想方设法让 A,B render,而是通过其他手段,来实现 C 的重新渲染。通常是使用 context 做依赖注入,即 context 只注入一次,后续不会发生变化,这样各种无视组件层级透传属性。context 里面的数据进行改造,添加 subscribe 这样的函数,然后当某个数据变化的时候做 patch。子组件可能会加这样的代码:
// 子组件 componentDidMount() { this.context.theme.subscribe(() => this.forceUpdate()) }
这种思想可以安全的使用 context,事实上 react-redux 也是这样做的。Provider 提供 context,connect 去做订阅。
关于此推荐一篇文章How to safely use React context,需翻墙
发布订阅
这种一个地方发送消息,另一个地方接收做出变化的需求,很容易想到的就是观察者模式了。具体的实现会有很多种,这里我们自己写了一个 EventEmitter 的类(其实就是仿照 node 中的 EventEmitter 类),如果不了解观察者,可以看我的另一篇文章观察者模式。
// 发布订阅类 class EventEmitter { _event = {} // on 函数用于绑定 on(eventName, handle) { let listeners = this._event[eventName]; if(!listeners || !listeners.length) { this._event[eventName] = [handle]; return; } listeners.push(handle); } // off 用于移除 off(eventName, handle) { let listeners = this._event[eventName]; this._event[eventName] = listeners.filter(l => l !== handle); } // emit 用于分发消息 emit(eventName, ...args) { const listeners = this._event[eventName]; if(listeners && listeners.length) { for(const l of listeners) { l(...args); } } } } const event = new EventEmitter; export { event };
// Container import A from './a'; import B from './b'; const Listener = () => { return ( <div> <A /> <B /> </div> ); }; export default Listener;
// 兄弟组件 A import { event } from './eventEmitter'; class A extends Component { handleChange = e => { this.value = e.target.value; } handleClick = () => { event.emit('dispatch', this.value); } render() { return ( <div className="card"> 我是Brother A, <input onChange={this.handleChange} /> <div className="button" onClick={this.handleClick}>通知</div> </div> ) } }
// 兄弟组件 B import { event } from './eventEmitter'; class B extends Component { state = { value: '' } componentDidMount() { event.on('dispatch', this.valueChange); } componentWillUnmount() { event.off('dispatch', this.valueChange); } valueChange = value => { this.setState({ value, }) } render() { return ( <div className="card"> 我是Brother B, value是: {this.state.value} </div> ); } }
仍然是组件 A 用于分发消息,组件 B 去接收消息。这里的中间者其实就是 event 对象。需要接收消息的 B 去订阅 dispatch 事件,并把回调函数 valueChange 传入,另外 B 定义了自己的 state,方便得到 value 值的时候自动渲染。组件 A 其实就是把内部的表单 value 在点击的时候分发,发布事件,从而 B 中的 valueChange 执行,改变 state。这种方式比较方便,也更直观,不需要借助 Container 组件去实现,省去了很多逻辑。
Redux || Mobx
Redux 或者 Mobx 是第三方的状态管理器,是这里我们通信的中间者。大型项目最直接的就是上库... 更方便,更不容易出错。 但其实小项目就没什么必要了。东西比较多,这里不再阐述它们的实现和做了什么。
总结
react 特殊的自上而下的单向数据流,和 state 的特性,造就以这样的思想实现组件通信。除去发布订阅和 Redux 等,其他的都是 props 自上而下传递的理念,子组件需要的总是通过父组件传递下来的,关于 state 的定义,还是看具体的应用场景了。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 引入 Mobx 使用和介绍
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
@sunyongjian 感谢回复
@neal1991 不是啊。你可以状态提升,或者使用 context,或者用 订阅,redux 等都可以。比如组件层级是
A1 包含 B1 和 B2,B 分别是 C1 和 C2。如果想要 C1 和 C2 通信,你可以把 state 定义到 A 中,这属于变量提升,风险就是如果再有和 A1 同层级的 A2 ,下面一个组件想和 C1 通信,state 可能就要重新改动,提升到 A1 和 A2 的 container。另外 Context 也是可以的啊,只不过层级太深会有一定的风险,我们通常用它作注入(provider)的一种方式 ,而不是 state 变化做通知的。所以还是要结合场景的啊,直接上状态管理库可能不用想太多,很少出现一些项目复杂后的问题,但是有时候是不必要的。
如果想和兄弟组件的子组件通信的话 是不是只能自己设置事件订阅 或者第三方状态管理?