從 0 到 1 實現 react - 9.onChange 事件以及受控組件

該系列文章在實現 cpreact 的同時理順 React 框架的核心內容

項目地址

從一個疑問點開始

接上一章 HOC 探索 拋出的問題 ———— react 中的 onChange 事件和原生 DOM 事件中的 onchange 表現不一致,舉例說明如下:

// React 中的 onChange 事件
class App extends Component {
  constructor(props) {
    super(props)
    this.onChange = this.onChange.bind(this)
  }

  onChange(e) {
    console.log('鍵盤松開立刻執行')
  }

  render() {
    return (
      <input onChange={this.onChange} />
    )
  }
}

/*--------------分割線---------------*/

// 原生 DOM 事件中的 onchange 事件:<input id='test'>
document.getElementById('test').addEventListener('change', (e) => {
  console.log('鍵盤松開以後還需按下回車鍵或者點下鼠標纔會觸發')
})

撥雲見霧

我們來看下 React 的一個 issue React Fire: Modernizing React DOM。有兩點信息和這篇文章的話題相關。

  • Drastically simplify the event system
  • Migrate from onChange to onInput and don’t polyfill it for uncontrolled components

從這兩點內容我們可以得知下面的信息:

React 實現了一套合成事件機制,也就是它的事件機制和原生事件間會有不同。比如它目前 onChange 事件其實對應着原生事件中的 input 事件。在這個 issue 中明確了未來會使用 onInput 事件替代 onChange 事件,並且會大幅度地簡化合成事件。

有了以上信息後,我們對 onChange 事件(將來的 onInput 事件)的代碼作如下更改:

function setAttribute(dom, attr, value) {
  ...
  if (attr.match(/on\w+/)) {        // 處理事件的屬性:
    let eventName = attr.toLowerCase().substr(2)
    if (eventName === 'change') { eventName = 'input' } // 和現階段的 react 統一
    dom.addEventListener(eventName, value)
  }
  ...
}

自由組件以及受控組件

區分自由組件以及受控組件在於表單的值是否由 value 這個屬性控制,比較如下代碼:

const case1 = () => <input />                    // 此時輸入框內可以隨意增減任意值
const case2 = () => <input defaultValue={123} /> // 此時輸入框內顯示 123,能隨意增減值
const case3 = () => <input value={123} />        // 此時輸入框內顯示 123,並且不能隨意增減值

case3 的情形即爲簡化版的受控組件。

受控組件的實現

題目可以換個問法:當 input 的傳入屬性爲 value 時(且沒有 onChange 屬性),如何禁用用戶的輸入事件的同時又能獲取焦點?

首先想到了 html 自帶屬性 readonly、disable,它們都能禁止用戶的輸入,但是它們不能滿足獲取焦點這個條件。結合前文 onChange 的實現是監聽 input 事件,代碼分爲以下兩種情況:

1.dom 節點包含 value 屬性、onChange 屬性
2.dom 節點包含 value 屬性,不包含 onChange 屬性

代碼如下:

function vdomToDom(vdom) {
  ...
  if (vdom.attributes
    && vdom.attributes.hasOwnProperty('onChange')
    && vdom.attributes.hasOwnProperty('value')) { // 受控組件邏輯
      ...
      dom.addEventListener('input', (e) => {
        changeCb.call(this, e)
        dom.value = oldValue
      })
      ...
    }
  if (vdom.attributes
    && !vdom.attributes.hasOwnProperty('onChange')
    && vdom.attributes.hasOwnProperty('value')) { // 受控組件邏輯
    ...
    dom.addEventListener('input', (e) => {
      dom.value = oldValue
    })
    ...
  }
  ...
}

可以發現它們的核心都在這段代碼上:

dom.addEventListener('input', (e) => {
  changeCb.call(this, e)
  dom.value = oldValue
})

區別是當有 onChange 屬性 時,能提供相應的回調函數 changeCb 通過事件循環機制改變表單的值。看如下兩個例子的比較:

const App = () => <input value={123} />

效果如下:

class App extends Component {
  constructor() {
    super()
    this.state = { num: 123 }
    this.change = this.change.bind(this)
  }

  change(e) {
    this.setState({
      num: e.target.value
    })
  }

  render() {
    return (
      <div>
        <input value={this.state.num} onChange={this.change} />
      </div>
    )
  }
}

這段代碼中的 change 函數即上個段落所謂的 changeCb 函數,通過 setState 的事件循環機制改變表單的值。

效果如下:

至此,模擬了受控組件的實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章