搜索
首页web前端js教程深入浅析React组件间通信的10种方法
深入浅析React组件间通信的10种方法Feb 25, 2021 am 10:10 AM
react组件组件通信

深入浅析React组件间通信的10种方法

【相关教程推荐:React视频教程

这两天被临时抽调到别的项目组去做一个小项目的迭代。这个项目前端是用React,只是个小型项目所以并没有使用Redux等状态管理的库。刚好遇到了一个小问题:两个不太相关的组件到底该怎么进行通信。我觉得这个问题还挺有趣的,所以把我的思考过程写下来,大家也可以一起讨论讨论。

虽然重点是要讲两个不相关的组件间的通信,但我还是从最常见的父子组件通信讲起,大家就当温故而知新了。先把完整的总结列出来,然后再详细展开。

组件间通信方式总结

  • 父组件 => 子组件:

    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

这是最常见的react组件之间传递信息的方法了吧,父组件通过props把数据传给子组件,子组件通过this.props去使用相应的数据

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

class Parent extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            name: &#39;zach&#39;
        }
    }
    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 &#39;hello&#39;
  }
  render() {
    return (
      <Child
        ref={foo => {
          this.foo = foo
        }}
      />
    )
  }
}

大致的过程:

  1. 首先子组件有一个方法myFunc
  2. 父组件给子组件传递一个ref属性,并且采用callback-refs的形式。这个callback函数接收react组件实例/原生dom元素作为它的参数。当父组件挂载时,react会去执行这个ref回调函数,并将子组件实例作为参数传给回调函数,然后我们把子组件实例赋值给this.foo
  3. 最后我们在父组件当中就可以使用this.foo来调用子组件的方法咯

了解了这个方法的原理后,我们要考虑的问题就是为啥我们要用这种方法,它的使用场景是什么?最常见的一种使用场景:比如子组件是一个modal弹窗组件,子组件里有显示/隐藏这个modal弹窗的各种方法,我们就可以通过使用这个方法,直接在父组件上调用子组件实例的这些方法来操控子组件的显示/隐藏。这种方法比起你传递一个控制modal显示/隐藏的props给子组件要美观多了。

class Modal extends React.Component {
  show = () => {// do something to show the modal}
  hide = () => {// do something to hide the modal}
  render() {
    return <div>I&#39;m a modal</div>
  }
}

class Parent extends React.Component {
  componentDidMount() {
    if(// some condition) {
        this.modal.show()
    }
  }
  render() {
    return (
      <Modal
        ref={el => {
          this.modal = el
        }}
      />
    )
  }
}

3. Callback Functions

讲完了父组件给子组件传递信息的两种方式,我们再来讲子组件给父组件传递信息的方法。回调函数这个方法也是react最常见的一种方式,子组件通过调用父组件传来的回调函数,从而将数据传给父组件。

const Child = ({ onClick }) => {
    <div onClick={() => onClick(&#39;zach&#39;)}>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

这种方法其实跟react本身没有关系,我们利用的是原生dom元素的事件冒泡机制。

class Parent extends React.Component {
  render() {
    return (
      <div onClick={this.handleClick}>
         <Child />
      </div>
    );
  }
  handleClick = () => {
    console.log(&#39;clicked&#39;)
  }
}
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

通常一个前端应用会有一些"全局"性质的数据,比如当前登陆的用户信息、ui主题、用户选择的语言等等。这些全局数据,很多组件可能都会用到,当组件层级很深时,用我们之前的方法,就得通过props一层一层传递下去,这显然太麻烦了,看下面的示例:

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

function Toolbar(props) {
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

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

上面的例子,为了让我们的Button元素拿到主题色,我们必须把theme作为props,从App传到Toolbar,再从Toolbar传到ThemedButton,最后Button从父组件ThemedButton的props里终于拿到了主题theme。假如我们不同组件里都有用到Button,就得把theme向这个例子一样到处层层传递,麻烦至极。

因此react为我们提供了一个新api:Context,我们用Context改写下上例

const ThemeContext = React.createContext(&#39;light&#39;);

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} />;
  }
}

简单的解析一下:

  1. React.createContext创建了一个Context对象,假如某个组件订阅了这个对象,当react去渲染这个组件时,会从离这个组件最近的一个Provider组件中读取当前的context值
  2. Context.Provider: 每一个Context对象都有一个Provider属性,这个属性是一个react组件。在Provider组件以内的所有组件都可以通过它订阅context值的变动。具体来说,Provider组件有一个叫value的prop传递给所有内部组件,每当value的值发生变化时,Provider内部的组件都会根据新value值重新渲染
  3. 那内部的组件该怎么使用这个context对象里的东西呢?
    a. 假如内部组件是用class声明的有状态组件:我们可以把Context对象赋值给这个类的属性contextType,如上面所示的ThemedButton组件

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

    b. 假如内部组件是用function创建的无状态组件:我们可以使用Context.Consumer,这也是Context对象直接提供给我们的组件,这个组件接受一个函数作为自己的child,这个函数的入参就是context的value,并返回一个react组件。可以将上面的ThemedButton改写下:

        function ThemedButton {
            return (
                <ThemeContext.Consumer>
                    {value => <Button theme={value} />}
                </ThemeContext.Consumer>
            )
        }

最后提一句,context对于解决react组件层级很深的props传递很有效,但也不应该被滥用。只有像theme、language等这种全局属性(很多组件都有可能依赖它们)时,才考虑用context。如果只是单纯为了解决层级很深的props传递,可以直接用component composition

7. Portals

Portals也是react提供的新特性,虽然它并不是用来解决组件通信问题的,但因为它也涉及到了组件通信的问题,所以我也把它列在我们的十种方法里面。

Portals的主要应用场景是:当两个组件在react项目中是父子组件的关系,但在HTML DOM里并不想是父子元素的关系。

举个例子,有一个父组件Parent,它里面包含了一个子组件Tooltip,虽然在react层级上它们是父子关系,但我们希望子组件Tooltip渲染的元素在DOM中直接挂载在body节点里,而不是挂载在父组件的元素里。这样就可以避免父组件的一些样式(如overflow:hiddenz-indexposition等)导致子组件无法渲染成我们想要的样式。

如下图所示,父组件是这个红色框的范围,并且设置了overflow:hidden,这时候我们的Tooltip元素超出了红色框的范围就被截断了。

1.png

怎么用portals解决呢?

首先,修改html文件,给portals增加一个节点

<html>
    <body>
        <div id="react-root"></div>
        <div id="portal-root"></div>
    </body>
</html>

然后我们创建一个可复用的portal容器,这里使用了react hooks的语法,看不懂的先过去看下我另外一篇讲解react hooks的文章:30分钟精通React今年最劲爆的新特性——React Hooks

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;

最后在父组件中使用我们的portal容器组件,并将Tooltip作为children传给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>
}

这样就ok啦,虽然父组件仍然是overflow: hidden,但我们的Tooltip再也不会被截断了,因为它直接超脱了,它渲染到body节点下的a5ed60f5e337070d20bb216c9390d13e16b28748ea4df4d9c2150843fecfba68里去了。

总结下适用的场景: Tooltip、Modal、Popup、Dropdown等等

8. Global Variables

哈哈,这也不失为一个可行的办法啊。当然你最好别用这种方法。

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

9. Observer Pattern

观察者模式是软件设计模式里很常见的一种,它提供了一个订阅模型,假如一个对象订阅了某个事件,当那个事件发生的时候,这个对象将收到通知。

这种模式对于我们前端开发者来说是最不陌生的了,因为我们经常会给某些元素添加绑定事件,会写很多的event handlers,比如给某个元素添加一个点击的响应事件elm.addEventListener('click', handleClickEvent),每当elm元素被点击时,这个点击事件会通知elm元素,然后我们的回调函数handleClickEvent会被执行。这个过程其实就是一个观察者模式的实现过程。

那这种模式跟我们讨论的react组件通信有什么关系呢?当我们有两个完全不相关的组件想要通信时,就可以利用这种模式,其中一个组件负责订阅某个消息,而另一个元素则负责发送这个消息。javascript提供了现成的api来发送自定义事件: CustomEvent,我们可以直接利用起来。

首先,在ComponentA中,我们负责接受这个自定义事件:

class ComponentA extends React.Component {
    componentDidMount() {
        document.addEventListener(&#39;myEvent&#39;, this.handleEvent)
    }
    componentWillUnmount() {
        document.removeEventListener(&#39;myEvent&#39;, this.handleEvent)
    }
    
    handleEvent = (e) => {
        console.log(e.detail.log)  //i&#39;m zach
    }
}

然后,ComponentB中,负责在合适的时候发送该自定义事件:

class ComponentB extends React.Component {
    sendEvent = () => {
        document.dispatchEvent(new CustomEvent(&#39;myEvent&#39;, {
          detail: {
             log: "i&#39;m zach"
          }
        }))
    }
    
    render() {
        return <button onClick={this.sendEvent}>Send</button>
    }
}

这样我们就用观察者模式实现了两个不相关组件之间的通信。当然现在的实现有个小问题,我们的事件都绑定在了document上,这样实现起来方便,但很容易导致一些冲突的出现,所以我们可以小小的改良下,独立一个小模块EventBus专门这件事:

class EventBus {
    constructor() {
        this.bus = document.createElement(&#39;fakeelement&#39;);
    }

    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

然后我们就可以愉快的使用它了,这样就避免了把所有事件都绑定在document上的问题:

import EventBus from &#39;./EventBus&#39;
class ComponentA extends React.Component {
    componentDidMount() {
        EventBus.addEventListener(&#39;myEvent&#39;, this.handleEvent)
    }
    componentWillUnmount() {
        EventBus.removeEventListener(&#39;myEvent&#39;, this.handleEvent)
    }
    
    handleEvent = (e) => {
        console.log(e.detail.log)  //i&#39;m zach
    }
}
class ComponentB extends React.Component {
    sendEvent = () => {
        EventBus.dispatchEvent(&#39;myEvent&#39;, {log: "i&#39;m zach"}))
    }
    
    render() {
        return <button onClick={this.sendEvent}>Send</button>
    }
}

最后我们也可以不依赖浏览器提供的api,手动实现一个观察者模式,或者叫pub/sub,或者就叫EventBus。

function EventBus() {
  const subscriptions = {};
  this.subscribe = (eventType, callback) => {
    const id = Symbol(&#39;id&#39;);
    if (!subscriptions[eventType]) subscriptions[eventType] = {};
    subscriptions[eventType][id] = callback;
    return {
      unsubscribe: function unsubscribe() {
        delete subscriptions[eventType][id];
        if (Object.getOwnPropertySymbols(subscriptions[eventType]).length === 0) {
          delete subscriptions[eventType];
        }
      },
    };
  };

  this.publish = (eventType, arg) => {
    if (!subscriptions[eventType]) return;

    Object.getOwnPropertySymbols(subscriptions[eventType])
      .forEach(key => subscriptions[eventType][key](arg));
  };
}
export default EventBus;

10. Redux等

最后终于来到了大家喜闻乐见的Redux等状态管理库,当大家的项目比较大,前面讲的9种方法已经不能很好满足项目需求时,才考虑下使用redux这种状态管理库。这里就先不展开讲解redux了...否则我花这么大力气讲解前面9种方法的意义是什么???

总结

十种方法,每种方法都有对应的适合它的场景,大家在设计自己的组件前,一定要好好考虑清楚采用哪种方式来解决通信问题。

文初提到的那个小问题,最后我采用方案9,因为既然是小迭代项目,又是改别人的代码,当然最好避免对别人的代码进行太大幅度的改造。而pub/sub这种方式就挺小巧精致的,既不需要对别人的代码结构进行大改动,又可以满足产品需求。

更多编程相关知识,请访问:编程视频!!

以上是深入浅析React组件间通信的10种方法的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文转载于:segmentfault。如有侵权,请联系admin@php.cn删除
react中canvas的用法是什么react中canvas的用法是什么Apr 27, 2022 pm 03:12 PM

在react中,canvas用于绘制各种图表、动画等;可以利用“react-konva”插件使用canvas,该插件是一个canvas第三方库,用于使用React操作canvas绘制复杂的画布图形,并提供了元素的事件机制和拖放操作的支持。

React是双向数据流吗React是双向数据流吗Apr 21, 2022 am 11:18 AM

React不是双向数据流,而是单向数据流。单向数据流是指数据在某个节点被改动后,只会影响一个方向上的其他节点;React中的表现就是数据主要通过props从父节点传递到子节点,若父级的某个props改变了,React会重渲染所有子节点。

react中antd和dva是什么意思react中antd和dva是什么意思Apr 21, 2022 pm 03:25 PM

在react中,antd是基于Ant Design的React UI组件库,主要用于研发企业级中后台产品;dva是一个基于redux和“redux-saga”的数据流方案,内置了“react-router”和fetch,可理解为应用框架。

react中为什么使用nodereact中为什么使用nodeApr 21, 2022 am 10:34 AM

因为在react中需要利用到webpack,而webpack依赖nodejs;webpack是一个模块打包机,在执行打包压缩的时候是依赖nodejs的,没有nodejs就不能使用webpack,所以react需要使用nodejs。

react是组件化开发吗react是组件化开发吗Apr 22, 2022 am 10:44 AM

react是组件化开发;组件化是React的核心思想,可以开发出一个个独立可复用的小组件来构造应用,任何的应用都会被抽象成一颗组件树,组件化开发也就是将一个页面拆分成一个个小的功能模块,每个功能完成自己这部分独立功能。

react和reactdom有什么区别react和reactdom有什么区别Apr 27, 2022 am 10:26 AM

react和reactdom的区别是:ReactDom只做和浏览器或DOM相关的操作,例如“ReactDOM.findDOMNode()”操作;而react负责除浏览器和DOM以外的相关操作,ReactDom是React的一部分。

react中forceupdate的用法是什么react中forceupdate的用法是什么Apr 19, 2022 pm 12:03 PM

在react中,forceupdate()用于强制使组件跳过shouldComponentUpdate(),直接调用render(),可以触发组件的正常生命周期方法,语法为“component.forceUpdate(callback)”。

react有没有双向绑定react有没有双向绑定Apr 21, 2022 am 10:24 AM

react中没有双向绑定;react的设计思想就是单向数据流,没有双向绑定的概念;react是view层,单项数据流只能由父组件通过props将数据传递给子组件,满足了view层渲染的要求并且更易测试与控制,所以在react中没有双向绑定。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前By尊渡假赌尊渡假赌尊渡假赌

热工具

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

螳螂BT

螳螂BT

Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

功能强大的PHP集成开发环境

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中