ref

在 react 中并不建议使用 ref 属性, 而应该尽量使用状态提升, 但是 react 还是提供了 ref 属性赋予了开发者操作 dom 的能力, react 的 ref 有 stringcallbackcreateRef 三种形式, 分别如下:

// string 这种写法未来会被抛弃
class MyComponent extends Component {
  componentDidMount() {
    this.refs.myRef.focus()
  }
  render() {
    return <input ref="myRef" />
  }
}

// callback(比较通用)
class MyComponent extends Component {
  componentDidMount() {
    this.myRef.focus()
  }
  render() {
    return <input ref={(ele) => {
      this.myRef = ele
    }} />
  }
}

// react 16.3 增加, 其它 react-like 框架还没有同步
class MyComponent extends Component {
  constructor(props) {
    super(props)
    this.myRef = React.createRef()
  }
  componentDidMount() {
    this.myRef.current.focus()
  }
  render() {
    return <input ref={this.myRef} />
  }
}

React ref 的前世今生 罗列了三种写法的差异, 下面对上述例子中的第二种写法(比较通用)进行实现。

首先在 setAttribute 方法内补充上对 ref 的属性进行特殊处理,

function setAttribute(dom, attr, value) {
  ...
  else if (attr === 'ref') {          // 处理 ref 属性
    if (_.isFunction(value)) {
      value(dom)
    }
  }
  ...
}

针对这个例子中 this.myRef.focus() 的 focus 属性需要异步处理, 因为调用 componentDidMount 的时候, 界面上还未添加 dom 元素。处理 renderComponent 函数:

function renderComponent(component) {
  ...
  else if (component && component.componentDidMount) {
    defer(component.componentDidMount.bind(component))
  }
  ...
}

刷新页面, 可以发现 input 框已为选中状态。

处理完普通元素的 ref 后, 再来处理下自定义组件的 ref 的情况。之前默认自定义组件上是没属性的, 现在只要针对自定义组件的 ref 属性做相应处理即可。稍微修改 vdomToDom 函数如下:

function vdomToDom(vdom) {
  if (_.isFunction(vdom.nodeName)) { // 此时是自定义组件
    ...
    for (const attr in vdom.attributes) { // 处理自定义组件的 ref 属性
      if (attr === 'ref' && _.isFunction(vdom.attributes[attr])) {
        vdom.attributes[attr](component)
      }
    }
    ...
  }
  ...
}

跑如下测试用例:

class A extends Component {
  constructor() {
    super()
    this.state = {
      count: 0
    }
    this.click = this.click.bind(this)
  }

  click() {
    this.setState({
      count: ++this.state.count
    })
  }

  render() {
    return <div>{this.state.count}</div>
  }
}

class B extends Component {
  constructor() {
    super()
    this.click = this.click.bind(this)
  }

  click() {
    this.A.click()
  }

  render() {
    return (
      <div>
        <button onClick={this.click}>加1</button>
        <A ref={(e) => { this.A = e }} />
      </div>
    )
  }
}

效果如下:

React.forwardRef 使用场景

React.forwardRef 后面跟一个 render 函数。用法如下:

function HOCComponent(WrapComponent) {
  return class extends React.Component {
    render() {
      const { ref, ...rest } = this.props
      return <WrapComponent ref={ref} { ...rest } />
    }
  }
}

// 此时的 ref 指向的是, 高阶组件里包裹的 WrapComponent 组件。
const Demo = React.forwardRef((props, ref) => {
  return <HOCComponent { ...props } ref={ref} />
})

使用场景: 需要引用高阶组件里的子组件节点的时候可以使用 React.forwordRef