一、父子关系组件通信
1)props传递数据与props传递方法
a)父组件向子组件传递数据使用props传递属性
思路:父组件给子组件传递属性,在子组件中通过{this.props.属性名}来获得数据
//父组件
import React from 'react';
import Son1 from '../../components/Son1/Son1';
import Son2 from '../../components/Son2/Son2';
export default class Father extends React.Component {
constructor(props) {
super(props)
this.state = {
titile: "这是父级",
msg: "传给儿子1的数据",
sendson2: {
name: "哈哈哈",
age: 12,
sex: "男"
}
}
}
render() {
return (
<div>
<p>父级组件的标题:{this.state.titile}</p>
<hr />
<Son1 name={this.state.msg}></Son1>
<hr />
{/*传给子组件的属性是一个对象,使用...将对象展开*/}
<Son2 {...this.state.sendson2}></Son2>
</div>
)
}
}
//子组件1
//父组件传递的props是一个值
import React from 'react';
export default class Son1 extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div>
<p>父级传来的数据:{this.props.name}</p>
</div>
)
}
}
//子组件2
//父组件传递的props是一个对象
import React from 'react';
export default class Son2 extends React.Component {
constructor(props) {
super(props)
render() {
return (
<div>
<p>爸爸传来的数据:{this.props.name}</p>
<p>爸爸传来的数据:{this.props.age}</p>
<p>爸爸传来的数据:{this.props.sex}</p>
</div>
)
}
}
b)子组件向父组件传递数据使用props传递方法
子传父的本质还是父给子传属性,只是父传过去的属性值是一个函数,子可以调这个函数,通过调这个函数传参来传值。父传给儿子属性,属性是函数,儿子调这个函数实现传值,但是要注意,此时的this指向已经改变,在父级定义被调用的函数时就使用箭头函数可以解决this丢失的问题。
父组件传方法给子组件时,函数名自定义,且不可加()调用;子组件调用父组件的方法时,不用写父组件的函数名,通过属性名即可调用,但必须要加()来调用方法。
//父组件
import React from 'react';
import Son1 from '../../components/Son1/Son1';
import Son2 from '../../components/Son2/Son2';
export default class Father extends React.Component {
constructor(props) {
super(props)
this.state = {
titile: "这是父级",
msg: "传给儿子1的数据",
sendson2: {
name: "哈哈哈",
age: 12,
sex: "男"
},
fromson2: ""
}
}
// 子传父的本质还是父给子传属性,只是父传过去的属性值是一个函数,子可以调这个函数,通过调这个函数传参来传值
fn1(str) {
console.log(str);
}
// 父传给儿子属性,属性是函数,儿子调这个函数实现传值,但是要注意,此时的this指向已经改变,在父级定义函数时就使用箭头函数可以解决this丢失的问题
fn2 = (str) => {
console.log(str);
this.setState({
fromson2: str
})
}
render() {
return (
<div>
<p>父级组件的标题:{this.state.titile}</p>
<p>从Son2来的数据{this.state.fromson2}</p>
<hr />
<Son1 onmyclick={this.fn1}></Son1>
<hr />
<Son2 onmyclick={this.fn2}></Son2>
</div>
)
}
}
//子组件1
import React from 'react';
export default class Son1 extends React.Component {
constructor(props) {
super(props)
this.state = {
titile: "这是儿子1"
}
}
render() {
return (
<div>
<input value="传给老父亲" type="button" onClick={() => this.props.onmyclick("传给老父亲的值")} />
</div>
)
}
}
//子组件2
import React from 'react';
export default class Son2 extends React.Component {
constructor(props) {
super(props)
this.state = {
titile: "这是儿子2",
age: 1333
}
}
chuan(str) {
this.props.onmyclick(str)
}
render() {
return (
<div>
<input value="传给老父亲" type="button" onClick={() => this.chuan(this.state.age)} />
</div>
)
}
}
2)onRef标记
思路:当在子组件中调用onRef函数时,正在调用从父组件传递的函数。this.props.onRef(this)这里的参数指向子组件本身,父组件接收该引用作为第一个参数:onRef = {ref =>(this.child = ref)}然后它使用this.child保存引用。之后,可以在父组件内访问整个子组件实例,并且可以调用子组件函数。官方文档建议不要过度依赖onRef。
ref标记的实质依旧是父给子传属性,只是父传过去的属性值是一个函数(向子组件传函数时并不调用这个函数,即不加()),这个属性的属性名刻意规定为onRef;子可以调这个属性携带的函数,通过调这个函数并传参,来传值。那就意味着子组件可以向父组件回传this,就是把自己传给父组件,在父组件中定义一个属性来接住子组件的this,这样在父组件中就可以任意获取子组件中的数据,甚至是方法(那么既然可以调方法就可以传值)。注:调用方法时需要加(),来调用父组件的函数。
在父组件内读取子组件的数据,实质上就实现了子组件向父组件传递数据;在父组件内调用子组件的方法,那么通过调用方法时的传参,实质上就实现了父组件向子组件传递数据。
//父组件
import React from 'react';
import Son1 from '../../components/Son1/Son1';
import Son2 from '../../components/Son2/Son2';
export default class Father extends React.Component {
constructor(props) {
super(props)
this.state = {
titile: "这是父级",
}
}
fn1(child) {
this.Children1 = child
}
fn2() {
console.log(this.Children1.state.titile);
}
fn3(child) {
this.Children2 = child
}
fn4 = () => {
//父组件可以通过保存子组件实例的Children2,来调用子组件中的任何东西(数据和方法)
this.Children2.son2log("老父亲传来的")
}
render() {
return (
<div>
<input value="测试1" type="button" onClick={this.fn2.bind(this)} />
<input value="测试2" type="button" onClick={() => this.fn4()} />
<hr />
{/*onRef={(son) => this.fn1(son)}可以理解为一个事件处理函数(但这样说不对,onRef并不是一个事件);其中第一个son是子组件调用的真正的处理函数(这个无名箭头函数的目的就是为了解决类似事件处理函数不能传参,因为这个函数不能加圆括号被直接调用,否则这段代码在被挂载的时候就直接调用了)的参数,箭头后面的是真正做事情的函数(所以要加圆括号调用它);第一个son是子组件通过this.props.onRef(this)拿到父组件传过去的函数并执行之后的形参,第二个son是这个形参被传给父组件里函数的形参*/}
<Son1 onRef={(son) => this.fn1(son)}></Son1>
<hr />
<Son2 onRef={(son) => this.fn3(son)}></Son2>
</div>
)
}
}
//子组件1
import React from 'react';
export default class Son1 extends React.Component {
constructor(props) {
super(props)
this.state = {
titile: "这是儿子1"
}
}
//生命周期钩子函数渲染完成后,此时数据挂载完成、DOM渲染完成,接收父组件传来的方法,并调用它,然后传入this(就是自己)作为参数。
componentDidMount() {
this.props.onRef(this)
}
render() {
return (
<div>
</div>
)
}
}
//子组件2
import React from 'react';
export default class Son2 extends React.Component {
constructor(props) {
super(props)
this.state = {
titile: "这是儿子2",
age: 18
}
}
componentDidMount() {
this.props.onRef(this)
}
son2log(fromFather) {
console.log("son2log", fromFather);
}
render() {
return (
<div>
</div>
)
}
}
二、非父子组件通信方式
1)订阅发布(PubSub模块)
思路:通过PubSub模块提供的PubSub.subscribe方法,在订阅的组件中声明一个函数名及函数,然后再发布组件中通过PubSub模块提供的PubSub.publish方法去调用指定函数名的函数,调用的时候传的参就是数据。
在所有类型的组件关系中,都可以使用订阅发布(PubSub模块)的方式进行传值。
- 安装PubSub模块
yarn add PubSub --save
- 新建pubSub.js文件,引入安装的模块,并对外开放PubSub实例。(最好将pubSub.js文件放在scr文件夹下的utils文件夹中,便于管理)
import PubSub from "PubSub"; export default new PubSub();
- 在需要通信的组件中各自引入pubSub.js文件
import PubSub from '../../utils/PubSub';
- 添加发布:即在传出数据的一方添加。(示例Son1向Son2传数据)
//组件1;发布组件(传出数据的组件) import React from 'react'; import PubSub from '../../utils/PubSub.js'; export default class Son1 extends React.Component { constructor(props) { super(props) this.state = { titile: "这是儿子1" } } send() { // 绑定发布 PubSub.publish("fn", "Son1发布的消息") } render() { return ( <div> <p>{this.state.titile}</p> <input type="button" value="传给Son2" onClick={this.send} /> {/*此处写onClick={this.send}是因为send函数中并没有使用this;如果有this就需要写成onClick={()=>this.send()}或bind之类等等*/} </div> ) } }
- 添加订阅:即在接收数据的一方添加。(示例Son1向Son2传数据)
//组件2;订阅组件(接收数据的组件) import React from 'react'; import PubSub from '../../utils/PubSub.js'; export default class Son2 extends React.Component { constructor(props) { super(props) this.state = { titile: "这是儿子2", } } // 订阅(接收数据):此处可以理解为定义了一个函数,函数名叫fn,fn分别有两个参数(m,n),其中第一个参数m是接收到的数据,是核心需要的东西,第二个参数n是一个对象,记录着消息名,例如{name: "fn", token: 0}。订阅消息安排在componentDidMount钩子函数中,确保渲染完成后第一时间订阅数据。 componentDidMount() { PubSub.subscribe("fn", function (m, n) { console.log("Son2中接收到的消息(数据)", m); console.log("Son2中拿到的消息名(是个对象)", n); }) } render() { return ( <div> <p>{this.state.titile}</p> </div> ) } }
PubSub核心方法:
发布:pubsub.publish('消息名',数据)
订阅:token=pubsub.subscribe('消息名',回调函数(数据,'消息名'))
清除指定订阅:pubsub.unsubscribe(token | '消息名');
清除所有:pubsub.unsubscribeAll()
2)状态提升
本来是多个无关组件,但需要共用一部分数据时,将这部分数据放在一个离他们最近的父组件中进行管理。即把原本在子组件中state放在父组件中进行管理。
思路:父组件先给子组件1传一个属性,属性值是一个方法,子组件1通过调用这个方法时传参先把数据传给父组件,父组件的方法被调用后先把数据保存,再给子组件2传一个属性,属性值是刚才保存的数据,在子组件2中在通过获取父组件传来的属性最终获取子组件1传来的数据。
注意:因为涉及到React框架的数据挂载、模板渲染、数据更新等环节,实际上React框架并不是按这个顺序进行处理的,这里只是借此梳理思路。
//父组件
//这次演示Son1组件向Son2组件传值
import React from 'react';
import Son1 from '../../components/Son1/Son1';
import Son2 from '../../components/Son2/Son2';
export default class Father extends React.Component {
constructor(props) {
super(props)
this.state = {
titile: "这是父级",
// 〇:设置好这个value为空,用来中转保存子组件中需要的数据
value: null
}
}
fn(val) {
// 5).Son1调用父组件中的方法,父组件的方法把从Son1中获得的数据保存在this.state中,并通过this.setState刷新数据。
// 建议fn的形参和键名写成一样,这样就可以简写为this.setState({val});此处是为了区分理解。
this.setState({
value: val
})
}
render() {
return (
<div>
{/* 1).父组件先通过属性,给Son1传一个方法 */}
<Son1 onMyClick={(val) => this.fn(val)}></Son1>
{/* 4).此处的val接到的就是Son1调用父组件onMyClick属性时传来的数据 */}
<hr />
{/* 6).父组件把接收到的值传给Son2 */}
<Son2 val={this.state.value}></Son2>
{/* 此处val为传给Son2组件的自定义属性名(只要Son2中获取时一致即可),this.state.value为父组件中接收到Son1组件的值;实际使用时写成一样省事,这里是为了理解区分 */}
</div>
)
}
}
//子组件1;就是它要传出数据
import React from 'react';
export default class Son1 extends React.Component {
constructor(props) {
super(props)
this.state = {
titile: "这是儿子1"
}
}
// 3).调用父组件的方法,调用时就可以传值,数据不就可以来自任何地方。
send() {
this.props.onMyClick("son1中的数据")
}
render() {
return (
<div>
<p>{this.state.titile}</p>
{/* 2).子组件点击按钮时通过调用事件处理函数来调用父组件传来的方法 */}
<input type="button" value="传" onClick={() => this.send()} />
</div>
)
}
}
//子组件2;就是它要接收数据
import React from 'react';
export default class Son2 extends React.Component {
constructor(props) {
super(props)
this.state = {
titile: "这是儿子2",
}
}
render() {
return (
<div>
<p>{this.state.titile}</p>
{/* 7).Son2组件中获取父组件传来的值,结束。 对了,接来的数据可以在render中食用,也可以在方法中食用。*/}
<p>{this.props.val}</p>
</div>
)
}
}
3)context 状态树传参
示例中的组件关系:App==>Father==>Son1
a)状态树由外向内的传参
Provider==>Consumer
- 新建myContext.js文件,从react中解构createContext,之后调用createContext函数,并对外开放Provider, Consumer。(最好将文件放在scr文件夹下的utils文件夹中,便于管理)
import { createContext } from "react"; export const { Provider, Consumer } = createContext({ msg: "myContext中默认的数据" }); // 此处的msg是默认值;这个默认值的意义是,在使用时并未使用Provider时,Consumer组件获得的值,而不是使用Provider没有value时自动获取的值。
在myContext.js中也可以发请求,因为模块中的代码也是会被执行的,模块代码的执行时机,是首次引入该模块的地方,而且多次引入,只执行一次。如果发请求的话,那么export const { Provider, Consumer }......这些代码就要写在请求的回调函数之中。
- 在App组件中引入myContext.js文件,并解构出Provider;再用Provider包裹住需要使用数据的位置。
//App组件(爷爷组件)App组件是函数式组件 import Father from "../src/views/Father/Father" import { Provider } from '../src/utils/myContext'; function App() { return ( <div className="App"> <p>这是App.js</p> <hr /> <Provider value={{ msg: "App.js中的数据" }}> <Father></Father> </Provider> </div> ); } export default App;
Provider是一个组件,接收一个将要被往下层层传递的props,该值需在组件树最顶层设置。一个Provider可以关联到多个Consumers。这是一个顶层用于提供context的组件,包含一个value的props,value是实际的context数据。
在顶层组件也可以不写Provider,在Consumer中也可以获取到Context中的值,这个值就是myContext.js中的默认值。但在顶层组件写了Provider却没有写value就会报错(这种情况也不符合逻辑,既然不写value那么反正都要用默认值,那何苦呢,不如连Provider也别写了)
- 在Son1组件中引入myContext.js文件,并解构出Consumer;再用Consumer包裹住使用数据的位置。此时并不需要经过父级组件Father。
//Son1组件(孙级组件) import React from 'react'; import { Consumer } from '../../utils/myContext'; export default class Son1 extends React.Component { constructor(props) { super(props) this.state = { titile: "这是儿子1" } } render() { return ( <div> <p>{this.state.titile}</p> <Consumer> { (val) => { return <p>{val.msg}</p> } } </Consumer> </div> ) } }
Consumer是一个组件,接收一个函数作为子节点,函数接收当前 context 的值。这是一个底层用于获取context的组件,需要一个函数作为其子元素,该函数包含一个val的参数,这个参数就是上层所传递数据。
b)状态树由内向外的传参
Consumer==>Provider
使用步骤同由外向内传参一样,首先新建myContext.js文件,各个组件引入myContext.js文件,并解构处对应的Provider和Consumer,并包裹在使用数据的地方。
思路:在App组件的this.state中不仅保存数据,再保存一个App组件的方法,通过Provider的value属性,把this.state这个对象整体传给Consumer,Consumer在接到这个对象之后通过调用App组件中this.state里保存的App组件的方法时传参,来传递数据。
//App组件
import React from 'react';
import Father from "../src/views/Father/Father"
import { Provider } from '../src/utils/myContext';
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
msg: "App.js中this.state的数据",
// 此处的this就是App对象
// 1).定义一个fn变量名,它的值是App组件中的一个方法,写在this.state,日后可以传给Consumer组件
fn: this.fn
}
}
// 如果此处不使用箭头函数,那么内部的this指向的是this.state这个对象,this就不是我们想要的App对象,就无法调用this.setState方法刷新数据。
// 5).将Son1组件中调用fn函数时传入的数据接住,通过this.setState刷新数据,之后被修改的数据又会被响应给Son1
fn = (str) => {
this.setState({
msg: str
})
}
render() {
return (
<div className="App">
<p>这是App.js</p>
<p>{this.state.msg}</p>
<hr />
{/* 2).传给Consumer的value属性的值是一个对象,这个对象里不仅有数据,还有App对象中的方法 */}
<Provider value={this.state}>
<Father></Father>
</Provider>
</div>
)
}
}
export default App;
//Son1组件
import React from 'react';
import { Consumer } from '../../utils/myContext';
export default class Son1 extends React.Component {
constructor(props) {
super(props)
this.state = {
titile: "这是儿子1"
}
}
render() {
return (
<div>
<p>{this.state.titile}</p>
<Consumer>
{
// 3).在Consumer中接到的obj是一个对象,就是App组件中的state
(obj) => {
return (
// React脚手架中如果根标签没有意义,允许使用空标签当作唯一的根标签包裹
<>
<p>{obj.msg}</p>
<input type="button" value="修改"
onClick={() => obj.fn("Son1里来的数据")}
/>
{/* 4).通过点击事件,来调用App组件传来的this.state中的fn方法,既然可以调用App的方法,那就传个参,带点货回去。 */}
</>
)
}
}
</Consumer>
</div>
)
}
}
都这里了...就说明你看完了...
如果你看完的话我们就相互鼓励一下吧,我纯手写的不容易,你看的肯定更不容易...
如果有错误或理解不对的地方欢迎指出,感谢!