Life of xhu

About

揭秘React组件属性之Refs

Jan 03, 2017

  |   #React

当我们开始用React编写前端之后, 虽然dom已经被组件化抽象化, 但是在某些情况下, 我们还是避免不了和dom打交道, 比如下面这个需求:

在页面上添加一个输入框和按键, 点击按键能弹出输入框内容.

实现这个功能的方式有很多种, 比如我们可以改写输入框的onChange事件, 修改state, 然后弹出的时候从state里取值, 不过实现这么点需求, 好像直接从dom里取值就行了, 动用state有点用高射炮打蚊子的感觉.

既然要从dom里直接取值, 作为React应用当然要避免直接写document.getElementById之类, 这时就可以用到今天要讲的Refs.

一个标准的React元素, props里都有一个名为ref的属性, 这个属性最简单的用法, 就是赋给一个字符串, 那么当元素被mount也就是被渲染到真实dom之后, 可以通过组件的refs对象以刚刚的字符串为key来访问这个dom.

那么问题就简单了, 这个组件可以这么写:

class Test extends Component {
  notify () {
    alert(this.refs.input.value);
  }

  render () {
    return (
      <div>
        <input ref = 'input' />
        <button onClick = {this.notify.bind(this)}> Notify </button>
      </div>
    );
  }
}

这样一来, 我们没有任何显示的dom操作, 也实现了文章最开始需要的功能.


嗯, 没错, 这篇文章并不会在这里结束.

上面这个方式虽然已经完成了我们需要的功能, 但是为了这么小的一个功能声明一个类似乎还是太奢侈了点, 而且我们在开头已经说了不用state来解决了, 而且这个notify函数完全可以和组件本身独立出来, 为什么不用函数而不是类, 来实现一个stateless版本呢? 说干就干:

const notify = (input) => {
  alert(input.value);
};

const Test = () => (
  <div>
    <input />
    <button onClick = {notify}> Notify </button>
  </div>
);

好了, 写出来了一个不能用的版本了.

这个版本最关键的一个问题就是, 我们没有能够拿到input的那个真实的dom, 那么我们继续从ref这个属性着手吧, 虽然上面那个可以工作的版本已经使用了这个属性, 而且也是个简单且常见的用法, 但是在官方文档里, 这个属性却并不是这么用的, 我们来看看:

When the ref attribute is used on an HTML element, the ref callback receives the underlying DOM element as its argument.

也就是说, 这个属性还可以被定义成一个参数为真实dom的函数, 至于这个函数里做什么, 就没有要求了, 那么我们完全可以在声明组件的函数里创建一个变量, 在ref函数里把真实dom赋给这个变量, 再用这个变量作为notify函数的参数, 不就完成上面的任务了吗?

那么, 第三版新鲜出炉:

const notify = (input) => {
  alert(input.value);
};

const Test = () => {
  var inputEle;

  return (
    <div>
      <input ref = {node => inputEle = node} />
      <button onClick = {notify.bind(null, inputEle)}> Notify </button>
    </div>
  );
};

一切看上去都很美好, 直到点击这个button:

Uncaught TypeError: Cannot read property 'value' of undefined

好吧, 继续排查错误, 其实很简单, 问题就是出在bind上面, 因为ref函数里的赋值是在元素被渲染到dom之后才执行的, 在这之前inputELe一直没有东西, 而notify函数在组件声明的时候, 就已经通过bind把inputEle放进了闭包本地的作用域里, 之后inputEle的改动并没有对闭包中的变量产生影响, 所以执行的时候才会有问题.

既然知道了问题所在, 那么换一种写法, 保证notify里拿到的是外部inputEle的引用就行了:

const notify = (input) => {
  alert(input.value);
};

const Test = () => {
  var inputEle;

  return (
    <div>
      <input ref = {node => inputEle = node} />
      <button onClick = {() => notify(inputEle)}> Notify </button>
    </div>
  );
};

Done.