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('鍵盤松開以後還需按下回車鍵或者點下滑鼠才會觸發') })
撥雲見霧
- 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
的事件迴圈機制改變表單的值。
效果如下:
至此,模擬了受控元件的實現。