React 重温之高阶组件(HOC)

2018-06-13 admin

什么是高阶组件

话不多说,先看官方释义:

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

上面这段话,已经很清楚明白的告诉我们高阶组件是什么,以及高阶组件是干啥的。a higher-order component is a function告诉我们说高阶组件是一个函数(function),是一个什么函数呢? takes a component and returns a new component.是一个接收一个组件作为参数,最终返回一个新组件的函数。

所以说,高阶组件并不是一个“组件”,而是一个函数,叫“高阶函数”可能更加合适一些,但高阶函数这个名字被人占用了,高阶函数是以函数为参数,最终返回一个新函数的函数。那为什么又要加高阶组件呢?这个高阶组件具体指的是什么东西呢?

其实,高阶组件指的是函数接收一个组件后,最终返回的那个新组件。因为这个新组件把我们当做参数传入的组件给包裹在内,相对于我们传入的组件来说,这个返回的新的组件就是“高阶组件”了。

干啥这么麻烦

我们都知道,React让我们抽象出一些可复用的组件从而减少前端工作量,一般情况下我们只需要定义一些组件,然后把他们组装成一个组件树就好了,为啥还要弄一个函数来去包裹组件呢?

其实呢,归根结底,都是因为懒。。。因为我们懒得一遍遍写相同的代码,我们把具有相同逻辑的内容抽象成一个组件,一次定义,到处可用;同样因为懒,我们把具有类似功能的组件抽象,用一个新的组件去包裹它,把相同的部分放到包裹组件里,不同的部分放到各自原本组件里,那么这个新的用来包裹我们类似组件的新组件,就是“高阶组件”了。

说到底,我们在业务逻辑的基础上完成一次抽象过程,得到一个个组件;在组件的基础再做一次抽象,得到一个高阶组件(高阶函数)。

Show me the code

闲话少说,让我们来看下官方的示例:

首先是一个CommentList组件,这个组件从外部数据源订阅数据并展示评论列表:

class CommentList extends React.Component {
  constructor() {
    super();
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource" 就是全局的数据源
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // 添加事件处理函数订阅数据
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // 清除事件处理函数
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // 任何时候数据发生改变就更新组件
    this.setState({
      comments: DataSource.getComments()
    });
  }

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

然后是一个BlogPost组件用来展示你的博客文章:

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} />;
  }
}

从这两个组件的代码上来看,我们很容易就可以发现一个问题:他俩长的太像了。。。这不都是监听外部数据源,有变动了就更新自己的state,然后把数据按照各自的逻辑渲染出来嘛。唯一不一样的地方就是每个组件需要的数据和渲染方式不一样。

作为一个以出名的程序员,看到这样的组件,你很可能已经想把他们相同的东西拿出来放到一个地方,只保留各自不同的部分,不然谁知道以后业务逻辑变化了,还有多少类似的组件等着你,难道要把重复的代码到处写吗?Don‘t Repeat Yourself!

OK,如果你这么想了,那就很靠近高阶组件的思想了,下面就是针对上面的组件,官方给出的高阶组件:

function withSubscription(WrappedComponent, selectData) {
  // ……返回另一个新组件……
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ……注意订阅数据……
      DataSource.addChangeListener(this.handleChange);
    }

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

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

    render() {
      // ……使用最新的数据渲染组件
      // 注意此处将已有的props属性传递给原组件
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

我们看到,withSubscription是一个函数,接收WrappedComponent, selectData两个参数,最终返回一个新的组件。在新组件的render()函数里,直接返回了WrappedComponent这个被包裹的组件。在handleChange函数里,使用selectData函数来筛选被包裹组件需要的数据。

我们上面说到,BlogPost和CommentList这两个组件除了需要的数据和渲染数据的方式不同外,其它基本都一样,于是在withSubscription函数里,我们把传入组件原封不动的渲染,在筛选数据的时候,使用传入的selectData函数来筛选,于是withSubscription这个函数就可以很容易的返回一个高阶组件来包裹 需要不同数据和渲染方式 的组件。

使用方式如下:

//首先简化组件定义

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

class BlogPost extends React.Component {
  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}
//去包裹组件

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()//自定义筛选数据
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)//自定义筛选数据
);

以上就是所有代码,我们把原来BlogPost和CommentList组件中重复的代码都放到包裹组件里,只保留各自不同的部分,然后调用高阶组件函数来生成CommentListWithSubscription和BlogPostWithSubscription这两个组件,之后在需要用到BlogPost和CommentList组件的地方都用CommentListWithSubscription和BlogPostWithSubscription来替换就好了。

好像哪里不太对

看完上面的官方示例后,如果你感觉好像哪里不太对,那么恭喜你,你基本上算是一个React高手了

那么到底是哪里不太对呢?细心的朋友可能已经发现了,我们在比较两个被包裹组件的时候提到,两个组件 需要不同数据和渲染方式,渲染方式是每个组件最核心的功能,这个没法变动,可是数据有两个来源啊,为啥非要从state里拿数据?

我们完全可以把数据来源从组件内部的state拿到外部的props里啊,这一样一来同样可以简化组件的代码啊!

然而事情并没有那么简单,我们之前提到,这些组件的数据来自 外部数据源,如果我们把数据来源从state迁移到props,同样需要在使用组件的地方去筛选数据,并没有减少这个工作量,只是把这个工作量从组件内部移到使用组件的地方罢了。。。

注意

不要在render函数中使用高阶组件

React使用的差异算法(称为协调)使用组件标识确定是否更新现有的子对象树或丢掉现有的子树并重新挂载。如果render函数返回的组件和之前render函数返回的组件是相同的,React就递归的比较新子对象树和旧子对象树的差异,并更新旧子对象树。如果他们不相等,就会完全卸载掉旧的之对象树。

在render使用高阶组件,其实就是调用函数生成一个高阶组件,基本每次render都会生成一个新的组件,这个就比较。。。

如果确实需要动态的调用高阶组件,一个比较合理的方式是在组件的构造函数或生命周期函数中调用。

必须将静态方法做拷贝

使用高阶组件包装组件,原始组件被容器组件包裹,也就意味着新组件会丢失原始组件的所有静态方法。

决这个问题的方法就是,将原始组件的所有静态方法全部拷贝给新组件:

Refs属性不能传递

一般来说,高阶组件可以传递所有的props属性给包裹的组件,但是不能传递refs引用。因为并不是像key一样,refs是一个伪属性,React对它进行了特殊处理。如果你向一个由高阶组件创建的组件的元素添加ref应用,那么ref指向的是最外层容器组件实例的,而不是包裹组件。

具体可以参考React 重温之 Refs

参考链接 参考链接

原文链接:https://segmentfault.com/a/1190000015273513

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-33944.html

文章标题:React 重温之高阶组件(HOC)

相关文章
Node.js的不足之处
跨平台编程能力不够强大 Bowery团队指出Go能很方便地在不同系统里进行程序编译,这是他们转入Go的重要原因之一。 作为开发平台,对Linux,Windows,OSX等常见操作系统提供支援是能否吸引开发者的基本要素。在Go中,开发者可以针...
2015-11-12
详解angular2封装material2对话框组件
1. 说明 angular-material2自身文档不详,控件不齐,使用上造成了很大的障碍。这里提供一个方案用于封装我们最常用的alert和confirm组件。 2. 官方使用方法之alert ①编写alert内容组件 @Componen...
2017-03-13
JavaScript正则进阶之路——活学妙用奇淫正则表达式
有些童鞋肯定有所疑惑,花了大量时间学习正则表达式,却发现没有用武之地,正则不就是验证个邮箱嘛,其他地方基本用不上,其实,大部分人都是这种感觉,所以有些人干脆不学,觉得又难又没多大用处。殊不知,想要成为编程大牛,正则表达式必须玩转,GitH...
2017-05-31
ajax教程之ajax使用Http请求
ajax中是如何让使用http请求的呢? 在传统的JS编程中,如果您希望从服务器上的文件或数据库中得到任何的信息,或者向服务器发送信息的话,就必须利用一个 HTML 表单向服务器 GET 或 POST 数据。而用户则需要单击“提交”按钮来发...
2015-11-12
DOM之通俗易懂讲解
DOM 是所有前端开发每天打交道的东西,但是随着 jQuery 等库的出现,大大简化了 DOM 操作,导致大家慢慢的 “遗忘” 了它的本来面貌。不过,要想深入学习前端知识,对 DOM 的了解是不可或缺的,所以本文力图系统的讲解下 DOM 的...
2016-01-13
JS教程之基础
javascript教程之什么是 JavaScript? JavaScript 被设计用来向 HTML 页面添加交互行为。JavaScript 是一种脚本语言(脚本语言是一种轻量级的编程语言)。JavaScript 由数行可执行计算机代码组...
2015-11-12
Ajax教程之Ajax介绍
Ajax 由 HTML、JavaScript™ 技术、DHTML 和 DOM 组成,这一杰出的方法可以将笨拙的 Web 界面转化成交互性的 Ajax 应用程序。本文的作者是一位 Ajax 专家,他演示了这些技术如何协同工作 —— 从总体概述...
2015-11-12
bootstrap table之通用方法( 时间控件,导出,动态下拉框, 表单验证 ,选中与获取信息)代码分享
1.bootstrap-table 单击单行选中 $(&#x27;#gzrwTable&#x27;).on(&#x27;click-row.bs.table&#x27;, function(e, row, $element) { $(&#x...
2017-02-17
数据格式之战:JSON vs XML
在比较JSON和XML之前,我们先来上一堂关于数据格式的简要历史(更准确的说,是关于XML的始祖): 早在1970年,IBM开发了一种叫Generalized Markup Language的标记语言,简称GML,它主要是为脚本语言定义的一...
2016-01-13
Vue 短信验证码组件开发详解
Vue.js(读音 /vjuː/, 类似于 view)是一个构建数据驱动的 web 界面的库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。 Vue.js 自身不是一个全能框架——它只聚焦于视图层。因此...
2017-03-17
回到顶部