React的Nested Component模式
leverli
8年前
<h2>JSX dot notation</h2> <p>一个偶然的机会,发现React的JSX语法里,Component Type是可以写成这样的:</p> <pre> <code class="language-javascript"><this.FlatButton /></code></pre> <p>React/JSX的Component Type是支持dot notation的,主要是为了方便把一组Component装在一个Object容器里,这样在export/import的时候很方便;如果一个Component是匿名的或者名字是小写字母开头的,JSX并不接受,但可以用一个大写字母开始的变量名来转换一下。</p> <p>在React的官方文档中给出来的使用dot notation的例子是:</p> <pre> <code class="language-javascript"><MyComponents.DatePicker color="blue" /></code></pre> <p>它给人一个错觉,似乎容器的名字也必须是大写字母开始的,但简单试一下就知道并非如此,dot notation前面的容器名字没有任何限制。例如</p> <pre> <code class="language-javascript">import { FlatButton } from 'material-ui' const wrap = { MyButton: FlatButton } // in render <wrap.MyButton label='hello' /></code></pre> <p>是完全可用的。Anyway,我们澄清了一个细节,JSX支持dot notation,这一点没问题,dot前面的容器对象名称无限制,所以 <this.FlatButton /> 可以工作。</p> <h2>Nested Component</h2> <p>那么,为什么要这样用呢?</p> <p>我举一个例子。比如在绑定行为时我们经常有这样的写法:</p> <pre> <code class="language-javascript">class Foo extends React.Component { handle() { // ... } render () { <FlatButton onTouchTap={this.handle.bind(this)} /> } }</code></pre> <p>如果不想总是写 bind(this) ,我们可以换一种方式写:</p> <pre> <code class="language-javascript">class Foo extends React.Component { constructor(props) { super(props) this.handle = () => { // ... } } render () { <FlatButton onTouchTap={this.handle} /> } }</code></pre> <p>把 handle 从类方法中搬到构造函数里去定义成为arrow function,虽然arrow function不能bind this,但是在函数内写this仍然是有效的,因为constructor里有this。</p> <p>上面的例子里没有参数传递,如果有参数传递,后者的写法很少会犯错误,但前者有时候忘了bind,或者搞错了参数形式都容易出问题。</p> <h2>Function Component</h2> <p>同样的,如果我们把component用类似的方式定义在constructor内,如果这是一个function component,它可以直接访问容器内的 this ,当然也就能直接访问容器内的 this.state 和 this.props ,这样直接的好处就是可以不用props翻译父组件的state或者props传递给子组件。</p> <p>如果子组件对应多个数据对象实例,那么只要把这个数据对象本身作为props传递给子组件即可,例如:</p> <pre> <code class="language-javascript">class Foo extends React.Component { constructor(props) { super() this.state = { selected: [] } this.deleteItem = item => { //... } this.Bar = props => { let item = props.item return ( <div> {item.name} <FlatButton label='delete' onTouchTap={() => this.deleteItem(item)} /> </div> ) } } render() { <div> { this.props.items.map(item => <this.Bar item={item} />) } </div> } }</code></pre> <p>这样写的 Bar Component,直接在父组件的构造函数内,它当然可以随意访问父容器的 state 和 props ;</p> <p>但好处不限于此。</p> <p>在实际的场景中,常常出现因为 item 的数据对象是多态的,我们可能需要定义很多种 Bar 来实现不同的显示和行为,在这种情况下,各种 Bar 的实现里,不论是行为还是表示,都有很多共用的地方。但是React的Component并不能使用继承的方式来实现共性;所以实际的情况是:</p> <ol> <li> <p>对于表示,如果 MyFirstBar 和 MySecondBar 之间需要共用,那么仍然需要抽取Component。</p> </li> <li> <p>对于行为,写在父组件里,向子组件binding。</p> </li> </ol> <p>但是如果写成上述的形式,抽取共用的部分仍然可以写成 this.BarCommonPart 这样的形式,同样的,无须传递 props ,抽取共同行为的部分就更加简单了,在子组件之内直接调用父组件方法即可,不需要用 onSomethingHappened 之类的 props 传递。</p> <h2>Class Component</h2> <p>当然上面写的都是Function Component,可以定义为arrow function,写在父组件的构造函数里,共享父组件的 this ,那么如果子组件需要有态呢?需要是Class Component呢?</p> <p>同样可以。</p> <p>虽然我们可能很少在实践中写出匿名class,但是在JavaScript里它是合法的。上面的 Bar 如果是Class Component,结果是这样:</p> <pre> <code class="language-javascript">class Foo extends React.Component { constructor(props) { super() const that = this this.state = { selected: [] } this.deleteItem = item => { //... } this.Bar = class extends React.Component { constructor(props) { super(props) this.state = { open: false } } render() { let item = this.props.item return ( <div> {item.name} <FlatButton label='delete' onTouchTap={() => that.deleteItem(item)} /> </div> ) } } } render() { <div> { this.props.items.map(item => <this.Bar item={item} />) } </div> } }</code></pre> <p>写成这样之后,在Bar里面的 this 不再指向父组件了,而是指向了子组件自己;但是我们可以在父组件容器里定义一个 that ,作为闭包或者叫词法域(lexical scope)变量,在整个 Bar 的内部这个 that 都是可用的。</p> <p>这样无论是Function Component还是Class Component都可以nest在父组件中,不仅可以直接访问父组件的全部上下文,更可以方便共享表示和行为,直接在子组件的方法内调用 this.setState() 或者 that.setState() 更新父组件的行为也完全不是问题。</p> <h2>And More</h2> <p>还不仅如此;</p> <p>父组件作为上下文还有其他功效,例如:</p> <pre> <code class="language-javascript">class Foo extends React.Component { constructor(props) { super() const that = this this.colors = { primary: () => '#FF89E0', secondary: () => '#DD7633', // ... } this.dims = { tableHeaderHeight: () => 64, tabelDataHeight: () => 48, // ... } this.styles = { mainText: () => ({ fontSize: 14, fontWeight: this.state.editing ? 'normal' : 'bold', }) // ... } } }</code></pre> <p>你可以看出父组件完全可以自己作为一个上下文的小世界,定义统一的color, dimension和style体系;他们都在父组件的构造函数内,因此可以在此访问所有状态,如果需要在这个组件内做动态,这非常方便。</p> <h2>Summary</h2> <p>如果你理解JavaScript的class和闭包是高度相似的(把function scope当成对象来理解),你就理解这个Pattern的要义:把React.Component从class对象翻成了类似闭包的基于lexical scope的context工作的方式。</p> <p>既然React.Component不能基于class继承实现重用,那么为什么不这么做让书写代码变得容易呢?在这个context内,你连额外的状态管理器(例如redux)都不需要,因为一切都是全局的,在任何地方都可以调用父组件的 setState 方法,而结果就是所有子组件都可以体现变化。</p> <p>我在过去的两天里把一个大约2000-3000行代码的单页面写成了这种形式,目前感觉非常好,不再有奇怪的不容易觉察的行为binding,也扔掉了所有的 props/state 传递,也不需要什么额外的东西来管理状态。</p> <p>当然这种做法反模式的地方是,这样写在容器内的组件在外部无法重用了,是的,如果需要外部重用我们仍然要回到写独立的React组件的模式,但是对于实际应用中,很多复杂组件都有自己的独特性,而容器拆解不可避免,所以至少在不太需要外部重用的地方,这种Nested Component Pattern,是一种不但可行,而且非常简洁易用的方式。</p> <p> </p> <p>来自:https://segmentfault.com/a/1190000008014225</p> <p> </p>