[関連チュートリアルの推奨事項: React ビデオ チュートリアル ]
以前、小規模プロジェクトに取り組むために別のプロジェクト チームに一時的に異動しました。 2 日間繰り返します。このプロジェクトのフロントエンドは React を使用していますが、小規模なプロジェクトなので Redux などの状態管理ライブラリは使用していません。関係のない 2 つのコンポーネント間で通信する方法という小さな問題が発生しました。この質問は非常に興味深いと思うので、みんなで議論できるように私の思考プロセスを書き留めておきます。
関連のない 2 つのコンポーネント間の通信に焦点を当てていますが、誰もが過去から学べるように、親コンポーネントと子コンポーネント間の最も一般的な通信から始めます。最初に完全な概要をリストし、次に詳細を展開します。
コンテキスト
const Child = ({ name }) => { <div>{name}</div> } class Parent extends React.Component { constructor(props) { super(props) this.state = { name: 'zach' } } render() { return ( <Child name={this.state.name} /> ) } }
2. インスタンス メソッド親コンポーネントから子コンポーネントに情報を渡す 2 番目の方法は、一部の学生には馴染みがないかもしれませんが、この方法は非常に便利なので、必ずマスターしてください。 . .原理: 親コンポーネントは、
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 }} /> ) } }
一般的なプロセス:
this.foo
を使用して、サブコンポーネントのメソッドを呼び出すことができます。 このメソッドの原理を理解した後、次のことを行う必要があります。考慮すべき問題は、なぜこの方法を使用するのか、またその使用シナリオは何なのかということです。最も一般的な使用シナリオ: たとえば、サブコンポーネントはモーダル ポップアップ コンポーネントです。サブコンポーネントには、モーダル ポップアップ ウィンドウを表示/非表示にするさまざまなメソッドがあります。このメソッドを使用して、親コンポーネント上でサブコンポーネントを直接呼び出すことができます。これらのメソッドインスタンスの は、子コンポーネントの表示/非表示を制御するために使用されます。このメソッドは、モーダルの表示/非表示を制御する props を子コンポーネントに渡すよりもはるかに美しいです。
class Modal extends React.Component { show = () => {// do something to show the modal} hide = () => {// do something to hide the modal} render() { return <div>I'm a modal</div> } } class Parent extends React.Component { componentDidMount() { if(// some condition) { this.modal.show() } } render() { return ( <Modal ref={el => { this.modal = el }} /> ) } }
親コンポーネントが子コンポーネントに情報を送信する 2 つの方法について説明した後、子コンポーネントが親コンポーネントに情報を送信する方法について説明します。コールバック関数メソッドも最も一般的な反応方法であり、子コンポーネントは、親コンポーネントから渡されたコールバック関数を呼び出すことによって、親コンポーネントにデータを渡します。
const Child = ({ onClick }) => { <div onClick={() => onClick('zach')}>Click Me</div> } class Parent extends React.Component { handleClick = (data) => { console.log("Parent received value from child: " + data) } render() { return ( <Child onClick={this.handleClick} /> ) } }
このメソッドは実際には React 自体とは何の関係もなく、ネイティブ 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. 親コンポーネント
についての説明を終えて親コンポーネントと子コンポーネント間の通信については、親以外のコンポーネントと子コンポーネント間の通信メソッドを見てみましょう。一般に、親子ではない 2 つのコンポーネントが通信したい場合、まずそれらが兄弟コンポーネントであるかどうか、つまり同じ親コンポーネントの下にあるかどうかを確認できます。そうでない場合は、それらをコンポーネントにラップして兄弟にすることが適切かどうかを検討してください。このようにして、中間層としての親コンポーネントを介したデータ通信を実現できます。
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> ); } }
通常、フロントエンド アプリケーションには、現在ログインしているユーザー情報、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} />; } }
したがって、react は新しい API を提供します: Context。Context を使用して上記の例を書き直します。
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} />; } }
簡単な分析:
React.createContext
创建了一个Context对象,假如某个组件订阅了这个对象,当react去渲染这个组件时,会从离这个组件最近的一个Provider
组件中读取当前的context值Context.Provider
: 每一个Context对象都有一个Provider
属性,这个属性是一个react组件。在Provider组件以内的所有组件都可以通过它订阅context值的变动。具体来说,Provider组件有一个叫value
的prop传递给所有内部组件,每当value
的值发生变化时,Provider内部的组件都会根据新value值重新渲染那内部的组件该怎么使用这个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
Portals也是react提供的新特性,虽然它并不是用来解决组件通信问题的,但因为它也涉及到了组件通信的问题,所以我也把它列在我们的十种方法里面。
Portals的主要应用场景是:当两个组件在react项目中是父子组件的关系,但在HTML DOM里并不想是父子元素的关系。
举个例子,有一个父组件Parent,它里面包含了一个子组件Tooltip,虽然在react层级上它们是父子关系,但我们希望子组件Tooltip渲染的元素在DOM中直接挂载在body节点里,而不是挂载在父组件的元素里。这样就可以避免父组件的一些样式(如overflow:hidden
、z-index
、position
等)导致子组件无法渲染成我们想要的样式。
如下图所示,父组件是这个红色框的范围,并且设置了overflow:hidden
,这时候我们的Tooltip元素超出了红色框的范围就被截断了。
怎么用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节点下的<div id="portal-root"></div>
里去了。
总结下适用的场景: Tooltip、Modal、Popup、Dropdown等等
哈哈,这也不失为一个可行的办法啊。当然你最好别用这种方法。
class ComponentA extends React.Component { handleClick = () => window.a = 'test' ... } class ComponentB extends React.Component { render() { return <div>{window.a}</div> } }
观察者模式是软件设计模式里很常见的一种,它提供了一个订阅模型,假如一个对象订阅了某个事件,当那个事件发生的时候,这个对象将收到通知。
这种模式对于我们前端开发者来说是最不陌生的了,因为我们经常会给某些元素添加绑定事件,会写很多的event handlers,比如给某个元素添加一个点击的响应事件elm.addEventListener('click', handleClickEvent)
,每当elm元素被点击时,这个点击事件会通知elm元素,然后我们的回调函数handleClickEvent会被执行。这个过程其实就是一个观察者模式的实现过程。
那这种模式跟我们讨论的react组件通信有什么关系呢?当我们有两个完全不相关的组件想要通信时,就可以利用这种模式,其中一个组件负责订阅某个消息,而另一个元素则负责发送这个消息。javascript提供了现成的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 zach } }
然后,ComponentB中,负责在合适的时候发送该自定义事件:
class ComponentB extends React.Component { sendEvent = () => { document.dispatchEvent(new CustomEvent('myEvent', { detail: { log: "i'm zach" } })) } render() { return <button onClick={this.sendEvent}>Send</button> } }
这样我们就用观察者模式实现了两个不相关组件之间的通信。当然现在的实现有个小问题,我们的事件都绑定在了document上,这样实现起来方便,但很容易导致一些冲突的出现,所以我们可以小小的改良下,独立一个小模块EventBus专门这件事:
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
然后我们就可以愉快的使用它了,这样就避免了把所有事件都绑定在document上的问题:
import EventBus from './EventBus' class ComponentA extends React.Component { componentDidMount() { EventBus.addEventListener('myEvent', this.handleEvent) } componentWillUnmount() { EventBus.removeEventListener('myEvent', this.handleEvent) } handleEvent = (e) => { console.log(e.detail.log) //i'm zach } } class ComponentB extends React.Component { sendEvent = () => { EventBus.dispatchEvent('myEvent', {log: "i'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('id'); 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;
最后终于来到了大家喜闻乐见的Redux等状态管理库,当大家的项目比较大,前面讲的9种方法已经不能很好满足项目需求时,才考虑下使用redux这种状态管理库。这里就先不展开讲解redux了...否则我花这么大力气讲解前面9种方法的意义是什么???
十种方法,每种方法都有对应的适合它的场景,大家在设计自己的组件前,一定要好好考虑清楚采用哪种方式来解决通信问题。
文初提到的那个小问题,最后我采用方案9,因为既然是小迭代项目,又是改别人的代码,当然最好避免对别人的代码进行太大幅度的改造。而pub/sub这种方式就挺小巧精致的,既不需要对别人的代码结构进行大改动,又可以满足产品需求。
更多编程相关知识,请访问:编程视频!!
以上がReact コンポーネント間で通信する 10 の方法の詳細な分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。