[译]React组件通信


date: 2017-06-25 22:44
status: public

title: [译]React组件通信

翻译部分内容,全文可查看原文

组件间如何通信?答案是多种多样,因为这取决于组件之间的关系,以及你的喜好。

组件间通信有三种关系:

  • 从父组件到子组件 parent to child
  • 子组件到父组件child to parent
  • 非直接关系组件brothers, cousins

父组件到子组件

Parent to children

先从最常用,最简单的组件关系开始,通过props即可:

var ToggleButton = React.createClass({
  render: function() {
    return <label>{this.props.text}:
              <input type="checkbox" checked={this.props.checked} />
           </label>;
  }
});

var MyContainer = React.createClass({
  getInitialState: function() {
    return { checked: true };
  },
  render: function() {
    return <ToggleButton text="Toggle me" checked={this.state.checked} />;
  }
});

ES6版本:

const ToggleButton = ({ text, checked }) =>
  <label>{text}: <input type="checkbox" checked={checked} /></label>

class MyContainer extends React.Component {
  constructor() { super(); this.state = { checked: true } }
  render() {
    return <ToggleButton text="Toggle me" checked={this.state.checked} />
  }
}
ReactDOM.render(<MyContainer />, document.getElementById('root'))

MyContainer通过将自身的checked传递给子组件ToggleButton进行渲染,如果父组件的checked属性改变,会重新渲染子组件。

需要注意的是:子组件的checked状态来自父组件,他是无法操作直接改变父组件的checked属性

所以,在这个实例中,点击选择框是没有效果,会有warning提示

Warning: You provided a *checked* prop to a form field without an *onChange* handler.
This will render a read-only field.
If the field should be mutable use *defaultChecked*.
Otherwise, set either *onChange* or *readOnly*.
Check the *render* method of *ToggleButton*.

嵌套问题

Hierarchy problem

父组件传递子组件我们已经遇到,那父组件和孙子组件grand-child(子组件嵌套子组件)怎么能其乐融融交流呢?照目前我们有限的能力:没门,和孙子的代沟太大,我们需要通过子组件来传话。

听着别扭,难于维护,来看下实现方式:

var MyContainer = React.createClass({
  render: function() {
    return <Intermediate text="where is my son?" />;
  }
});
var Intermediate = React.createClass({
  render: function() {
    // Intermediate doesn't care of "text", but it has to pass it down nonetheless
    return <Child text={this.props.text} />;
  }
});
var Child = React.createClass({
  render: function() {
    return <span>{this.props.text}</span>;
  }
});

ES6版本:

const MyContainer = () => <Intermediate text="where is my son?" />
const Intermediate = ({ text }) => <Child text={text} />
const Child = ({ text }) => <span>{text}</span>

哟,爷爷辈和孙子辈沟通还要跨这个儿子,这让爷爷辈的很没面子,是该关爱中老年人,为他们做点什么。我们有个方案:context

上下文关联

Context

有这么一段时间,context是一个实验属性,无文档,不可靠,官方提供的三无产品,但是大家都喜欢用,为什么?因为很有用嘛,码的多了,谁没写过一些四世同堂的组件

现在好了,有了文档,却仍然被官方打上了实验的标签,未来某个版本会取消,因为官方一直强调:context不是最完美的解决方案,仅仅是没有办法的时候你才能打开的锦囊。

官方说的替代方案,可通过使用第三方框架,如reduxmobx去处理组件通信。

但是如若我们不想引入其他框架,仅仅想纯粹的使用react,那么就看看这个例子。

Context also brings its own set of problems: if it changes, the component that depends on it may not notified automatically. Solutions exist to fix this but it’s outside of React, such as explained here.

我们整理下context使用场景:

  • 组件是动态的
  • 组件是嵌套的
  • 需要传递很多属性

想要使用context,我们需要:

  • 父组件定义getChildContext,返回一个js对象
  • 父组件定义childContextTypes,用来定义类型React.PropTypes
  • 子组件定义contextTypes,用来定义需要的属性

上代码:

# 父组件
var MyContainer = React.createClass({
  getChildContext: function() {
    // we exposes one property "text", any of the components
    // in its sub-hierarchy will be able to access it
    return { text: 'Where is my son?' };
  },
  // we declare we expose one property "text" which is a string
  childContextTypes: {
    text: React.PropTypes.string
  },
  render: function() {
    // we pass nothing to the intermediate component
    return <Intermediate />;
  }
});
# 中间组件
// this component does nothing expect rendering a Child
var Intermediate = React.createClass({
  render: function() {
    return <Child />;
  }
});
# 底层组件
var Child = React.createClass({
  // we declare we want to read the "text" property of the context
  // and we expect it to be a string
  contextTypes: {
    text: React.PropTypes.string
  },
  render: function() {
    return <span>{this.context.text}</span>;
  }
});

ES6版本看这里:

class MyContainer extends React.Component {
  getChildContext() {
    return { text: 'Where is my son?' }
  }
  render() {
    return <Intermediate />
  }
}
MyContainer.childContextTypes = { text: React.PropTypes.string }

const Intermediate = () => <Child />

const Child = (props, context) => <span>{context.text}</span>
Child.contextTypes = { text: React.PropTypes.string }

在线示例点这里

子组件到父组件

Children to parent

子组件需要更改状态时,可通过事件驱动由父组件更改更新。

var ToggleButton = React.createClass({
  getInitialState: function() {
    return { checked: true };
  },
  onTextChanged: function() {
    console.log(this.state.checked); // it is always true for now
  },
  render: function() {
    return <label>{this.props.text}:
             <input type="checkbox" checked={this.state.checked}
                    onChange={this.onTextChanged}/>
           </label>;
  }
});

ES6:

class ToggleButton extends React.Component {
  constructor() {
    super();
    this.state = { checked: true }
  }
  onTextChanged() {
    console.log(this.state.checked) // it is always true for now
  }
  render() {
    return <label>{this.props.text}:
             <input type="checkbox" checked={this.state.checked}
                    onChange={() => this.onTextChanged()}/>
           </label>;
  }
}

完整实现代码如下:

// the parent
var MyContainer = React.createClass({
    getInitialState: function() {
        return { checked: false };
    },
    onChildChanged: function(newState) {
        this.setState({ checked: newState });
    },
    render: function() {
        return  <div>
                  <div>Are you checked ? {this.state.checked ? 'yes' : 'no'}</div>
                  <ToggleButton text="Toggle me"
                                initialChecked={this.state.checked}
                                callbackParent={this.onChildChanged} />
                </div>;
    }
});

// the child
var ToggleButton = React.createClass({
  getInitialState: function() {
    // we set our initial state from our props
    return { checked: this.props.initialChecked };
  },
  onTextChanged: function() {
    var newState = !this.state.checked;
    this.setState({ checked: newState }); // we update our state
    this.props.callbackParent(newState); // we notify our parent
  },
  render: function() {
    return <label>{this.props.text}: <input type="checkbox"
                                            checked={this.state.checked}
                                            onChange={this.onTextChanged}/></label>;
  }
});

ES6:

class MyContainer extends React.Component {
    constructor() {
      super();
      this.state = { checked: false }
    }
    onChildChanged(newState) {
      this.setState({ checked: newState })
    }
    render() { return <div>
      <div>Are you checked ? {this.state.checked ? 'yes' : 'no'}</div>
        <ToggleButton text="Toggle me"
                      initialChecked={this.state.checked}
                      callbackParent={(newState) => this.onChildChanged(newState) } />
      </div>
    }
}

class ToggleButton extends React.Component {
  constructor({ initialChecked }) {
    super();
    this.state = { checked: initialChecked }
  }
  onTextChanged() {
    const newState = !this.state.checked;
    this.setState({ checked: newState }); // we update our state
    this.props.callbackParent(newState); // we notify our parent
  }
  render() {
    return <label>{this.props.text}: <input type="checkbox"
                                            checked={this.state.checked}
                                            onChange={() => this.onTextChanged()}/></label>
  }
}

让我们休息下(根本停不下...),看下React如何处理DOM事件

React有自己的DOM事件层

当我们处理事件,像onChange,我们需要注意一下几点:

  • this:this是组件本身
  • 参数:事件如onTextChanged(e),不是一个标准的JS事件,其实是React的SyntheticEvent

[图示]()

react将自己的事件层加载了JS事件系统顶部,使用了事件代理,事件绑定在了根DOM:

document.on('change', 'input[data-reactid=".0.2"]', function() { ... })

This code is not from React, it’s just an example to explain how React handles the events.

如果没问题,那么react是这样处理事件的:

var listenTo = ReactBrowserEventEmitter.listenTo;
...
function putListener(id, registrationName, listener, transaction) {
...
  var container = ReactMount.findReactContainerForID(id);
  if (container) {
    var doc = container.nodeType === ELEMENT_NODE_TYPE ?
      container.ownerDocument :
      container;
    listenTo(registrationName, doc);
}
...
// at the very of end of the listenTo inner functions, we can find the core function:
target.addEventListener(eventType, callback, false);

react支持的全部事件列表

组件之间没有关联

My components are not directly related!

这个,我们可以通过基本事件模式实现:

  • 监听事件发生
  • 发生后,执行回调事件

Event Emitter:

  • subscribe:otherObject.addEventListener('click', function() { alert('click!') })
  • dispatch:this.dispatchEvent('click')

单例模式:

  • subscribe:otherObject.clicked.add(function() { alert('click') })
  • dispatch: this.clicked.dispatch()

Publish/Subscribe:

  • subscribe:globalBroadcaster.subscribe('click', function(who) { alert('click from ' + who) })
  • dispatch: globalBroadcaster.publish('click', 'me!')

实现方式:

// any object can just extends EventEmitter to have access to `this.subscribe` and `this.dispatch`
var EventEmitter = {
    _events: {},
    dispatch: function (event, data) {
        if (!this._events[event]) return; // no one is listening to this event
        for (var i = 0; i < this._events[event].length; i++)
            this._events[event][i](data);
    },
    subscribe: function (event, callback) {
      if (!this._events[event]) this._events[event] = []; // new event
      this._events[event].push(callback);
    }
    // note that we do not handle unsubscribe here
}

// Anywhere we have a reference to a Person (that extends EventEmitter)
person.subscribe('namechanged', function(data) { alert(data.name); });

// in the Person object
this.dispatch('namechanged', { name: 'John' });

相关的库可使用Mobx,Redux

原文

How to communicate between ReactJS components

@2017-06-25 22:45