组件通信

Misaka10032组件通信十种方法大约 4 分钟

提前总结

父组件 => 子组件

  1. Props
  2. Instance Methods

子组件 => 父组件

  1. Callback Functions
  2. Event Bubbling

兄弟组件之间

  1. Parent Component

不太相关的组件之间

  1. Context
  2. Portals
  3. Global Variables
  4. Observer Pattern
  5. Redux 等

1.Props

最常见的组件间传递信息方法,父组件 props 传数据给子组件,子组件接收 props 数据

const Child = ({ name }) => {
  <div>{name}</div>;
};

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "misaka",
    };
  }
  render() {
    return <Child name={this.state.name} />;
  }
}

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 (
      <Child
        ref={(foo) => {
          this.foo = foo;
        }}
      />
    );
  }
}

在 hook 中使用 ref 转发调用子组件方法需要用到useImperativeHandleforwardRef两个 API

3.Callback Functions

子组件传给父组件信息的最常见方式

const Child = ({ onClick }) => {
  <div onClick={() => onClick("misaka")}>Click Me</div>;
};

class Parent extends React.Component {
  handleClick = (data) => {
    console.log("Parent received value from child: " + data);
  };
  render() {
    return <Child onClick={this.handleClick} />;
  }
}

4.Event Bubbling

这个方法主要利用了原生 DOM 中的事件冒泡机制


class Parent extends React.Component {
  render() {
    return (
      <div onClick={this.handleClick}>
         <Child />
      </div>
    );
  }
  handleClick = () => {
    console.log('clicked')
  }
}
function Child {
  return (
    <button>Click</button>
  );
}

巧妙的利用下事件冒泡机制,我们就可以很方便的在父组件的元素上接收到来自子组件元素的点击事件

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 (
      <div>
        <SiblingA count={this.state.count} />
        <SiblingB onClick={this.setCount} />
      </div>
    );
  }
}

6.Context

通过React.Context提供的上下文环境,使得后代组件可以轻松拿到挂在根组件上的全局数据如用户信息、UI 主题、选择语言等

const ThemeContext = React.createContext("light");

class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

7.Portals

Portals 虽然不是用来解决组件通信问题的,但因为它也涉及到组件通信问题,所以也把它列在十种方法里

Portals 的主要应用场景是:当两个组件在 react 项目中是父子组件的关系,但在 HTML DOM 里并不想是父子元素的关系。一般多用于 Tooltip、Modal、Popup、Dropdown

// Portal

import { useEffect } from "react";
import { createPortal } from "react-dom";

const Portal = ({ children }) => {
  const mount = document.getElementById("portal-root");
  const el = document.createElement("div");

  useEffect(() => {
    mount.appendChild(el);
    return () => mount.removeChild(el);
  }, [el, mount]);

  return createPortal(children, el);
};

export default Portal;
// Parent
import Portal from "./Portal";

const Parent = () => {
  const [coords, setCoords] = useState({});

  return (
    <div style={{ overflow: "hidden" }}>
      <Button>Hover me</Button>
      <Portal>
        <Tooltip coords={coords}>
          Awesome content that is never cut off by its parent container!
        </Tooltip>
      </Portal>
    </div>
  );
};

8.Global Variables

这种办法慎用,注意全局变量污染


class ComponentA extends React.Component {
    handleClick = () => window.a = 'test'
    ...
}
class ComponentB extends React.Component {
    render() {
        return <div>{window.a}</div>
    }
}

9.Observer Pattern

DOM 提供了现成的 API 来发送自定义事件:CustomEvent,我们利用它来实现观察者模式

ComponentA 接收事件

class ComponentA extends React.Component {
  componentDidMount() {
    document.addEventListener("myEvent", this.handleEvent);
  }
  componentWillUnmount() {
    document.removeEventListener("myEvent", this.handleEvent);
  }

  handleEvent = (e) => {
    console.log(e.detail.log); //i'm misaka
  };
}

ComponentB 发送事件

class ComponentB extends React.Component {
  sendEvent = () => {
    document.dispatchEvent(
      new CustomEvent("myEvent", {
        detail: {
          log: "i'm misaka",
        },
      })
    );
  };

  render() {
    return <button onClick={this.sendEvent}>Send</button>;
  }
}

改良通信模块

改良通信模块可以专门建立一个 class 来管理

class EventBus {
  constructor() {
    this.bus = document.createElement("fakeelement");
  }

  addEventListener(event, callback) {
    this.bus.addEventListener(event, callback);
  }

  removeEventListener(event, callback) {
    this.bus.removeEventListener(event, callback);
  }

  dispatchEvent(event, detail = {}) {
    this.bus.dispatchEvent(new CustomEvent(event, { detail }));
  }
}

export default new EventBus();

手动实现

通过闭包对象,也可以手动实现观察者模式

function EventBus() {
  const subscriptions = {};
  // 订阅方法
  this.subscribe = (eventType, callback) => {
    // 创建唯一的symbol变量id
    const id = Symbol("id");
    // 判断事件名称是否已建立模型,没有则初始化一个对象
    if (!subscriptions[eventType]) subscriptions[eventType] = {};
    // 将 id-callback 的键值对放进事件对象中
    subscriptions[eventType][id] = callback;
    // 返回一个包含取消监听方法的对象,调用该方法时删除对应事件对象中的id
    return {
      unsubscribe() {
        delete subscriptions[eventType][id];
        if (
          Object.getOwnPropertySymbols(subscriptions[eventType]).length === 0
        ) {
          delete subscriptions[eventType];
        }
      },
    };
  };
  // 发布方法
  this.publish = (eventType, arg) => {
    // 判断事件对象不存在立即返回
    if (!subscriptions[eventType]) return;
    // 获取事件对象中全部symbol值,循环执行callback回调
    Object.getOwnPropertySymbols(subscriptions[eventType]).forEach((key) =>
      subscriptions[eventType][key](arg)
    );
  };
}

10.Redux 等

最后才是 Redux 等状态管理库,当项目庞大且前面的方法都不能很好满足需求时,才考虑使用 Redux 这种状态管理库

优点:全局数据统一管理、与组件树数据解耦

缺点:臃肿、影响加载速度与性能