react 组件通信方式原理及应用

430 阅读5分钟

组件是独立且封闭的单元,默认情况下,只能使用组件自己的私有数据。在页面组件化过程中,我们将一个完整的功能拆分成多个组件,在这个过程中,多个组件之间需要共享某些数据,或者与外界沟通,这个过程就是组件通信。

组件之间通信方式大致分为四类:

  • 父组件向子组件通信
  • 子组件向父组件通信
  • 跨级组件通信
  • 非嵌套组件之间通信

父组件向子组件通信

通过props : React中主要通过props来实现组件之间数据的传输。

  • 可以给组件传递任意类型的数据。
  • props 是只读的对象,只能读取属性的值,无法修改对象。
  • 所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
export default function Father() {
    // 传值
    const numCount = 1;

    return (
        <div>
            <h2>父组建------ </h2>
            <Son num={numCount}></Son>
        </div>
    )
}

function Son(props) {
    return (
        <div>
            <h2>子组建------ </h2>
            <p>接收到的数据为:{props.num}</p>
        </div>
    )
}

通过Refs:父组件通过 ref 获取到子组件的实例或者元素,调用子组件的方法进行数据传递。

在某些情况下,需要在典型数据流之外强制修改子组件,被修改的组件可能是一个React组件的实例或是Dom元素。react提供这个ref属性,表示对组件实例真正的引用。

所以 需要注意!! :不能在函数组件上使用 ref 属性,因为他们没有实例。

// 父组件
export default class FatherToSonRefs extends Component {
    constructor() {
        super();
        this.msg = '1'
        // 1、 通过 createRef() 生成ref
        this.childComp = createRef()
    }

    componentDidMount() {
        console.log('componentDidMount', this.childComp.current);
    }

    clickHandle = () => {
        // 2 、 调用子组件的方法,并传递数据
        this.childComp.current.childClickHandleName("传值给子组建");
    }

    render() {
        return (
            <div>
                <h2>父组建------ </h2>
                <button onClick={this.clickHandle}>按钮</button>
                {/* 给子组件设置ref属性 */}
                <Child ref={this.childComp}></Child>
            </div>
        )
    }
}
// 子组件
class Child extends Component {
    state = {
        name: "",

    }

    // 3、子组件声明该方法
    childClickHandleName = (v) => {
        this.setState({
            name: v
        })
    }

    render() {
        return (
            <div>
                <h2>子组建------ </h2>
                <div>name:{this.state.name}</div>
            </div>
        )
    }
}

子组件向父组件通信

回调函数方式:

子组件通过回调函数向父组件传递数据,父组件将自己的某个方法传递给子组件,

子组件通过props接收到父组件的方法进行调用。

// 父组件
export default function Father() {
    const [name, setName] = useState('');
    const [value, setValue] = useState('');

    const handleAlert = (v) => {
        setName(v.name);
        setValue(v.value);
    }

    return (
        <div>
            <h2>父组建------ </h2>
            <Son handleAlert={handleAlert.bind(this)}></Son>
            <div>子组建内容{name}-{value}</div>
        </div>
    )
}
// 子组件
function Son(props) {

    const obj = {
        name: 'name',
        value :'111'
    }
   
    return (
        <div>
            <h2>子组建------ </h2>
            <button onClick={() => { props.handleAlert(obj)} }>子组件调用父组件方法</button>
        </div>
    )
}

嵌套关系通信方式

Context.Provider

1、创建一个context对象,每个 Context 对象都会返回一个 Provider React 组件。

const MyContext = createContext('context');


2、Provider 接收一个 value  属性,传递给消费组件。

<MyContext.Provider value={/* 某个值 */}>

3、一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

4、当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件的传播不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。

function GrandPa() {
  
  // 通过createContext 创建 React 的 上下文(context)
   const ThemeContext = createContext('light');

    return (
        <div>
            <h2>根组建------ </h2>
				{/*通过 Context.Provider 最外层包装组件,并且需要显示的通过 <MyContext.Provider value={{xx:xx}}> 的方式传入 value,指定 context 要对外暴露的信息*/}            <ThemeContext.Provider value='dark'>
                <Father />
            </ThemeContext.Provider>
        </div>
    );
};
const Father = () => {
    return (
        <div>
            <h2>父组建------ </h2>
            <Son />
        </div>
    )
};
// 函数组件
const Son = () => {
    // 子组件useContext 解析上下文
    const data = useContext(ThemeContext);

    return (
        <div>
        <h2>子组建------ </h2>
        <div>{data}</div>
        </div>
    )
};
// 类组件
class SonWithClass extends React.Component {
    static contextType = ThemeContext;

    render() {
        return (
            <div>
            <h2>子组建 class------ </h2>
                <div>{this.context}</div>
            </div>
        )
    }
};

Context.consumer

在函数式组件中订阅context

<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

这个方法需要以一个函数作为子元素,这个函数接收当前context值,并返回一个react节点。

const FatherContext = createContext({
    id: 1,
    name: 'father'
})

const SonContext = createContext({
    id: 2,
    name: 'son'
})

function Father() {
    return (
        <div>
            <h2>根组建------ </h2>
            {/*函数参数接收当前的 FatherContext 值,返回一个 React 节点。*/}
            <FatherContext.Consumer>
                {
                    (prop) => {
                        return (
                            <div>
                                <h2>子组件------ </h2>
                                {prop.name}- {prop.id}
                                  {/*函数参数接收当前的SonContext的值,返回一个 React 节点。*/}
                                <SonContext.Consumer>
                                    {(status) => {
                                        return <div>
                                            <div>子组件</div>
                                            {status.name} - {status.id}
                                            <GrandSon name={prop.name} id={prop.id} />
                                        </div>
                                    }}
                                </SonContext.Consumer>
                            </div>
                        )
                    }
                }
            </FatherContext.Consumer>
        </div>
    );
};
const GrandSon = (props) => {
    return (
        <div>
            <h2>孙子组件------ </h2>
            <div>接收到的数据为:{props.name} - {props.id}</div>
        </div>
    )
}

缺点:

Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。

请谨慎使用,因为这会使得组件的复用性变差。如果用组件组合可以解决的问题,就不要使用 Context 。

Context + useReducer

hooks成为广泛使用的api后,可以通过使用useReducer+useContext实现redux全局状态共享

再简单介绍一下useReducer:

useReducer 接收一个形如 (state,action)=> newState的reducer,并返回当前state以及其配套的dispatch方法。

const [state, dispatch] = useReducer(reducer, initialArg, init);

示例如下:

function ContextWithReducer() {
  	// 1、创建context
  	const Context = createContext();
    const initialState = { count: 0 };
  
  	// 2、调用useReducer,并返回state和dispatch方法
    const [state, dispatch] = useReducer(reducer, initialState);

    // 2、创建全局的reducer
    function reducer(state, action) {
        switch (action.type) {
            case 'increment':
                return { count: state.count + 1 , ...action};
            case 'decrement':
                return { count: state.count - 1 , ...action};
            default:
                throw new Error();
        }
    }

    return (
        <div>
            <h2>根组建------ </h2>
      
      		// 4、通过context.provider 把值state和dispatch传入
            <Context.Provider value={{ state, dispatch }}>
                <div>创建 reducer </div>
                <Father />
                <Aunt />
            </Context.Provider>
        </div>
    );
};

const Father = () => {
    return (
        <div>
            <h2>父组建------ 什么都不做,只是传值 </h2>
            <Son />
        </div>
    )
};
const Son = () => {
    // 子组件useContext 解析上下文
    const sonContext = useContext(Context);
    const [value, setValue] = useState('');
  
    useEffect(() => {
        setValue(sonContext.state)
     },[sonContext.state])

    return (
        <div>
            <h2>子组建------ 调用dispatch,接收value </h2>
            <button onClick={() => {
                sonContext.dispatch({
                    type: 'increment',
                    payload: 'payload  increment info',
                })
            }}>type:'increment'</button>
            <button onClick={() => {
                sonContext.dispatch({
                    type: 'decrement',
                    payload: 'payload decrement info',
                })
            }}>type:'decrement'</button>
            <div>{value.count}</div>
        </div>
    )
};
const Aunt = () => {
    const auntContext = useContext(Context);
    const [values, setValue] = useState('');

    useEffect(() => {
        setValue(auntContext.state)
        console.log('auntContext', auntContext, auntContext.state)
        // eslint-disable-next-line
     },[auntContext.state])

    return (
        <div>
             <h2>aunt组建------ 接收到子组建的内容 </h2>
             <div>{values.payload}</div>
        </div>
    )
};

非嵌套关系 - 订阅模式 EventEmitter

Events 基本介绍

Node.js的events模块对外提供了一个EventEmitter对象,用于对Node.js中的事件进行统一创建。

核心是:

事件监听(订阅)addListener方法

事件触发(发布)emit方法

事件删除(取消订阅)removeListener方法

此外务必注意在组件销毁的时候卸载订阅的事件调用,否则会造成内存泄漏。

使用方式:

1、安装events包

npm install events --save

2、提供events事件对象

import { EventEmitter } from "events";export default new EventEmitter();

我们在ev.js文件里引入eventEmitter,并抛出一个实例

3、开始订阅、触发、删除

// 引入Ev.js文件
import { EventEmitter } from "events";
export default new EventEmitter();
function Event() {
  return (
    <div>
          <Foo />
          <Boo />
    </div>
  );
}
export default class Foo extends Component{   
    constructor(props) {
        super(props);
        this.state = {
            msg: '开始没有内容',
        };
    }

    componentDidMount() {
        // 声明一个自定义事件
        // 在组件装载完成以后
        this.eventEmitter = emitter.addListener("callMe", (msg) => {
            this.setState({
                msg
            })
        }
        );
    }

        // 组件销毁前移除事件监听
        componentWillUnmount() {
            emitter.removeAllListeners(['callMe']);
        };

    render(){       
     return(          
              <div>
                  <h2>同级 Foo -----  { this.state.msg }</h2>
               </div>
        );
    }
}
export default class Boo extends Component{
    render(){      
      const cb = (msg) => {      
          return () => {
                // 触发自定义事件
                emitter.emit("callMe",msg);
            }
        }
        
        return (
                <div>
                <h2>同级 Boo -----  </h2>
                <button onClick={cb("Boo传递的内容")}>点击我</button>
            </div>
        );
    }
}