從 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
的事件循環機制改變表單的值。
效果如下:
至此,模擬了受控組件的實現。
從 0 到 1 實現 react - 9.onChange 事件以及受控組件