上一篇文章 玩转 React(六)- 处理事件 介绍了在 React 中如何处理用户事件,以及 React 事件机制与原生 DOM 事件的差异和注意的问题,同时也介绍了事件处理函数中 this 的指向问题以及处理的几种方式及其优缺点。
大家在阅读的过程中有任何为题可以给我留言,同时欢迎大家加入玩转 React 微信群,我的微信号是 leobaba88
,先加我好友,验证信息:玩转 React,然后我会拉你进群。
今天这篇文章要讲的内容是关于多个组件之间如何共享数据,或者说是如何通信的。只有掌握了正确的组件之间通信的方式,才能在开发交互复杂的前端应用时做到游刃有余,所谓正确的方式也就是符合 React 设计理念的方式。使用一个框架时,一定要遵从框架的最佳实践,人家框架是这样设计的,你偏要那样来用,用得不爽还要喷其不好用,那就不应该了。
内容摘要
- React 中的数据是单向自顶向下传递的。
- 单向数据流与双向绑定的差异。
- 最符合 React 理念的组件之间共享数据的方式。
- 数据唯一来源原则。
- 一些不好的方式。
- 先跟 Redux 打个招呼。
- 其他一些关于组件间通信的内容(context、ref)。
组件之间通信的最佳方式
现在我们就来探讨下,什么样的方式才是 React 中组件之间通信的正确方式。
在前面的文章中,我们有说过,React 之所以能胜任大型复杂前端项目的开发,是因为其 单向数据流 这一重要特性,单向数据流能让视图更新逻辑变得简单,从原始的对 DOM 操作变为对数据操作,简单了就容易维护。
React 组件中数据的流动方向是自顶向下的,也就是说在组件树中,数据只能从父组件以属性的方式传递到子组件,父组件的数据可能是其接收到的属性,也可能是自身的内部状态。
有些同学这里可能会比较困惑,说子组件明明可以通过一个函数属性将数据传递给父组件呀。好多同学甚至因此搞不明白单向数据流和双向绑定的差异。其实换个角度考虑一下就清楚很多了,既然“数据传递”这个词区分度不够大,那就换个区分度比较大的说法。我们可以这样理解,函数属性是子组件用来通知父组件发生了什么,它更像是子组件触发的一个事件,父组件可以依据业务逻辑来选择如何处理这个事件,它可以更新数据后重新传递给子组件,也可以置之不理。
函数属性(或者说事件)在组件之间通信过程中是必不可少的,但是切莫让它影响了大家对单向数据流这一概念的理解。
数据双向绑定不一样,在双向绑定中父组件将数据传递给子组件,子组件修改数据后会将数据回传同步给父组件,父组件是无条件接受的。这里就不过多去说哪个好哪个差了,有兴趣的同学可以自己去体会,懒一点的就坚持学习 React 吧。
状态提升(Lifting State Up)
既然 React 中的数据是单向自顶向下传递的,那么符合 React 这一特性的组件通信方式就显而易见了。
状态提升的意思是,当组件 A 需要依赖另外一个组件 B 的内部状态,而他们又不是父子关系时,需要将组件 B 的内部状态提升到他们公共的祖先组件中管理。这样他们就都可以通过属性接收到这份数据了。
当组件 B 需要对数据进行变更时,可以通过函数属性来通知祖先组件对数据更新,然后重新传递给子组件。
唯一数据来源(Single source of truth)
有些同学可能又会迷惑,为什么多个组件之间必须要共用同一份数据,我可不可以引入一个事件库,一个组件分发事件,另一个组件注册相应的事件来接受数据自己维护。
类似的方案五花八门,会有很多,我认为这样做当然是不好的,会有如下问题:
- 破坏了组件的封装性,易于复用的组件都是相对独立的,它只需要定义自己需要的数据和行为(函数属性)即可,我不需要谁帮我分发事件。
- 数据传递是不连续的,这样做会增加项目的复杂性,当项目到一定阶段后,对这份数据的依赖就变得千丝万缕、难以维护了。
- 相同的数据会有多个副本,需要保证数据同步,在增加项目复杂性的同时也提高了出现BUG的几率。
这是我个人的看法,我也确实有遇到过这种用法,有不同意见大家可以进群交流。
数据唯一来源是官方推荐的数据共享的原则,也是最符合 React 设计理念,与单向数据流特性相辅相成的,希望大家务必遵守。
Redux
Redux 是一个状态管理库,它不是专属于 React 技术栈的,但是跟 React 配合起来相当不错。
当我们的前端应用规模较小的时候,我们可以不引入任何的状态管理工具,只需要依据上面说的状态提升的方式来管理应用状态即可。为了让应用的状态更直观,你可以将跟组件作为状态总线,来管理整个应用所有的状态。而且对于小规模的项目是推荐这样来做的,没有必要高射炮打蚊子,过渡设计。
但是当前端应用规模变得比较复杂时,我们就需要有类似 Redux 这样一个来专门进行状态管理的东西了。它的职责如下:
- 维护一个数据仓库(store)管理整个应用的状态(state),确保数据的唯一来源。
- 可以通过 dispatch 方法分发一个 action,来通知 Redux 需要对数据进行变更。
- Redux 接收到 action 后可以依据 action 的类型对 state 进行相应的修改。
- 数据跟新后 Redux 会触发注册的监听器(如:更新组件属性),完成视图更新。
Redux 跟 React 一起来用,更详细的介绍可以参考:官方文档,这里大家可以先简单了解下,在后面关于 React 实战的文章中也会详细介绍 Redux 的使用。
类似的状态管理工具还有:MobxJS,感兴趣的同学也可以了解下。
关于组件通信的其他内容
在 React 中还有一些其他的与组件间通信相关的知识,这里也顺便跟大家介绍下。
context
首先说一下,这是一个不推荐使用的特性,React 官方有明确说明,这是一个实验性的API,可能会在后面的版本中去掉这个东西。所以我是从来不用的,呵呵!
context 的作用是啥呢,当大家有过 React 实战经验时,很容易遇到这种场景,如果组件的层级组织得不合适,可能会嵌套的非常深,当底层的一个组件需要使用顶层一个组件的数据时,需要通过属性一层层传递下去,非常繁琐。
context 就是解决这个问题的,只需要在顶层组件中声明 context,那它的所有子组件可以通过 this.context 直接获取得到。如下实例所示:
import React from 'react'; import PropTypes from 'prop-types'; class Button extends React.Component { render() { return ( <button style={{background: this.context.color}}> {this.props.children} </button> ); } } Button.contextTypes = { color: PropTypes.string }; class Message extends React.Component { render() { return ( <div> {this.props.text} <Button>Delete</Button> </div> ); } } class MessageList extends React.Component { getChildContext() { return {color: "purple"}; } render() { const children = this.props.messages.map((message) => <Message text={message.text} /> ); return <div>{children}</div>; } } MessageList.childContextTypes = { color: PropTypes.string };
实例中,组件层级关系是:MessageList -> Message -> Button。
MessageList 组件中维护一个 color
值用于 Button 组件的背景色,一般情况下我们需要将 color 以属性的方式传给 Message 组件,再通过属性传给 Button 组件。然后在实例中,通过 React 的 context 功能,MessageList 可以将 color 的值越过 Message 直接传给 Button。
是不是很方便?确实很方便,但是这会导致数据传递不连续,过度使用会使得项目逻辑变得不直观,增加项目维护的复杂性。
ref
每一个 React 组件有一个特殊的属性 ref
,该属性的值可以是一个字符串,也可以是一个函数。由于字符串形式的 ref 在内部实现和实际使用中存在诸多问题,官方不推荐使用,而且可能在未来的版本中会移除,所以我们也没必要聊它了,只要大家在看到字符串形式的 ref 属性时知道也有这种用法就可以了。
当 ref
属性值是一个函数时,如果组件是一个 HTML 元素兼容的 React 内部组件时(如:div、img 等),函数接收其对应的原生 DOM 节点作为参数。如果组件是一个我们以类的方式定义的组件时,函数接收该组件类的实例作为参数。需要注意的是,如果组件是一个以函数的方式定义的组件,那么设置为 ref
值得函数永远都会接收到一个 null
。
那么 ref
与组件之间的通信有什么关系呢?请看上段文字加粗内容和下面这个实例:
class UserForm extends React.Component { constructor(props) { super(props) this.state = { name: null,age: null } } formData() { return this.state } handleFieldChange(e) { const { name,value } = e.target this.setState({ [name]: value }) } render() { return ( <div> <input type="text" name="name" placeholder="Name" onChange={e => this.handleFieldChange(e)} /> <input type="text" name="age" placeholder="Age" onChange={e => this.handleFieldChange(e)} /> </div> ) } } class App extends React.Component { handleSubmit() { const formData = this.form.formData() alert(`formData: ${JSON.stringify(formData)}`) } render() { return ( <div> <UserForm ref={form => {this.form = form}} /> <button onClick={() => this.handleSubmit()}>Submit</button> </div> ) } } ReactDOM.render(<App />,document.querySelector('#root'))
演示地址:https://codepen.io/Sarike/pen...
既然通过 ref
能够获取子组件的实例,那么我们自然可以调用其成员方法,从而获取数据。
当然,目前这确实能工作,但绝对不是一种好的方式。因为作为一个组件,是需要有一定的封装性的,它应该对外只会承诺我接受什么样的属性,而不会承诺有什么样的成员方法。换句话说,如果 JavaScript 的类支持私有成员方法,那么 React 组件类中的成员方法都应该定义成私有的。
这应该属于一种 Hack 的使用方式,而且这样做有悖单向数据流原则。
ref 有它自己的使用场景,这里只是说明这种方式不适用于组件之间通信。
总结
虽然啰嗦了这么多,实际上只希望大家知道一件事情,请使用状态提升的方式在多个组件之间共享数据,切记维持应用单向数据流和数据唯一来源原则。
文章中有些观点仁者见仁,有什么疑惑欢迎留言讨论。
好久没更新了,但是没有放弃,感谢大家支持。欢迎加我微信好友:leobaba88
,进群交流。验证信息:玩转 React
。