從 0 到 1 實現 React 系列 —— 5.PureComponent 實現 && HOC 探幽

本系列文章在實現一個 cpreact 的同時幫助你們理順 React 框架的核心內容(JSX/虛擬DOM/組件/生命週期/diff算法/setState/PureComponent/HOC/...) 項目地址html

PureComponent 精髓

使用 PureComponent 是優化 React 性能的一種經常使用手段,相較於 Component, PureComponent 會在 render 以前自動執行一次 shouldComponentUpdate() 函數,根據返回的 bool 值判斷是否進行 render。其中有個重點是 PureComponent 在 shouldComponentUpdate() 的時候會進行 shallowEqual(淺比較)。node

PureComponent 的淺比較策略以下:react

對 prevState/nextState 以及 prevProps/nextProps 這兩組數據進行淺比較:git

1.對象第一層數據未發生改變,render 方法不會觸發; 2.對象第一層數據發生改變(包括第一層數據引用的改變),render 方法會觸發;github

PureComponent 的實現

照着上述思路咱們來實現 PureComponent 的邏輯算法

function PureComponent(props) {
  this.props = props || {}
  this.state = {}

  isShouldComponentUpdate.call(this) // 爲每一個 PureComponent 綁定 shouldComponentUpdate 方法
}

PureComponent.prototype.setState = function(updater, cb) {
  isShouldComponentUpdate.call(this) // 調用 setState 時,讓 this 指向子類的實例,目的取到子類的 this.state
  asyncRender(updater, this, cb)
}

function isShouldComponentUpdate() {
  const cpState = this.state
  const cpProps = this.props
  this.shouldComponentUpdate = function (nextProps, nextState) {
    if (!shallowEqual(cpState, nextState) || !shallowEqual(cpProps, nextProps)) {
      return true  // 只要 state 或 props 淺比較不等的話,就進行渲染
    } else {
      return false // 淺比較相等的話,不渲染
    }
  }
}

// 淺比較邏輯
const shallowEqual = function(oldState, nextState) {
  const oldKeys = Object.keys(oldState)
  const newKeys = Object.keys(nextState)

  if (oldKeys.length !== newKeys.length) {
    return false
  }

  let flag = true
  for (let i = 0; i < oldKeys.length; i++) {
    if (!nextState.hasOwnProperty(oldKeys[i])) {
      flag = false
      break
    }

    if (nextState[oldKeys[i]] !== oldState[oldKeys[i]]) {
      flag = false
      break
    }
  }

  return flag
}
複製代碼

測試用例

測試用例用 在 React 上提的一個 issue 中的案例,咱們指望點擊增長按鈕後,頁面上顯示的值可以加 1。redux

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

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

  render() {
    return (
      <div> <button onClick={this.click}>增長</button> <div>{this.state.count}</div> </div>
    )
  }
}
複製代碼

然而,咱們點擊上述代碼,頁面上顯示的 0 分絕不動!!!性能優化

揭祕以下:app

click() {
  const t = ++this.state.count
  console.log(t === this.state.count) // true
  this.setState({
    count: t,
  })
}
複製代碼

當點擊增長按鈕,控制檯顯示 t === this.state.count 爲 true, 也就說明了 setState 先後的狀態是統一的,因此 shallowEqual(淺比較) 返回的是 true,導致 shouldComponentUpdate 返回了 false,頁面所以沒有渲染。框架

相似的,以下寫法也是達不到目標的,留給讀者思考了。

click() {
  this.setState({
    count: this.state.count++,
  })
}
複製代碼

那麼如何達到咱們指望的目標呢。揭祕以下:

click() {
  this.setState({
    count: this.state.count + 1
  })
}
複製代碼

感悟:小小的一行代碼裏蘊藏着無數的 bug。

HOC 實踐

高階組件(Higher Order Component) 不屬於 React API 範疇,可是它在 React 中也是一種實用的技術,它能夠將常見任務抽象成一個可重用的部分。這個小節算是番外篇,會結合 cpreact(前文實現的類 react 輪子) 與 HOC 進行相關的實踐。

它能夠用以下公式表示:

y = f(x),

// x:原有組件
// y:高階組件
// f():
複製代碼

f() 的實現有兩種方法,下面進行實踐。

屬性代理(Props Proxy)

這類實現也是裝飾器模式的一種運用,經過裝飾器函數給原來函數賦能。下面例子在裝飾器函數中給被裝飾的組件傳遞了額外的屬性 { a: 1, b: 2 }。

聲明:下文所展現的 demo 均已在 cpreact 測試經過

function ppHOC(WrappedComponent) {
  return class extends Component {

    render() {
      const obj = { a: 1, b: 2 }
      return (
        <WrappedComponent { ...this.props } { ...obj } /> ) } } } @ppHOC class B extends Component { render() { return ( <div> { this.props.a + this.props.b } { /* 輸出 3 */ } </div> ) } } 複製代碼

要是將 { a: 1, b: 2 } 替換成全局共享對象,那麼不就是 react-redux 中的 Connect 了麼?

改進上述 demo,咱們就能夠實現可插拔的受控組件,代碼示意以下:

function ppDecorate(WrappedComponent) {
  return class extends Component {
    constructor() {
      super()
      this.state = {
        value: ''
      }
      this.onChange = this.onChange.bind(this)
    }

    onChange(e) {
      this.setState({
        value: e.target.value
      })
    }

    render() {
      const obj = {
        onChange: this.onChange,
        value: this.state.value,
      }

      return (
        <WrappedComponent { ...this.props } { ...obj } />
      )
    }
  }
}

@ppDecorate
class B extends Component {
  render() {
    return (
      <div>
        <input { ...this.props } />
        <div>{ this.props.value }</div>
      </div>
    )
  }
}
複製代碼

效果以下圖:

這裏有個坑點,當咱們在輸入框輸入字符的時候,並不會立馬觸發 onChange 事件(咱們想要讓事件當即觸發,然而如今要按下回車鍵或者點下鼠標才觸發),在 react 中有個合成事件 的知識點,下篇文章會進行探究。

順帶一提在這個 demo 中彷佛看到了雙向綁定的效果,可是實際中 React 並無雙向綁定的概念,可是咱們能夠運用 HOC 的知識點結合 setState 在 React 表單中實現僞雙向綁定的效果。

繼承反轉(Inheritance Inversion)

繼承反轉的核心是:傳入 HOC 的組件會做爲返回類的父類來使用。而後在 render 中調用 super.render() 來調用父類的 render 方法。

《ES6 繼承與 ES5 繼承的差別》中咱們提到了做爲對象使用的 super 指向父類的實例。

function iiHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      const parentRender = super.render()
      if (parentRender.nodeName === 'span') {
        return (
          <span>繼承反轉</span>
        )
      }
    }
  }
}

@iiHOC
class B extends Component {
  render() {
    return (
      <span>Inheritance Inversion</span>
    )
  }
}
複製代碼

在這個 demo 中,在 HOC 內實現了渲染劫持,頁面上最終顯示以下:

可能會有疑惑,使用屬性代理的方式貌似也能實現渲染劫持呀,可是那樣作沒有繼承反轉這種方式純粹。

鳴謝

Especially thank simple-react for the guidance function of this library. At the meantime,respect for preact and react

相關連接

相關文章
相關標籤/搜索