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

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

同步 setState 的問題

而在現有 setState 邏輯實現中,每調用一次 setState 就會執行 render 一次。所以在以下代碼中,每次點擊增長按鈕,由於 click 方法裏調用了 10 次 setState 函數,頁面也會被渲染 10 次。而咱們但願的是每點擊一次增長按鈕只執行 render 函數一次。node

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,其形式以下:react

setState(updater, [callback])

它能接收兩個參數,其中第一個參數 updater 能夠爲對象或者爲函數 ((prevState, props) => stateChange),第二個參數爲回調函數;git

肯定優化思路爲:將屢次 setState 後跟着的值進行淺合併,並藉助事件循環等全部值合併好以後再進行渲染界面。github

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 三種形式,分別以下:api

// 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 的前世此生 羅列了三種寫法的差別,下面對上述例子中的第二種寫法(比較通用)進行實現。promise

首先在 setAttribute 方法內補充上對 ref 的屬性進行特殊處理,框架

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

針對這個例子中 this.myRef.focus() 的 focus 屬性須要異步處理,由於調用 componentDidMount 的時候,界面上還未添加 dom 元素。處理 renderComponent 函數:dom

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 的分享。

相關文章
相關標籤/搜索