美文网首页
React 之 Render Props 的设计模式

React 之 Render Props 的设计模式

作者: 472abb2e4941 | 来源:发表于2018-11-25 18:08 被阅读34次

    很多人应该看到官方的高阶指南, 新加了 “render props” 的设计模式, 注意这只是一个设计模式, 不是新的 api, 而且有意思的是,2016年年底的时候, 我看到一个 sortable 的列表的 react 开源库代码时候, 就见识了这种写法,当时很不理解。因为 react 灵活性很大,code resusable 的方式有很多种,最开始的 mixin,cloneElement,props.children,HOC,可能会有细微的区别,这里我们讲讲 render props 的设计模式的特别的地方。(匆匆写完,如有错误请指出)

    基本使用

    官网的例子解释有一点啰嗦,我们来个精简版,我们需要一个显示鼠标位置的 MouseTracker:

    class MouseTracker extends React.Component {
      constructor(props) {
        super(props);
        this.handleMouseMove = this.handleMouseMove.bind(this);
        this.state = { x: 0, y: 0 };
      }
    
      handleMouseMove(event) {
        this.setState({
          x: event.clientX,
          y: event.clientY
        });
      }
    
      render() {
        return (
          <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
            <h1>Move the mouse around!</h1>
            <p>The current mouse position is ({this.state.x}, {this.state.y})</p>  // 如果我们这里想展示另外的东西怎么办?
          </div>
        );
      }
    }
    

    开始:

    // 把和鼠标移动相关的东西抽出来
    class Mouse extends React.Component {
      constructor(props) {
        super(props);
        this.handleMouseMove = this.handleMouseMove.bind(this);
        this.state = { x: 0, y: 0 };
      }
    
      handleMouseMove(event) {
        this.setState({
          x: event.clientX,
          y: event.clientY
        });
      }
    
      render() {
        return (
          <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
             <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
          </div>
        );
      }
    }
    
    
    // 此时 mouseTracker 变成了这样
    class MouseTracker extends React.Component {
      render() {
        return (
          <div>
            <h1>Move the mouse around!</h1>
            <Mouse />
          </div>
        );
      }
    }
    
    

    完成:

    //  p 标签的部分用 this.props.render() 代替
    class Mouse extends React.Component {
      constructor(props) {
        super(props);
        this.handleMouseMove = this.handleMouseMove.bind(this);
        this.state = { x: 0, y: 0 };
      }
    
      handleMouseMove(event) {
        this.setState({
          x: event.clientX,
          y: event.clientY
        });
      }
    
      render() {
        return (
          <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
             {this.props.render(state)}
          </div>
        );
      }
    }
    
    // 定义一个 UI 组件,也就是我们希望替换 p 标签的那一段
    class Cat extends React.Component {
      render() {
        const mouse = this.props.mouse;
        return (
          <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
        );
      }
    }
    
    // 改写后的 MouseTracker
    class MouseTracker extends React.Component {
      render() {
        return (
          <div>
            <h1>Move the mouse around!</h1>
            <Mouse render={mouse => (
              <Cat mouse={mouse} />
            )}/>
          </div>
        );
      }
    }
    

    额外提及

    注意的是 render props 如上面所说是个设计模式, 所以没有必要一定要取名叫 render,官网举例说你可以用 children 作为 prop 的名字,这样你甚至都不用显示去声明它,如下:

    class MouseTracker extends React.Component {
      render() {
        return (
          <div>
            <h1>Move the mouse around!</h1>
            <Mouse>
              {mouse => (
                <p>The mouse position is {mouse.x}, {mouse.y}</p>
            )}
            </Mouse>
          </div>
        );
      }
    }
    

    说实话,我觉得不举这个例子也许不会困惑,举了反倒。。。
    这和普通的 children 有什么区别?先回忆下普通 children 的使用,如下:

    class Header extends React.Component {
      render() {
        return <div className='header'>{this.props.children}</div>
      }
    }
    

    可以看到通常的 children 其实是个 react element(不需要函数调用),但 render props 模式中的 children 是个函数,所以如果你要用这种特殊技巧,最好在 propTypes 里面把 children 声明成一个函数:

    Mouse.propTypes = {
      children: PropTypes.func.isRequired
    };
    

    另外一点就是,官网提到一个坑就是 pureComponent,也是一行注释能说清楚的:

    class MouseTracker extends React.Component {
      render() {
        return (
          <div>
            <h1>Move the mouse around!</h1>
            //  因为每次传进去的render是个新的匿名函数,如果 Mouse 是 PureComponent, 那么只会浅比较
           //   每次新的匿名函数和上次永远不会相等,shouldComponentUpdate 永远为 false  
           //   所以 MouseTracker 每次重新render, Cat 里面会重新执行 this.props.render()
            <Mouse render={mouse => (
              <Cat mouse={mouse} />
            )}/>
          </div>
        );
      }
    }
    

    如果要避免,那当然是把render方法用变量存一下:

    class MouseTracker extends React.Component {
      renderTheCat(mouse) {
        return <Cat mouse={mouse} />;
      }
    
      render() {
        return (
          <div>
            <h1>Move the mouse around!</h1>
            <Mouse render={this.renderTheCat} /> // render 函数不会发生变化哦
          </div>
        );
      }
    }
    

    Props render 模式在 React Router 里面的使用

    render props 这种设计模式在 react-router 4.0 里面使用到了, 之所以要写这个,我是觉得看明白这个,会在你理解 render props 时少走弯路。(贡献 render props 官方文档的和 react-router 的开发者就是同一个人,有兴趣可以查一下,有亮点~)

    <Route /> 组件支持 component 和 render ,children 三种(https://reacttraining.com/react-router/web/api/Route) , render 和 children 都是一个函数,差别不大,我们比较下 component 和 render: component vs. children

    说实话我之前在用的时候一直以为 render 既然是传一个函数,那么就是一个 stateless component 嘛, 而 component 传入就是传一个 stateful component 嘛,真是 too young too simple。首先我们看一个 demo:

    const Home = () => <div>home</div>
    <Route component={Home}/>  // route1
    <Route render={() => <div>home</div>}/> // route2
    
    

    这样写是有差别,但是如果你把 render 里面的传参也指向 Home,route1 和 route2 有区别吗??

    ============= think about it ===========================

    如果你答不上来,我把有关源码以及注释贴一下,你可能就明白了。

    if (component)
      // We already know the differences:
      // React.createElement(component)
      // React.createElement(() => <component/>)
      return match ? React.createElement(component, props) : null
    
    if (render)
      return match ? render(props) : null   
    

    render 并不是一个 component,虽然这个函数看上去很像 stateless component,但是并不是走 React.createElement 组件逻辑,而是单纯的返回一个 element。所以第一个 route 的 render 的 element 的 type 是 Home,而第二个是 div。在这个例子里面看不出什么差别,但是如果我们要传入自己的 props,如果我们这么写:

    <Route component={props => 
          <Home name={this.state.name} {...props} /> }
    />  // route3
    <Route render={props => 
          <Home  name={this.state.name} {...props} />}
    /> // route4
    

    如之前提到的,每次 Route 所在的组件 render 一次,会形成一个新的匿名函数。所以针对 route3, component 传参每次都发生变化,type 也会发生变化。但是 route4 由于只是执行函数返回 Home,render 的 element 的 type 永远是 Home。而 React 虚拟节点比较根据之一就是 type 和之前的 type 是不是一样。所以 route3 里面的 Home 会随着父组件 render 经历 unmount 和 mount 生命周期,而 route4 里面的 Home 不会

    有点绕? 请看代码:https://codesandbox.io/s/m72rolk4mx

    并附上官方解释:


    官网文档

    所以在有些库里面 render props 的入参很像一个 stateless 组件,比如 Formik, 但实际上走的是函数调用的逻辑。从我个人来讲,虽然 render props 灵活性很大,但是有一点隐性。以上就是 render props 的总结吧!

    相关文章

      网友评论

          本文标题:React 之 Render Props 的设计模式

          本文链接:https://www.haomeiwen.com/subject/bitdqqtx.html