1. 程式人生 > >從 0 到 1 實現 react - 9.onChange 事件以及受控組件

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

sets 兩種 The readonly 顯示 .html 分割線 還需 fire

該系列文章在實現 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 的事件循環機制改變表單的值。

效果如下:

技術分享圖片

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

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