Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React高阶组件 #4

Open
xchunzhao opened this issue Mar 29, 2019 · 0 comments
Open

React高阶组件 #4

xchunzhao opened this issue Mar 29, 2019 · 0 comments
Labels

Comments

@xchunzhao
Copy link
Owner

  • 什么是高阶组件?

高阶组件(HOC)是React中的高级技术,用来重用组件逻辑。但高阶组件本身并不是React API。它只是一种模式,这种模式是由React自身的组合性质必然产生的。

关于高阶组件的定义:

a higher-order component is a function that takes a component and returns a new component.

从定义来看,高阶组件是一个函数,函数接收一个组件,返回一个新的组件。

  • 高阶组件应用场景

举一个官方文档上的例子。

假设你有一个CommentList组件,该组件从外部数据源订阅数据并渲染评论列表:

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource" is some global data source
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // Subscribe to changes
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // Clean up listener
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // Update component state whenever the data source changes
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

然后, 你又写了一个订阅单个博客文章的组件,该组件遵循类似的模式:

class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id)
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id)
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}

可以清晰发现,CommentListBlogPost 组件并不相同——他们调用 DataSource 的方法不同,并且他们渲染的输出也不相同。但是,他们有很多实现是相同的:

  • 挂载组件时, 向 DataSource 添加一个改变监听器。
  • 在监听器内, 每当数据源发生改变时,调用 setState
  • 卸载组件时, 移除改变监听器。

设想一下,一个应用中有很多这样的场景,每个组件都要重复做这一系列操作。理想状态下,我们希望能够抽象出这部分逻辑,达到一个复用的作用。

我们可以写一个函数,创建的组件类似于CommonListBlogPost能够订阅 DataSource

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

第一个参数是需要被包裹的组件,第二个参数检索所需要的数据,从给定的DataSource和当前props属性中。

那相应的 withSubscription函数如下:

function withSubscription(WrappedComponent, selectData) {
  // ...and returns another component...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ... that takes care of the subscription...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... and renders the wrapped component with the fresh data!
      // Notice that we pass through any additional props
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}
  • 常见高阶组件的实现方式

    • 属性代理(Props Proxy)

      属性代理是最常见的实现高阶组件的方式。主要有如下几种操作:

      • 操作props

        最直观的就是接受到Props,我们可以做任何的读取、编辑、删除的操作。包括HOC中自定义的事件,我们都可以通过Props传入到被包裹组件中去。

        const propsProxyHoc = WrappedComponent => class extends Component {
          handleClick() {
            console.log('click');
          }
        
          render() {
            return (<WrappedComponent
              {...this.props}
              handleClick={this.handleClick}
            />);
          }
        };
        
        
      • refs获取组件实例(官方不推荐使用)

      • 抽离state
        这边不是使用refs获取state,而是通过{props,callback}传递给wrappedComponent,通过回调函数去获取state。最常用的是React处理表单的时候,一般使用受控组件。即把input做成受控的,在改变value的时候,使用onChange事件同步到state。

        FormCreate.js

        const FormCreate = WrappedComponent => class extends Component {
        
          constructor() {
            super();
            this.state = {
              fields: {},
            }
          }
        
          onChange = key => e => {
            this.setState({
              fields: {
                ...this.state.fields,
                [key]: e.target.value,
              }
            })
          }
        
          handleSubmit = () => {
            console.log(this.state.fields);
          }
        
          getField = fieldName => {
            return {
              onChange: this.onChange(fieldName),
            }
          }
        
          render() {
            const props = {
              ...this.props,
              handleSubmit: this.handleSubmit,
              getField: this.getField,
            }
        
            return (<WrappedComponent
              {...props}
            />);
          }
        };
        
        

        普通组件Login.js

        @FormCreate
        export default class Login extends Component {
          render() {
            return (
              <div>
                <div>
                  <label id="username">
                    账户
                  </label>
                  <input name="username" {...this.props.getField('username')}/>
                </div>
                <div>
                  <label id="password">
                    密码
                  </label>
                  <input name="password" {...this.props.getField('password')}/>
                </div>
                <div onClick={this.props.handleSubmit}>提交</div>
                <div>other content</div>
              </div>
            )
          }
        }
        

        这里我们把state,onChange等方法都放到HOC里,其实是遵从的react组件的一种规范,子组件简单,傻瓜,负责展示,逻辑与操作放到Container。比如说我们在HOC获取到用户名密码之后,再去做其他操作,就方便多了,而state,处理函数放到Form组件里,只会让Form更加笨重,承担了本不属于它的工作。

    • 反向继承(Inheritance Inversion)

      跟属性代理不同的是,反向继承是继承自 WrappedComponent。本来是一种嵌套关系,结果返回的组件却继承 WrappedComponent,达到了一个反转的效果。
      通过继承 WrappedComponent,除了一些静态方法,其他包括生命周期、state以及各种function,都可以得到。

      • 劫持渲染
        在HOC中定义的组件继承了 WrappedComponentrender,我们可以以此来劫持。

        const RenderHoc = config => WrappedComponent => class extends WrappedComponent {
          render() {
            const { style = {} } = config;
            const elementsTree = super.render();
            if (config.type === 'add-style') {
              return <div style={{...style}}>
                {elementsTree}
              </div>;
            }
            return elementsTree;
          }
        };
        
  • 我的应用场景

    • 权限组件

      • 每个组件需要根据用户权限进行判断,用户是否有权限访问 Container的某些功能。那就将判断的逻辑放到HOC中,达到一个逻辑复用的效果。
    • 最近 React-Native项目中发现大部分页面都需要监听请求超时、网络断开并做相关的处理。监听的代码及组件销毁时清除监听的代码都是一致的,唯一不同的是监听的回调不一致。那把监听的逻辑抽象到HOC中,而监听处理的逻辑还在各个组件中。(中间遇到的问题不再赘述。)

  • 高阶组件注意点

    官网比较全面,这边简单做个记录:

    • 高阶组件不会修改子组件,也不拷贝子组件的行为。高阶组件只是通过组合的方式将子组件包装在容器里,是一个无副作用的函数
    • 要给hoc添加class名,便于debugger
    • 静态方法需要手动复制。这里有个解决方法是 hoist-non-react-statics,这个组件会自动把所有绑定在对象上的非React方法都绑定到新的对象上。
    • refs不会传递。
    • 不要再render内部使用高阶组件。简单来说react的差分算法会去比较 NowElement === OldElement, 来决定要不要替换这个elementTree。也就是如果你每次返回的结果都不是一个引用,react以为发生了变化,去更替这个组件会导致之前组件的状态丢失。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant