從 0 到 1 實現 React 系列 —— 4.setState優化和ref的實現

看原始碼一個痛處是會陷進理不順主幹的困局中,本系列文章在實現一個 (x)react 的同時理順 React 框架的主幹內容(JSX/虛擬DOM/元件/生命週期/diff演算法/setState/ref/...)

同步 setState 的問題

而在現有 setState 邏輯實現中,每呼叫一次 setState 就會執行 render 一次。因此在如下程式碼中,每次點選增加按鈕,因為 click 方法裡呼叫了 10 次 setState 函式,頁面也會被渲染 10 次。而我們希望的是每點選一次增加按鈕只執行 render 函式一次。

export default class B extends Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
    this.click = this.click.bind(this)
  }

  click() {
    for (let i = 0; i < 10; i++) {
      this.setState({ // 在先前的邏輯中,沒呼叫一次 setState 就會 render 一次
        count: ++this.state.count
      })
    }
  }

  render() {
    console.log(this.state.count)
    return (
      <div>
        <button onClick={this.click}>增加</button>
        <div>{this.state.count}</div>
      </div>
    )
  }
}

非同步呼叫 setState

查閱 setState 的 api,其形式如下:

setState(updater, [callback])

它能接收兩個引數,其中第一個引數 updater 可以為物件或者為函式 ((prevState, props) => stateChange),第二個引數為回撥函式;

確定優化思路為:將多次 setState 後跟著的值進行淺合併,並藉助事件迴圈等所有值合併好之後再進行渲染介面。

let componentArr = []

// 非同步渲染
function asyncRender(updater, component, cb) {
  if (componentArr.length === 0) {
    defer(() => render())       // 利用事件迴圈,延遲渲染函式的呼叫
  }

  if (cb) defer(cb)             // 呼叫回撥函式
  if (_.isFunction(updater)) {  // 處理 setState 後跟函式的情況
    updater = updater(component.state, component.props)
  }
  // 淺合併邏輯
  component.state = Object.assign({}, component.state, updater)
  if (componentArr.includes(component)) {
    component.state = Object.assign({}, component.state, updater)
  } else {
    componentArr.push(component)
  }
}

function render() {
  let component
  while (component = componentArr.shift()) {
    renderComponent(component) // rerender
  }
}

// 事件迴圈,關於 promise 的事件迴圈和 setTimeout 的事件迴圈後續會單獨寫篇文章。
const defer = function(fn) {
  return Promise.resolve().then(() => fn())
}

此時,每點選一次增加按鈕 render 函式只執行一次了。

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() {
    super() {
      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>
    )
  }
}

效果如下:

專案地址關於如何 pr

本系列文章拜讀和借鑑了 simple-react,在此特別感謝 Jiulong Hu 的分享。

關鍵詞:component ref gt lt setstate react 實現 state click render

相關推薦:

React Hooks: What’s Going to Happen to My Tests?

Deep In React(五)setState中的黑魔法

React's setState is not asynchronous!

react拾遺

React生命週期以及注意事項

深入理解React

React淺談setState

從 0 到 1 實現 React 系列 —— 4.setState優化和ref的實現

從原始碼看React.PureComponent

React Anti-Pattern: setState Async