返回 登录
0

React常用的六大实用技巧

阅读10031

本文节选自李晋华撰写的《React前端技术与工程实践》一书,由电子工业出版社出版。
作者:李晋华,信息系统架构师和技术顾问。多年从事军事物流信息系统研发工作和相关教学工作。在后勤信息化领域承担多项重点项目的研发工作。曾获军队科技进步奖二等奖。在系统架构设计、系统集成和前端交互设计等方面具有丰富的实战经验。
责编:陈秋歌,寻求报道或者投稿请发邮件至chenqg#csdn.net,或加微信:Rachel_qg。
了解更多前沿技术资讯,获取深度技术文章推荐,请关注CSDN研发频道微博

在实际前端开发中,我们往往还会遇到一些具有共性的问题,针对其中的很多问题业界也提供相应的解决方案或工具。本文围绕实际工程问题列出了一些常用的技巧和工具,便于读者引用。

绑定React未提供的事件

由于React的事件是面向虚拟DOM的,因此,有些真实的浏览器DOM事件是React未提供的。如果要在React中响应这样的事件,需要在componentDidMount中获取对应的真实DOM元素绑定事件,并在componentWillUnmount中取消事件绑定。这种技巧也常用于React与jQuery结合使用时。参见下面的示例:

var Comp = React.createClass({
  getInitialState: function() {
    return {windowWidth: window.innerWidth};
  },

  onWindowResize: function(e) {
    this.setState({windowWidth: window.innerWidth});
  },

  componentDidMount: function() {
    window.addEventListener('resize', this. onWindowResize);
  },

  componentWillUnmount: function() {
    window.removeEventListener('resize', this. onWindowResize);
  },

  render: function() {
    return <div>当前window宽度: {this.state.windowWidth}</div>;
  }
});

React.render(<Comp />, document.body);

通过AJAX加载初始数据

通过AJAX加载数据是一个很普遍的场景。在React组件中如何通过AJAX请求来加载数据呢?首先,AJAX请求的源URL应该通过props传入;其次,最好在componentDidMount函数中加载数据。加载成功,将数据存储在state中后,通过调用setState来触发渲染更新界面。

注意:

AJAX通常是一个异步请求,也就是说,即使componentDidMount函数调用完毕,数据也不会马上就获得,浏览器会在数据完全到达后才调用AJAX中所设定的回调函数,有时间差。因此当响应数据、更新state前,需要先通过this.isMounted() 来检测组件的状态是否已经mounted。

下面是利用GitHub网站提供的API接口获取某个用户近况信息的例子。

var UserGist = React.createClass({
  getInitialState: function() {
    return {
      username: '',
      lastGistUrl: ''
    };
  },

  componentDidMount: function() {
    $.get(this.props.source, function(result) {
      var lastGist = result[0];
      if (this.isMounted()) {
        this.setState({
          username: lastGist.owner.login,
          lastGistUrl: lastGist.html_url
        });
      }
    }.bind(this));
  },

  render: function() {
    return (
      <div>
        {this.state.username}'s last gist is
        <a href={this.state.lastGistUrl}>here</a>.
      </div>
    );
  }
});

React.render(
  <UserGist source="https://api.github.com/users/octocat/gists" />,
  mountNode
);

使用jQuery库所提供的ajax请求$.ajax函数数据也存在一些问题,如兼容性问题就很令人头疼。React推荐使用fetch库,其在API接口层面和jQuery类似,读者可以自行搜索相关资料,熟悉 $.ajax可以很快上手。

使用ref属性

在React中,ref属性是特定用途的属性,将ref属性绑定到渲染函数中输出的任何组件上,就可以获得对应的组件实例,通过这个实例可以获得对应的实际DOM元素。

ref字符串属性

React通常在组件上使用一个字符串识别符来作为ref属性。在渲染函数中给要渲染的子组件增加ref属性,如:

<input ref="myInput" />

在其他地方,如事件处理代码中,通过this.refs访问真正的组件。

var input = this.refs.myInput;
var inputValue = input.value;

完整示例:

  var MyComp = React.createClass({
    getInitialState: function() {
      return {userInput: ''};
    },
    handleChange: function(e) {
      this.setState({userInput: e.target.value});
    },
    clearAndFocusInput: function() {
      // 清空输入的数据
      this.setState({userInput: ''}, function() {
        // 此处获得真实DOM并获得焦点
        this.refs.theInput.getDOMNode().focus();
      });
    },
    render: function() {
      return (
        <div>
          <div onClick={this.clearAndFocusInput}>
            单击获得焦点并清空
          </div>
          <input
            ref="theInput"
            value={this.state.userInput}
            onChange={this.handleChange}
          />
        </div>
      );
    }
  });

在这个例子中,MyComp组件的渲染函数渲染一个组件,组件的实例通过this.refs.theInput获取,这个过程由React自动填充。值得注意的是:ref属性对于React组件获得的是组件实例,而对于HTML获得的则是对应的真实浏览器DOM元素。对于前者,我们可以直接调用组件在类定义中公开的任何成员函数。

对于复合组件来说,这个引用会指向一个组件类的实例,进而可以调用任何该类定义的方法。如果需要访问该组件类对应的实际DOM节点,可以用ReactDOM.findDOMNode来找到实际的节点。不过并不推荐这种做法,因为这样做绝大多数情况下都会打破封装性,并增加了对真实浏览器DOM的依赖,一般都能找到更清晰的模式,使只在React模型中编写代码就能达到同样的效果。

ref回调函数属性

React组件的ref属性也可以是一个回调函数,并且这个回调函数会在组件被挂载后立刻被执行。回调函数的参数对应组件实例的引用,这个回调函数可以立即使用组件,或者保存这个引用便于以后使用。上面的例子使用ref回调函数改写之后如下:

var MyComp = React.createClass({
    getInitialState: function() {
      return {userInput: ''};
    },
    handleChange: function(e) {
      this.setState({userInput: e.target.value});
    },
    clearAndFocusInput: function() {
      // 清空输入的数据
      this.setState({userInput: ''}, function() {
        // 此处获得真实DOM并获得焦点
        this.theInput.focus();
      });
    },
    render: function() {
      return (
        <div>
          <div onClick={this.clearAndFocusInput}>
            单击获得焦点并清空
          </div>
          <input
            ref={(compInstance)=>this.theInput=compInstance}
            value={this.state.userInput}
            onChange={this.handleChange}
          />
        </div>
      );
    }
  });

一旦引用的组件被卸载,或者ref属性本身发生了变化,原有的ref会再次被调用,此时参数为null,这样可以防止内存泄露。如果和上面的例子一样使用内联的函数表达式,那么React每次更新(即调用渲染函数)时,都会得到不同的函数对象,之后每次更新时ref回调函数都会被调用两次:前一次参数是null,后一次是具体的组件实例。

注意:

  • 不要在任何组件的渲染函数或者被渲染函数调用的过程中访问refs。
  • 在Google Closure Compiler的高级模式下,不能访问用字符串形式声明的动态属性,即如果定义ref=’myRefString’,则必须以this.refs[‘myRefString’]的形式来访问引用。
  • 对于stateless function来说,引用不会被加载,因为无状态组件并没有对应的实例。可以使用标准的复合组件来包装一个无状态组件,以在其上附加引用。

使用classNames.js

classNames介绍

在实际应用中,经常会遇到根据某些状态增加或更改组件属性中类名的情况,例如Bootstrap中的动态行为往往是由不同的class来控制,这就经常需要对class进行修改。看看下面的这段代码:

var classString = 'content';
if (this.state.isBgRed) {
    classString = classString + ' bg-red';
} else {
    classString = classString + ' bg-green';
}

这里实现了根据isBgRed状态切换不同class项的效果,但这样的代码语义不太清晰,也不容易维护,一旦状态控制变量变多,这样的代码将会变成一团乱麻。为了更好地满足这种类似的class动态切换的需求,可以使用classNames工具。针对这个问题以前还出现过classSets工具,功能与classNames相似,现已废弃不用。

使用classNames工具首先要导入这个模块:

import classNames from 'classnames'

然后就可正常使用 classNames()函数了。前面的代码用 classNames 工具重写如下:

let isBgRed = this.state.isBgRed;
let classes = classNames ({' bg-red ':isBgRed, ' bg-green ': ! isBgRed });

classNames用法

classNames()函数可以接受任意数量的参数,并将参数连接成一个class字符串,参数可以是一个字符串或者一个对象。如果参数是对象,则对象的属性名(属性值为false、0、null或undefine的属性除外)也会加入到结果class字符串中。看下面的示例:

classNames('foo', 'bar'); // 结果为“foo bar”
classNames('foo', { bar: true }); //结果为“foo bar”
classNames({ 'foo-bar': true }); // 结果为“foo-bar”
classNames({ 'foo-bar': false }); //结果为空
classNames({ foo: true }, { bar: true }); //结果为“foo bar”
classNames({ foo: true, bar: true }); // 结果为“foo bar”

// 多个不同类型的参数示例 
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // 结果为“foo bar baz quux”

// 忽略参数的示例 
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // 结果为“bar 1”

如果参数是数组,则数组中的元素也被当作参数进行处理:

var arr = ['b', { c: true, d: false }];
classNames('a', arr); // 结果为“a b c”

在ES 6中使用动态的classNames

ES 6为JavaScript新标准语法,如果在工程中用到了ES 6语法(需要使用Babel进行转译),那么也可以结合ES 6中的动态属性特性来使用classNames。如下面的例子:

let buttonType = 'primary';
classNames({ [`btn-${buttonType}`]: true });

多类名去重

当多个参数进行合并时,可能会遇到类名有重复的情况,这就需要进行去重处理。classNames模块中的dedupe模块包含这方面的功能。dedupe是classNames的增强版本,但性能要差些,因此,如非确实有去重需求,没必要使用它。这里只对其进行简要介绍。

要使用dedupe,首先需要引入模块:

var classNames = require('classnames/dedupe');

或者在html文件中加入:

<script src="js/dedupe.js" type="text/JavaScript"></script>

使用方法与classnames一样,只是额外增加了自动去重的功能:

classNames('foo', 'foo', 'bar'); // 结果为“foo bar”
classNames('foo', { foo: false, bar: true }); // 结果为“bar”

使用Immutable.js

Immutable.js介绍

JavaScript中的对象一般是可变的(Mutable),作为参数传递时使用的是引用。当声明一个对象的引用变量时,该引用变量直接指向原对象,如:

var foo={a: 1};
var bar=foo;    //此时bar与foo为同一对象
bar.b = 2;      //此时foo.b也为2

其实这样的语义是不清晰的,可能我们只是想复制一个与foo一样的对象,结果却改变了原对象。尽管这样的共享变量机制可以节约内存,但在复杂场景下却往往是造成各种怪异问题的根源。Pete Hunt说,共享的可变状态是罪恶之源(Shared mutable state is the root of all evil)。一般使用对象复制来避开这个问题,但马上就会遇到是浅复制还是深复制的选择,把问题复杂化了。使用同样来自名门facebook公司的Immutable模块来管理这样的数据是更好的选择。

使用Immutable来管理的数据对象具有只能创建不能更改的特性。对Immutable数据对象的任何修改、添加、删除操作都会返回一个新的Immutable对象,同时原对象依然可用且不变,这称为持久化数据结构(Persistent Data Structure)。为了避免深度复制,Immutable使用了被称为结构共享(Structural Sharing)的策略,如果对象树中只有一个节点发生了变化,则只修改受到影响的父节点对它的引用,其他节点则与原对象共享,从而避免了深度复制带来的内存开销。

基于这样的策略,Immutable还具有很多优秀的特性,比如支持回撤、并发安全、与函数式编程天然一致等,可以说Immutable是对数据管理的革新,其将产生更广泛的影响。如React就提倡把this.state当作只可创建不能更改的Immutable,Redux也推荐搭配使用Immutable来管理state数据等。

对于Immutable的具体用法,官网上有详细的文档。限于篇幅,本书主要列举了Immutable一些常用的、实用的用法,我们需要重点关注的是Immutable提供的持久化List、Map等用法,部分代码引自官方网站(http://facebook.github.io/ immutable-js/)。

Immutable基本用法

1.创建Immutable对象

import Immutable from 'immutable';
// 支持数据嵌套的创建方式
var imtArray = Immutable.fromJS([1, {2,3}]) //根据数组创建
var imtObj = Immutable.fromJS({a: 1, b:[2,3]}) //根据JavaScript对象创建
// 不支持数据嵌套的创建方式
var imtList = Immutable.List([1,2]) //根据数组创建,支持数据
var imtMap = Immutable.Map({a: 1}) //根据JavaScript对象创建

2.从Immutable对象中提取JavaScript对象

var jsObject = immutableData.toJS(); // 提取JavaScript对象
var jsArray = imtArray.toArray(); // 提取数组对象

Immutable对象比较

Immutable对象的比较有两种方式,一种是引用比较,一种是值比较。

1.引用比较

引用比较用来识别两个对象是不是同一个对象。使用操作符===进行比较,由于比较的是内存地址,所以速度很快。

let map1 = Immutable.Map({a:1, b:2, c:3});
let map2 = Immutable.Map({a:1, b:2, c:3});
map1 === map2;             // 结果为false

因为只比较内存地址,即使两个不同的对象内容一样也被认为是不相等的。如果需要进行内容上的比较,可以使用值比较的方式。

2.值比较

值比较用于判断两个数据对象的值是否相等,使用Immutable.is()函数进行比较:

Immutable.is(immutableObjectA, immutableObjectB);

Immutable.is()函数实质上比较的是两个对象的 hashCode 或 valueOf(对于JavaScript对象)。由于Immutable使用持久化数据结构存储对象,只要两个对象的hashCode值相等,两个对象的值就是一样的,有效地避开了对对象值的深度遍历,非常高效。

Immutable List用法

(1)创建Immutable List。

Immutable.List(); //生成空的Immutable List对象
Immutable.List([1,2]); //生成Immutable数组对象,不支持嵌套
Immutable.fromJS([1,{2,3}]); //生成Immutable数组对象

(2)查看List的大小。

immutableA.size
immutableA.count()

(3)判断是否是List。

Immutable.List.isList(x);

(4)在React组件中判断propTypes是否是List的写法。

React.PropTypes.instanceOf(Immutable.List).isRequired

(5)获取List索引的元素。

immutableData.get(0);
immutableData.get(-1); //当索引值为负数时为反向索引

(6)访问嵌套数组中的数据。

var imtNestedData = Imutable.fromJS({a: {b: {c:3}}});
imtNestedData.getIn(['a', 'b', 'c']); //结果为3

(7)更新List,其实就是根据原来的List对象创建一个新的List对象。

immutableA = Immutable.fromJS([0, 0, [1, 2]]);
immutableB = immutableA.set(1, 1);
immutableC = immutableB.update(1, (x) -> x + 1);
immutableC = immutableB.updateIn([2, 1], (x) -> x + 1);

(8)针对List的排序方法有 sort 和 sortBy。

immutableData.sort(function(a, b){
  if a < b then return -1;
  if a > b then return 1;
  return 0;
});
immutableData.sortBy((x) -> x);

(9)遍历。

immutableData.forEach(function(a, b){
  // 此处进行遍历操作
  return true; //如果返回值为false则终止遍历
});

(10)检索List中的元素。

immutableData.find((x) -> x > 1);  // 返回第一个匹配的元素
immutableData.filter((x) -> x > 1);  // 返回匹配的元素的数组
immutableData.filterNot((x) -> x <= 1); // 返回不匹配的所有元素的数组

Immutable Map用法

(1)创建Immutable Map。

Immutable.Map();        // 创建空的Immutable Map
Immutable.Map({a: 1});   // 根据对象创建Immutable Map
Immutable.fromJS({a: 1}); // 根据对象创建Immutable Map

(2)判断Map,写法和List类似。

Immutable.Map.isMap(obj);

(3)获取Map中的数据。

immutableData.get('a');

通过getIn访问嵌套的Map中属性a值对象中b属性的值。

immutableData.getIn(['a', 'b']);

(4)更新对象。

immutableB = immutableA.set('a', 1);
immutableB = immutableA.setIn(['a', 'b'], 1);
immutableB = immutableA.update('a', (x) -> x + 1);
immutableB = immutableA.updateIn(['a', 'b'], (x) -> x + 1);

合并对象。

immutableB = immutableA.merge(immutableC);

(5)Map的检索,与List相似。

data = Immutable.fromJS({a: 1, b: 2});
data.filter((value, key) -> value is 1);

判断属性是否存在要先转换为JavaScript的原生对象,再判断:

immutableData = Immutable.fromJS({key: null});
immutableData.has('key');

(6)分别获取key的数组和value的数组。

immutableData.keySeq();
immutableData.valueSeq();

与jQuery集成

毋庸置疑,jQuery仍然是当前主流的Java工具库。尽管React很优秀,但它毕竟是新生事物,和庞大的jQuery生态资源相比,React就显得很不足了。即使使用React,我们也会因为各种各样的原因,再将它和jQuery结合到一起使用。如何延续jQuery的资源又结合React的技术呢?围绕这个问题,我们首先来总结一下React与jQuery的区别。

React与jQuery的区别

  1. DOM操作方式不同:jQuery主要操作的是实际DOM元素,React操作的是虚拟DOM。React是数据驱动的,我们也很少需要操作DOM,只需要关注数据的变化即可。另外,jQuery中有专门针对不同浏览器的处理方案,React则不再需要了,React中包含了由虚拟DOM到真实DOM的转换模块,因此React不再需要考虑兼容性问题。
  2. 元素选取方式有所不同:jQuery通过ID、class等选择器选择元素。而在React中,因为有虚拟DOM,所以jQuery的选取方式不再有效,React通常使用ref方式来获取元素。
  3. 渲染方式不同:jQuery将更新DOM的职责交给用户,用户可以整体更新也可以局部更新,取决于具体实现。而React考虑的是整体更新机制,在实际更新的时候更新的是局部。
  4. 事件处理方式不同:jQuery实现了自己的一套事件处理逻辑,React也有自己的一套事件处理系统。但总体来说,两者都是在原生JavaScript的基础上进行封装,而且都类似于原接口。

基于以上考虑,我们很难不做改动地将jQuery组件变为React组件。因此,我们重点考虑如何在jQuery中使用React,以及如何在React中使用jQuery。

在React中使用jQuery

要在React中使用jQuery的功能,首先要获得组件所对应的真实浏览器DOM元素,这点通过ref属性可以实现;其次要注意jQuery事件系统与React的不同,重点需要关注componentDidMount和componentWillUnmount函数,并在这两个函数内适配jQuery的生命周期。以下代码片段引自React官方示例。

var BootstrapModal = React.createClass({
  componentDidMount: function() {
    var rootElem = this.refs.root; //获得真实的浏览器DOM节点
    // 调用jQuery方法,将节点内容转换为模态对话框
    $(rootElem).modal({backdrop: 'static', keyboard: false, show: false});
    // 绑定jQuery事件,注意到root组件并没有在React中绑定事件
    $( rootElem).on('hidden.bs.modal', this.handleHidden);
  },
  componentWillUnmount: function() {
    $(this.refs.root).off('hidden.bs.modal', this.handleHidden);
  },
  close: function() {
    $(this.refs.root).modal('hide');
  },
  open: function() {
    $(this.refs.root).modal('show');
  },
  render: function() {
    var confirmButton = null;
    var cancelButton = null;

    if (this.props.confirm) {
      confirmButton = (
        <BootstrapButton
          onClick={this.handleConfirm}
          className="btn-primary">
          {this.props.confirm}
        </BootstrapButton>
      );
    }
    if (this.props.cancel) {
      cancelButton = (
        <BootstrapButton onClick={this.handleCancel} className= "btn-default">
          {this.props.cancel}
        </BootstrapButton>
      );
    }
    return (
      <div className="modal fade" ref="root">
        <div className="modal-dialog">
          <div className="modal-content">
            <div className="modal-header">
              <button
                type="button"
                className="close"
                onClick={this.handleCancel}>
                &times;
              </button>
              <h3>{this.props.title}</h3>
            </div>
            <div className="modal-body">
              {this.props.children}
            </div>
            <div className="modal-footer">
              {cancelButton}
              {confirmButton}
            </div>
          </div>
        </div>
      </div>
    );
  },
  handleCancel: function() {
    if (this.props.onCancel) {
      this.props.onCancel();
    }
  },
  handleConfirm: function() {
    if (this.props.onConfirm) {
      this.props.onConfirm();
    }
  },
  handleHidden: function() {
    if (this.props.onHidden) {
      this.props.onHidden();
    }
  }
});

在jQuery中使用React

在jQuery中使用React与在HTML中使用React没有什么不同,我们只要声明一个HTML标签(通常是div)作为React组件的容器,在初始化阶段调用ReactDOM.render()函数将组件挂接到该标签下即可。如下面的例子:

ReactDOM.render(
   React.createElement(HelloComponent, null),
   document.getElementById('reactContainer')
);

点击订购:《React前端技术与工程实践》

图片描述

欢迎加入“CSDN前端开发者”群,与更多专家、技术同行进行热点、难点技术交流。请扫描以下二维码加群主微信,申请入群,务必注明「姓名+公司+职位」
图片描述

评论