本文为阅读《深入React技术栈》 (opens new window)React官方文档 (opens new window)、相关技术博客、工作实践,思考、整理而成,阅读书籍请支持正版。书籍非常不错,强烈推荐。如有侵权,请联系删除相关内容。

# React简介

React 是 Facebook 在 2013 年开源在 GitHub 上的 JavaScript 库。React把用户界面抽象成一个个组件,开发者通过组合这些组件,可以得到功能丰富、可交互的页面。

  1. 专注视图层

React专注于提供清晰、简洁的视图层解决方案。

  1. Virtual DOM

在传统开发模式中更新页面需要手动操作DOM,这样不但性能消耗大,代码中充斥着过多的DOM操作也不利于代码的维护。React把真实DOM树转换成了Virtual DOM。当数据发生变化会重新计算Virtual DOM,将新、老VDOM进行对比,对发生变化的部分做批量更新。

  1. 函数式编程

React把重复构建UI的过程抽象成了组件,在给定参数的情况下约定渲染对应的 UI 界面。React 能充分利用很多函数式方法去减少冗余代码。此外,由于它本身就是简单函数,所以易于测试。

React的特点:

  1. 声明式设计 − React采用声明范式,可以轻松描述应用。
  2. 高效 − React通过对DOM的模拟,最大限度地减少与DOM的交互。
  3. 灵活 − React可以与已知的库或框架很好地配合。
  4. JSX − JSX 是 JavaScript 语法的扩展。React 开发不一定使用 JSX ,但我们建议使用它。
  5. 组件 − 通过 React 构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。
  6. 单向响应的数据流 − React 实现了单向响应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单。

无论你现在正在使用什么技术栈,你都可以随时引入 React 来开发新特性,而不需要重写现有代码。React 还可以使用 Node 进行服务器渲染,或使用 React Native 开发原生移动应用。

# JSX语法

JSX是JavaScript语法扩展,用于在React中描述UI呈现。JSX允许我们像写HTML一样编写虚拟DOM。

  1. 在 JSX 中可以嵌入表达式

在JSX中可以嵌入任何表达式。

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

ReactDOM.render(
  element,
  document.getElementById('root')
);
  1. JSX可以用于if 语句和 for 循环的代码块中,可以将 JSX 赋值给变量也可以把 JSX 当作参数传入给函数,以及当做函数返回值返回。
function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;
  }
  return <h1>Hello, Stranger.</h1>;
}
  1. JSX可以给标签定义属性,属性值为字符串时使用双引号包裹,属性值为变量时使用{}包裹,双引号和{}不能混在一起使用。
const element1 = <div tabIndex="0"></div>;
const element2 = <img src={user.avatarUrl}></img>;
  1. JSX中可以包含多个子元素
const element = (
  <div>
    <h1>Hello!</h1>
    <h2>Good to see you here.</h2>
  </div>
);
  1. Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。
const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

Babel编译后

const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

上述代码调用React.createElement()后实际上它创建了一个这样的对象

// 注意:这是简化过的结构
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world!'
  }
};
  1. JSX可以防止XSS攻击。

如下代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <p>请您在输入框中输入您要评论的内容</p>
  <input id="input"/>
  <button id="sendBtn">评论</button>
  <div>
    您评论的内容为:
    <p id="content"></p>
  </div>
</body>
<script>
  sendBtn.onclick = function() {
    content.innerHTML = input.value
  }
</script>
</html>

当我们在输入框中输入如下代码

<img src="" onerror="alert('开始攻击!')" /> 

点击评价按钮,页面会弹出“开始攻击弹窗”,触发恶意代码。

当我们用React模拟相同功能时,则不会弹出”开始攻击弹窗“。这是因为React DOM 在渲染所有输入内容之前,默认会进行转义,所有的内容在渲染之前都被转换成了字符串。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
<script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
<script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>

<div id="example"></div>
<script type="text/babel">
class Evaluation extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
		content:'',
		value: ''
	};
  }
  
  sendEvaluation = () => {
  	this.setState({
		content: this.state.value
	})
  }
  inputVal = (e) => {
  	this.setState({
		value: e.target.value
	})
  }
  render() {
    return (
      <div>
          <p>请您在输入框中输入您要评论的内容</p>
		  <input onBlur={this.inputVal}/>
		  <button onClick={this.sendEvaluation}>发布评论</button>
		  <div>
			您评论的内容为:
			<p id="content">{this.state.content}</p>
		  </div>
      </div>
    );
  }
}

ReactDOM.render(
  <Evaluation />,
  document.getElementById('example')
);
</script>

</body>
</html>

# 编写JSX注意项

  1. 定义标签时,只允许被一个标签包裹。原因是一个标签会被转译成对应的 React.createElement 调用方法,最外层没有被包裹,无法转译成方法调用。
// 报错
const component1 = <span>name</span><span> value</span>

// 正确
const component2 = <div><span>name</span><span> value</span></div>
  1. 标签一定要闭合。
  2. DOM元素首字母小写,组件元素首字母大写。
  3. 注释,JSX最后被转换成JS,因此可以使用JS中添加注释的方法添加注释,在一个组件的子元素位置使用注释要用 {} 包起来。示例代码如下
const App = (
  <Nav>
    {/* 节点注释 */}
    <Person
      /* 多行
         注释 */
      name={window.isLoggedIn ? window.name : ''}
    />
  </Nav>
);
  1. DOM 元素的属性是标准规范属性,但有两个例外——class 和 for,因为在JS中这两个都是关键字,需要做转换
  • class 属性改为 className;
  • for 属性改为 htmlFor。 组件元素的属性是自定义属性,也就是组件所需的参数,在写自定义属性的时候,都由标准写法改为小驼峰写法。
  1. JSX特有的属性
  • 省略 Boolean 属性值会导致 JSX 认为 bool 值设为了 true。
<Checkbox checked={true} /> //等价于
<Checkbox checked />
  • 展开属性
const data = { name: 'foo', value: 'bar' };
const component = <Component {...data} />;
  • 自定义 HTML 属性 在JSX中,往DOM元素传入自定义属性需要使用data-前缀
<div data-attr="xxx">content</div>

自定义标签支持任何属性。

<my-component custom-attr="foo" />
  1. JavaScript 属性表达式

属性值要使用表达式,只要用 {} 替换 "" 即可:

const person = <Person name={window.isLoggedIn ? window.name : ''} />;
// 对应输出
const person = React.createElement(
  Person,
  {name: window.isLoggedIn ? window.name : ''}
);
const content = <Container>{window.isLoggedIn ? <Nav /> : <Login />}</Container>;
// 对应输出
const content = React.createElement(
  Container,
  null,
  window.isLoggedIn ? React.createElement(Nav) : React.createElement(Login)
);
  1. HTML 转义。React 会将所有要显示到 DOM 的字符串转义,防止 XSS。React 提供了 dangerouslySetInnerHTML 属性。正如其名,它的作用就是避免 React 转义字符。
<div dangerouslySetInnerHTML={{__html: 'cc &copy; 2015'}} />

# React组件

# React组件的构建方法

  1. React.createClass(React 16后已废弃)
const Text = React.createClass({
  getDefaultProps() {
    return {
      text: 'hello',
    };
  },

  render() {
    const { text } = this.props;
 
    return (
      <span>{text}</span>
    );
  }
})
  1. ES6 classes
import React, { Component } from 'react';
 
class Text extends Component {
  constructor(props) {
    super(props);
  }
 
  static defaultProps = {
    text: 'hello'
  };
 
  render() {
    const { text } = this.props;

    return (
      <span>{text}</span>
    );
  }
}

React 的所有组件都继承自顶层类 React.Component。它的定义非常简洁,只是初始化了 React.Component 方法,声明了 props、context、refs 等,并在原型上定义了 setState 和 forceUpdate 方法。

  1. 无状态函数
const Text = ({ text }) => <span>{text}</span>

无状态组件没有state和生命周期方法,在合适的情况下我们应该使用无状态组件进行开发。因为无状态组件不像上述两种方法在调用时会创建新实例,它创建时始终保持了一个实例,避免了不必要的检查和内存分配,做到了内部优化。

函数组件和类组件有什么不同呢?详见 函数式组件与类组件有何不同? (opens new window)

# React数据流

React中的数据流是自顶向下单向流动的,也就是从父组件到子组件,state和props是React组件中非常重要的概念

# state

state用于管理组件内部状态,这些状态我们通过setState方法来进行改变,当状态改变的后组件会尝试重新渲染。

下面以一个简单的计数器为例:

import React, { Component } from 'react'

class Count extends Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
  }
  handleClick = () => {
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    return <span onClick={this.handleClick}>{this.state.count}</span>
  }
}

ReactDOM.render(
  <Count/>,
  document.getElementById('app')
);

当我们点击文本时,会触发setState方法并更新state,使得组件重新渲染。

注意:一个生命周期内所有的 setState 方法会合并操作。

# props

props用于父子组件间通信,子组件内不能直接改变props的值,defaultProps静态变量可以定义props的默认值。在 React 中有一个重要且内置的 prop——children,它代表组件的子组件集合。如果顶层组件初始化 props,那么 React 会向下遍历整棵组件树,重新尝试渲染所有相关的子组件。

// ...

class Count extends Component {
  static defaultProps = {
    count: 0
  }
  render() {
    return <span>{this.props.count}</span>
  }
}

ReactDOM.render(
  <Count count={1}/>,
  document.getElementById('example')
);

# React生命周期

React生命周期在React 16进行了较大变更,这里我们以React 16为分界线简要介绍下React的生命周期。

# React 16 前的生命周期

  1. 初始化阶段

constructor用来做一些组件初始化操作,比如初始化state

import React, { Component } from 'react'

class Count extends Component {
  constructor(props) {
    // 调用基类的构造函数并把基类的props注入到子组件
    super(props)
  }
}
  1. 挂载阶段
  • componentWillMount

在组件挂在到DOM前调用且只调用一次

  • render

render函数会插入jsx生成的dom结构,react会生成一份虚拟dom树,在每一次组件更新时,react会通过diff算法比较更新前后的新旧DOM树,找到最小的有差异的DOM节点,并重新渲染

  • componentDidMount

组件挂载到DOM后调用且只调用一次

  1. 更新

如上图在Updation阶段触发更新的有两种情况,一种是组件的props改变了,一种是组件的state改变了。

在React中组件更新一般有两种情况:

(1) 父组件重新render导致子组件更新,如果此时我们并不想子组件也跟着更新,可以使用shouldComponentUpdate方法阻止子组件的更新。

class Count extends Component {
   shouldComponentUpdate(nextProps){
      if(nextProps.count === this.props.count){
        return false // 返回false阻止更新
      }
      return true
    }
    render() {
      return <div>{this.props.count}</div>
    }
}

如果Count组件中没有使用props,可以直接在shouldComponentUpdate返回false,阻止Count组件渲染。

(2) 组件内调用setState,无论state有没有变化都会引起组件的重新渲染,这时可以使用shouldComponentUpdate进行优化,只有state变化后组件才重新渲染。

class Count extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
  }
  shouldComponentUpdate(nextProps, nextState){
    if(nextState.count === this.state.count){ // 如果count的值没有发生变化,组件不重新渲染
      return false
    }
	  return true
  }
  handleClick = () => {
    this.setState({ 
      count: this.state.count
    })
  }

  render() {
    return <span onClick={this.handleClick}>{this.state.count}</span>
  }
}
  • componentWillReceiveProps(nextProps)

如果组件是由父组件更新 props 而更新的,那么在 shouldComponentUpdate 之前会先执行 componentWillReceiveProps 方法。

在组件接收到一个新的 prop (更新后)时被调用,参数nextProps是父组件传给当前组件的新props,这个方法中需要对比nextProps和this.props,将nextProps的state做为当前组件的state,从而重新渲染组件

componentWillReceiveProps (nextProps) {
  nextProps.count !== this.props.count&&this.setState({
      count:nextProps.count
  })
}
  • shouldComponentUpdate(nextProps, nextState)

返回一个布尔值。在组件接收到新的props或者state时被调用,返回true组件继续执行更新操作,返回false组件停止更新操作,可以用来减少组件的不必要渲染,优化性能。在初始化时或者使用forceUpdate时不被调用。

  • componentWillUpdate (nextProps,nextState)

在组件接收到新的props或者state但还没有render时被调用。在初始化时不会被调用。

  • render

render函数会插入jsx生成的dom结构,react会生成一份虚拟dom树,在每一次组件更新时,react会通过diff算法比较更新前后的新旧DOM树,找到最小的有差异的DOM节点,并重新渲染

  • componentDidUpdate(prevProps,prevState)

在组件完成更新后立即调用。在初始化时不会被调用。

  1. 卸载

componentWillUnmount

在组件被卸载前调用,一般用于一些清理工作,如清理定时器、事件绑定等等,以免造成内存泄露。

# React 16.3后的组件生命周期

React16.3之后新增了两个新的生命周期函数。

  • getDerivedStateFromProps 因为在React推出Fiber后之前的某些生命周期方法就不合适了,因为要开启async rendering,在render函数之前的所有函数,都有可能被执行多次。

随着getDerivedStateFromProps的推出,deprecate了一组生命周期API

(1) componentWillReceiveProps

(2)componentWillMount

(3)componentWillUpdate

除了shouldComponentUpdate之外,render之前的所有生命周期函数全灭。

getDerivedStateFromProps是一个静态函数,不能访问this。

static getDerivedStateFromProps(nextProps, prevState) {
  //根据nextProps和prevState计算出预期的状态改变,返回结果会被送给setState
}
  • getSnapshotBeforeUpdate

这个函数在render之后执行,执行的时候DOM元素还没有更新,因此有机会去获取一个DOM信息,计算得到一个snapshot,这个snapshot会作为componentDidUpdate的第三个参数传入。

官方示例:

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // If we have a snapshot value, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    // (snapshot here is the value returned from getSnapshotBeforeUpdate)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}

React16.3生命周期图:

从图中可以看出,在Updating过程中,setState或者forceUpdate触发的更新不会调用getDerivedStateFromProps方法。

React 16.4版本对其进行了修改:

让getDerivedStateFromProps方法无论是Mounting还是Updating,无论是因为什么引起的Updating,全部都会被调用。

# React 与 DOM

react-dom的package提供了可在应用顶层使用的 DOM(DOM-specific)方法,如果有需要,你可以把这些方法用于 React 模型以外的地方。不过一般情况下,大部分组件都不需要使用这个模块。ReactDOM 的关注点在 DOM 上,因此只适用于 Web 端。在 React 组件的开发实现中,我们并不会用到 ReactDOM,只有在顶层组件以及由于 React 模型所限而不得不操作 DOM 的时候,才会使用它。

  1. findDOMNode

语法:ReactDOM.findDOMNode(component)

我们可以通过ReactDOM.findDOMNode方法获取到组件对应的已渲染的真实DOM元素。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class App extends Component {
  componentDidMount() {
    // this 为当前组件的实例
    const dom = ReactDOM.findDOMNode(this);
  }

  render() {}
}

findDOMNode() 不能用在无状态组件上。在大多数情况下,不推荐使用该方法,因为它会破坏组件的抽象结构,严格模式下该方法已弃用。如果在开发中实在想获取真实DOM元素,可以尝试绑定一个 ref 到 DOM 节点上。

  1. render

语法:ReactDOM.render(element, container[, callback])

该方法把element挂载到container,并返回对该组件的引用。如果是无状态组件,render 会返回 null。当组件在初次渲染之后再次更新时,React 不会把整个组件重新渲染一次,而会用它高效的 DOM diff 算法做局部的更新。如果提供了可选的回调函数,该回调将在组件被渲染或更新之后被执行。

ReactDOM.render() 目前会返回对根组件 ReactComponent 实例的引用。 但是,目前应该避免使用返回的引用,因为它是历史遗留下来的内容,而且在未来版本的 React 中,组件渲染在某些情况下可能会是异步的。 如果你真的需要获得对根组件 ReactComponent 实例的引用,那么推荐为根元素添加 callback ref。

使用 ReactDOM.render() 对服务端渲染容器进行 hydrate 操作的方式已经被废弃,并且会在 React 17 被移除。作为替代,请使用 hydrate()。

  1. unmountComponentAtNode

语法:ReactDOM.unmountComponentAtNode(container)

作用:从 DOM 中卸载组件,会将其事件处理器(event handlers)和 state 一并清除。如果指定容器上没有对应已挂载的组件,这个函数什么也不会做。如果组件被移除将会返回 true,如果没有组件可被移除将会返回 false。

  1. createPortal

语法:ReactDOM.createPortal(child, container)

作用:创建 portal。Portal 将提供一种将子节点渲染到 DOM 节点中的方式,该节点存在于 DOM 组件的层次结构之外。

# Refs

在某些情况下如果需要操作真实DOM,React提供了Refs,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

虽然React提供了直接操作DOM的方法,但是不可乱用,我们应该尽量避免使用 refs 来做任何可以通过声明式实现来完成的事情。

下面是几个适合使用 refs 的情况:

  • 管理焦点,文本选择或媒体播放。
  • 触发强制动画。
  • 集成第三方 DOM 库。

在React16.3之前通过字符串或者回调函数的形式获取,在React 16.3中引入了React.createRef API。在React 16.8中引入了React.useRef API。

下面的代码简单列举了四种ref的使用方法:

// string ref
class MyComponent1 extends React.Component {
  componentDidMount() {
    this.refs.myRef.focus();
  }
  render() {
    return <input ref="myRef" />;
  }
}

// callback ref
class MyComponent2 extends React.Component {
  componentDidMount() {
    this.myRef.focus();
  }
  render() {
    return <input ref={(ele) => {
      this.myRef = ele;
    }} />;
  }
}

// React.createRef()
class MyComponent3 extends React.Component {
  constructor(props) {
    super(props);
    // 使用React.createRef()创建ref并绑定到一个实例属性上,以便可以在整个组件中使用
    this.myRef = React.createRef();
  }
  componentDidMount() {
    this.myRef.current.focus();
  }
  render() {
    // 通过ref 属性将创建的Refs添加到 React 元素上
    return <div ref={this.myRef} />;
  }
}

// React.useRef
function MyComponent4() {
  const myRef = React.useRef(null)
  return <span ref={myRef}>hello world</span>
}

React官方文档中已明确提出string ref这种方式会被移除,所以开发时不要再使用这中方式了。我们可能会疑惑为啥string ref这种形式被废弃了呢,从上面简单的例子来看,string ref也挺方便的呀。这是因为:

some issues (opens new window)

String refs are bad in quite a few ways (opens new window)

这个网站需要梯子,这里放下截图

  1. string ref 无法被组合,例如一个第三方库的父组件已经给子组件传递了 ref,那么我们就无法再在子组件上添加 ref 了,而 callback ref 可完美解决此问题。
/** string ref **/
class Parent extends React.Component {
  componentDidMount() {
    // 可获取到 this.refs.childRef
    console.log(this.refs);
  }
  render() {
    return <input ref="childRef"/>
  }
}

class App extends React.Component {
  componentDidMount() {
    // this.refs.child 无法获取到
    console.log(this.refs);
  }
  render() {
    return (
      <Parent>
        <input ref="child" />
      </Parent>
    );
  }
}

/** callback ref **/
class Parent extends React.Component {
  componentDidMount() {
    // 可以获取到 child ref
    console.log('parent', this.childRef);
  }
  render() {
    const { children } = this.props;
    return <input ref={child=>{ this.childRef =child; children.ref && children.ref(child) }}/>
  }
}

class App extends React.Component {
  componentDidMount() {
    // 可以获取到 child ref
    console.log('app', this.child);
  }
  render() {
    return (
      <Parent>
        <input ref={(child) => {
          this.child = child;
        }} />
      </Parent>
    );
  }
}
  1. string ref 不适用于Flow之类的静态分析。 Flow不能猜测框架可以使字符串ref“出现”在react上的神奇效果,以及它的类型(可能有所不同)。 回调引用比静态分析更友好。
  2. 字符串ref的所有者由当前执行的组件确定。这意味着,使用常见的“渲染回调”模式(e.g. <DataTable renderRow={this.renderRow} />),错误的组件将拥有ref(它将在DataTable上结束,而不是你定义的组件renderRow)。
class MyComponent extends Component {
  renderRow = (index) => {
    // string ref 会挂载在 DataTable this 上
    return <input ref={'input-' + index} />;

    // callback ref 会挂载在 MyComponent this 上
    return <input ref={input => this['input-' + index] = input} />;
  }
 
  render() {
    return <DataTable data={this.props.data} renderRow={this.renderRow} />
  }
}
  1. string ref 强制React跟踪当前正在执行的组件。 这是有问题的,因为它使react模块处于有状态,并在捆绑中复制react模块时导致奇怪的错误。在 reconciliation 阶段,React Element 创建和更新的过程中,ref 会被封装为一个闭包函数,等待 commit 阶段被执行,这会对 React 的性能产生一些影响。
function coerceRef(
  returnFiber: Fiber,
  current: Fiber | null,
  element: ReactElement,
) {
  ...
  const stringRef = '' + element.ref;
  // 从 fiber 中得到实例
  let inst = ownerFiber.stateNode;
  
  // ref 闭包函数
  const ref = function(value) {
    const refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
    if (value === null) {
      delete refs[stringRef];
    } else {
      refs[stringRef] = value;
    }
  };
  ref._stringRef = stringRef;
  return ref;
  ...
}
  1. 在根组件上使用无法生效。
ReactDOM.render(<App ref="app" />, document.getElementById('main'));
  1. 对于静态类型较不友好,当使用 string ref 时,必须显式声明 refs 的类型,无法完成自动推导。
  2. 编译器无法将 string ref 与其 refs 上对应的属性进行混淆,而使用 callback ref,可被混淆。
/** string ref,无法混淆 */
this.refs.myRef
<div ref="myRef"></div>

/** callback ref, 可以混淆 */
this.myRef
<div ref={(dom) => { this.myRef = dom; }}></div>

this.r
<div ref={(e) => { this.r = e; }}></div>

我们再来看下createRef 和 callback ref,从语义上来看createRef更直观一些且如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。

ref 回调函数以内联函数的方式定义:

class Count extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
  }

  handleClick = () => {
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    return (
      <span ref={
        ele => {
          this.spanRef = ele
          // ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次
          console.log('执行了')
        }
      }
        onClick={this.handleClick}
      >
        {this.state.count}
      </span>
    )
  }
}

ReactDOM.render(
  <Count />,
  document.getElementById('example')
);

ref 的回调函数定义为class 的绑定函数:

class Count extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
    this.setSpanRef = ele => {
      this.spanRef = ele
      console.log('执行了');
    }
  }

  handleClick = () => {
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    return (
      <span
      // ref 的回调函数定义成 class 的绑定函数时,只执行一次
        ref={this.setSpanRef}
        onClick={this.handleClick}
      >
        {this.state.count}
      </span>
    )
  }
}

ReactDOM.render(
  <Count />,
  document.getElementById('example')
);

上面我们简单列举了下四种创建ref的方式和些许区别。下面我们

# 访问Refs

当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。

const node = this.myRef.current;

默认不能在函数组件上使用ref属性,因为没有实例。

function DangerousTip() {
  return <span ref="3">禁止触碰!!!</span>
}

class App extends React.Component {
  constructor(props) {
    super(props)
    this.myRef = React.createRef()
  }
  render() {
    // Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
    return <DangerousTip ref={this.myRef}/>
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('example')
);

如果非想在函数组件上使用ref可以使用forwardRef

我们可以通过React.useRef()在函数组件内部使用ref属性了只要它指向一个 DOM 元素或 class 组件

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World</title>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

    <!-- Don't use this in production: -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/babel">
      const { useRef } = React
      const { render } = ReactDOM

      function DangerousTip() {
        const myRef = useRef(null)
        const printContent = () => console.log(myRef.current.innerText)
        return <span ref={myRef} onClick={printContent}>禁止触摸!!!</span>
      }

      class App extends React.Component {
        render() {
          return <DangerousTip/>
        }
      }

      render(
        <App/>,
        document.getElementById('root')
      );

    </script>
  </body>
</html>

我们不能在类组件中使用useRef创建ref

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World</title>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

    <!-- Don't use this in production: -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/babel">
      const { useRef } = React
      const { render } = ReactDOM

      class App extends React.Component {
        constructor(props) {
          super(props)
          this.myRef = useRef()
        }
        render() {
          // Invalid hook call. Hooks can only be called inside of the body of a function component.
          return <input ref={this.myRef}/>
        }
      }

      render(
        <App/>,
        document.getElementById('root')
      );

    </script>
  </body>
</html>

# React.forwardRef

在某些情况下可能会希望父组件能引用子组件的DOM节点,这个使用可以使用Refs 转发。Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。

我们可以通过React.forwardRef 来获取传递给它的 ref。下面是一个简单的实例,点击APP组件中的button在控制台打印出input中输出的内容

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello React</title>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

    <!-- Don't use this in production: -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/babel">
      const { useRef, forwardRef, createRef } = React
      const { render } = ReactDOM

      const Input = forwardRef((props, ref)=><input ref={ref} />)

      function App() {
        // const inputRef = useRef(null)
        const inputRef = createRef()
        const print = () => console.log(inputRef.current.value)
        
        return (
          <div>
            评价:<Input ref={inputRef}/>
            <button onClick={print}>提交</button>
          </div>
        )
      }
      render(
        <App/>,
        document.getElementById('root')
      );

    </script>
  </body>
</html>

简要描述下步骤:

  1. 我们通过调用 React.useRef 创建了一个 React ref 并将其赋值给 inputRef 变量。
  2. 我们通过指定 ref 为 JSX 属性,将其向下传递给<Input ref={ref}>
  3. React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数。
  4. 我们向下转发该 ref 参数到<input ref={ref}/>,将其指定为 JSX 属性。
  5. 当 ref 挂载完成,ref.current 将指向<input> DOM 节点。

注意

第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref。

Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。

可能你会问,如果我使用的是低版本的react,不支持React.forwardRef怎么办呢?可以使用 ref 作为特殊名字的 prop 直接传递。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello React</title>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

    <!-- Don't use this in production: -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/babel">
      const { useRef, forwardRef, createRef } = React
      const { render } = ReactDOM

      function Input(props) {
        return <input ref={props.inputRef}/>
      }
      function App() {
        // const inputRef = useRef(null)
        const inputRef = createRef()
        const onSubmit = () => console.log(inputRef.current.value)
        
        return (
          <div>
            评价:<Input inputRef={inputRef}/>
            <button onClick={onSubmit}>提交</button>
          </div>
        )
      }
      render(
        <App/>,
        document.getElementById('root')
      );

    </script>
  </body>
</html>

当然React.forwardRef还可以用于高阶组件中,这里不再赘述,请参考官方文档 Forwarding Refs (opens new window)

# React.createRef vs React.useRef

细心的读者可能会发现在上面代码App中我们分别使用React.createRef和React.useRef两种方式创建了ref,都能实现我们想要的效果。那么它俩有什么区别呢?

  1. useRef不能用在类组件中,如下代码会报错:
class App extends Component {
  constructor(props) {
    super(props)
    // Invalid hook call. Hooks can only be called inside of the body of a function component.
    this.valRef = useRef()
  }
  render() {
    return null
  }
}
  1. useRef除了可以用于ref属性,它还可以保存任何可变值,类似于一个 class 的实例属性,并且可以通过参数传入初始值createRef则不可以传默认值。
function App() {
  const [, setCount] = useState()
  let count = useRef(0)
  const handleClick = () => {
    count.current += 1
    setCount({})
  }
  return <span onClick={handleClick}>{count.current}</span>
}
  1. useRef 会在每次渲染时返回同一个 ref 对象,它将会在函数组件的生命周期中,保持状态不变,除非手动进行修改,createRef创建的值会随着函数组件渲染时重复执行而不断被初始化。

当我们将上例中的useRef换成createRef再点击span标签,控制台输出的是null,这是怎么回事呢?

function App() {
  const [, setCount] = useState()
  let count = createRef()
  const handleClick = () => {
    console.log(count);
    count.current += 1
    setCount({})
  }
  return <span onClick={handleClick}>123{count.current}</span>
}

这是因为App组件是函数组件,每次渲染、更新时都要将App函数从头到尾执行一遍,createRef返回的值每次都是{current: null}。createRef能在类组件中使用是因为类组件中有明确的生命周期钩子函数,组件渲染时不会不会将类中所有的变量、方法都重新执行、初始化。

  1. 当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

参考:

函数式组件与类组件有何不同? (opens new window)

分析React.createRef和React.useRef (opens new window)

React Ref 原来是这样的 (opens new window)

React ref 的前世今生 (opens new window)

React生命周期网址 (opens new window)

React生命周期项目github地址 (opens new window)

详解React生命周期 (opens new window)

React v16.3之后的组件生命周期函数 (opens new window)