問題來源是來自這個React官方存儲庫的issue #3926,與這個議題關聯的有不少其餘的issue,來自許多項目,有些是與React相關,有些則是vue或其它JS套件。也已經有其餘的項目是專一於解決這個問題,例如react-composition,不過它是一個使用ES5語法的React組件。在其餘的討論區上也有相似的問題與解答。本文的目的是但願能針對這個問題提供一些說明、如今暫時性的解決方案。vue
下圖爲目前解決React中"Controlled"(受控制的)input元件的演示,能夠到這裏去測試:react
注意事項: 目前的解決方案我認爲是暫時性的,結果都放在這個github庫上。這要分爲"Controlled"(受控制的)與"Uncontrolled"(不受控制的)兩個種類的組件,影響的主要是input與textarea兩個組件,輸入法(IME, input method editor)的問題,不僅會發生在中文,一樣的在日文、韓文或其它使用輸入法的語言應該都有一樣問題。linux
React組件主要使用onChange
人造事件,做爲文本輸入框(input)或文字輸入區(textarea)觸發文字輸入時的事件,這個事件用起來很直覺,理應當是如此。但onChange
在瀏覽器上,只要在這個文本輸入框上,有任何的鍵盤動做它都會觸發,也就是若是你是使用了中文、日文、韓文輸入法(IME),不管是哪種,拼音的、筆劃的仍是其餘的,只要有按下一個鍵盤的動做,就會觸發一次瀏覽器上這個元素的change
事件,對於本來就使用鍵盤上的英文字符做爲輸入的語言來講,這沒什麼太大的問題,但對於要使用輸入法的語言用戶來講,不停的觸發change
事件,可能會形成程序功能上的運行邏輯問題。git
舉出一個實際的應用狀況,一個使用React撰寫的搜索計算機書籍的功能,用戶能夠在文本輸入框裏輸入要搜索的書名,程序中是利用onChange
事件觸發,進行比對數據庫中的書籍標題,當你想搜索一本名爲"林哥的Java教程",第一個字爲"林",拼音輸入法須要輸入"lin"三個鍵盤上的字符,在"林"這個字從輸入法編輯器中加到真正的input元素前,onChange
已經捕捉到"lin"三個字符,在列表中已搜索出一大堆有關"linux"的書籍。細節就不說了,還有可能對字符數量的的檢查之類的問題。不過,這是正確的程序運做邏輯嗎?很明顯的這是一個大問題。github
固然,你也能夠用對中文字詞檢查的修正方式,或是乾脆不要用change
事件,改用其餘按鈕觸發之類的事件來做這事情,或是不要用React中的"Controlled"(受控制的)input或textare組件,但這會侷限住在程序開發應用上的自由,要如何選擇就看你本身了,是不要使用它仍是想辦法正視問題來解決它。web
這個問題在瀏覽器中,早就已經有了可應對的解決方法,DOM事件中有一組額外的CompositionEvent(組成事件)能夠輔助開發者,它能夠在可編輯的DOM元素上觸發,主要是input與textarea上,因此能夠用來輔助解決change
事件的輸入法問題。CompositionEvent(組成事件)共有三個事件,分別爲compositionstart
、compositionupdate
與compositionend
,它們表明的是開始進行字的組成、刷新與結束,也就是表明開始以輸入法編輯器來組合鍵盤上的英文字符,選字或刷新字的組合,到最後輸出字到真實DOM中的文本輸入框中,實務上每一箇中文字在輸入時,compositionstart
與compositionend
都只會會被觸發一次,而compositionupdate
則是有可能屢次觸發。chrome
藉由CompositionEvent的輔助來解決的方式,也就是說在網頁上的input元素,能夠利用CompositionEvent做爲一個信號,若是正在使用IME輸入中文時,change
事件中的代碼就先不要運行,等compositionend
觸發時,接着的change
事件才能夠運行其中的代碼,運做的原理就是這樣簡單而已。數據庫
在React應用中,若是是一個"Uncontrolled"(不受控制的)的input組件,它與網頁上真實DOM中的input元素的事件行爲無差別,也就是說,直接使用CompositionEvent的解決方式,就能夠解決這個輸入法的問題,如下面的代碼爲例子:api
// @flow import React from 'react' const Cinput = (props: Object) => { // record if is on Composition let isOnComposition: boolean = false const handleComposition = (e: KeyboardEvent) => { if (e.type === 'compositionend') { // composition is end isOnComposition = false } else { // in composition isOnComposition = true } } const handleChange = (e: KeyboardEvent) => { // only when onComposition===false to fire onChange if (e.target instanceof HTMLInputElement && !isOnComposition) { props.onChange(e) } } return ( <input {...props} onCompositionStart={handleComposition} onCompositionUpdate={handleComposition} onCompositionEnd={handleComposition} onChange={handleChange} /> ) } export default Cinput
上面這是一個典型的"Uncontrolled"(不受控制的)input組件,主要是它不用value
這個屬性。但若是它有來自上層組件的value
屬性與值,也就是上層組件用props傳遞給它value
屬性的值,就成了"Controlled"(受控制的)組件,它的事件整個模式就會與網頁上的真實DOM中的input元素不同,這後面再說明。瀏覽器
這個解決方案在幾乎全部能支持CompositionEvent的瀏覽器(IE9以上)均可以運行得很好,不過在Google Chrome瀏覽器在2016年的版本53以後,更動了change
與compositionend
的觸發順序,因此須要針對Chrome瀏覽器調整一下,若是是在Chrome瀏覽器中觸發compositionend
時,也要運行一次在本來在change
要運行的代碼,就改爲這樣而已。下面在上個代碼中的handleComposition
函數中,多加了偵測是否爲Chrome瀏覽器,與觸發本來的onChange方法代碼,修改過的代碼以下:
// detect it is Chrome browser? const isChrome = !!window.chrome && !!window.chrome.webstore const handleComposition = (e: KeyboardEvent) => { if (e.type === 'compositionend') { // composition is end isOnComposition = false // fixed for Chrome v53+ and detect all Chrome // https://chromium.googlesource.com/chromium/src/ // +/afce9d93e76f2ff81baaa088a4ea25f67d1a76b3%5E%21/ if (e.target instanceof HTMLInputElement && !isOnComposition && isChrome) { // fire onChange props.onChange(e) } } else { // in composition isOnComposition = true } }
"Uncontrolled"(不受控制的)input或textarea組件,解決方式就是這麼簡單而已,利用CompositionEvent過濾掉沒必要要的change
事件。
注: 其它的解決方式還有,像InputEvent中有一個
isComposing
屬性,它也能夠做爲偵測目前是否正在進行輸入法的組字工做,但InputEvent事件目前只有Firefox中能夠用,看起來沒什麼前景。另外,W3C新提出的IME API或許是一個將來較佳的解決方案,但目前只有IE11 有實做,其餘瀏覽器品牌都沒有。
在React應用中,使用"Controlled"(受控制的)的input或textarea組件是另外一回事,它會開始複雜起來。
"Controlled"(受控制的)的組件並非只有加上value
這個屬性這麼簡單,input或textarea組件所呈現的值,主要會來自state,state有多是上層組件的,利用props一層層傳遞過來的,或是這個組件中自己就有的state,直接賦給在這個組件中的render中的input或textarea組件。也就是說,input最後呈現的文字若是要進行改變,就須要改變到組件(不論在何處)的state,要改變state只有透過setState方法,而setState方法有多是個異步(延時)運行的狀況。
把這整個流程串接在一塊兒後,我相信事件觸發的不連續狀況會變得很嚴重,須要對不一樣狀況下做測試與評估。目前我所做的測試還只是最基本的組件運用而已,複雜的組件狀況尚未開始進行。由於state有不少種用途,有時候內部使用,有時候要對外部用戶輸入介面的事件,或是有時候要對服務器端的數據接收或傳送,不管是不是要使用Redux、MobX或Flux之類的state容器函數庫或框架,最終要進行從新渲染的工做,仍是得調用React中的setState方法才行。
在基本的測試時,我發現"Controlled"(受控制的)的input組件,它不只事件觸發不連續的狀況嚴重,並且有可能在不一樣瀏覽器上會有不一樣的結果。徹底不會有問題的只有一個瀏覽器,就是上面註釋中所說的已經實做出IME API的IE11,IE11上可能根本不須要任何解決方案,它的輸入法編輯器是獨立於瀏覽器上的文本輸入框以外的。
目前已測試的結果是有三種狀況,"Chrome, Opera, IE, Edge"爲一種,"Firefox"爲一種,"Safari"爲一種。我爲這三種狀況分別寫了不一樣的解決方式的代碼,但這個事件觸發的不連續狀況,如今沒法有一致性的解決方案,我只能推測這大概多是React內部設計的問題。
不管是三種的那一種解決方案,有一個重點是你不能像上面的通常性解決方案,阻擋change
事件時要運行的代碼,也就是阻擋setState
變更state
值,由於只要一經阻擋,input
組件的value
值就賦不到值,並且也不會觸發從新渲染。因此你只能讓change
事件不斷觸發,就像往常同樣。
那麼要如何解決程序邏輯運做的問題?
我使用了另外一個內部的state對象中的值,稱爲innerValue
,它是對比在input組件上不斷因觸發change
事件而輸入的值,稱爲inputValue
。innerValue
是個會通過CompositionEvent修正過的值,因此它永遠不會帶有在輸入法組字過程的字符串值。
這個解決方案,是一個"掛羊頭賣狗肉"的用法,不論用戶在input組件如何輸入,輸入的過程都會改變inputValue
而已,inputValue
是一個暫存與呈現用的值,最終用來進行程序邏輯運算的是innerValue
。以最一開始的例子來講,用戶輸入"林哥的Java教程",在一開始的"林"字輸入時,inputValue
是從"lin"到輸入完成變爲"林",而innerValue
是在輸入期間是空字符串值,輸入完成纔會變爲"林"。因此,搜索功能能夠用innerValue
來做爲運算的依據,用這個值來搜索對應的數據,這纔是正確的運算邏輯,由於innerValue
纔是真正的不帶輸入法組字過程的值。
大體上說明一下解決方式的代碼,首先它有兩個在這個模塊做用域中的全局變量,一個用來記錄是否在輸入法的組字過程當中,另外一個是給專給Safari瀏覽器用的:
// if now is in composition session let isOnComposition = false // for safari use only, innervalue can't setState when compositionend occurred let isInnerChangeFromOnChange = false
在專門處理change
事件的handleChange
方法中,判斷isInnerChangeFromOnChange
這一段是專門爲了解決Safari瀏覽器的問題所寫,Safari瀏覽器的行爲是CompositionEvent在觸發時,其中的event.target.value
竟然是組字過程當中的英文字符,而不是觸發這個事件的input元素的全部字符串,這也是特別怪異的地方,因此纔會利用在compositionend
後會再觸發一次change
的特性,在這裏刷新innerValue
。
後面的代碼,是表明在輸入法的組字過程當中,setState方法使用的差別,在組字過程當中(isOnComposition === true
)的話,只會更動inputValue
值,而不會更動到innerValue
的值,這對應了上述所說的一個運做過程,通常的輸入鍵盤上的字符時不會有輸入法的問題,則是兩個值一併更動。代碼以下:
handleChange = (e: Event) => { // console.log('change type ', e.type, ', target ', e.target, ', target.value ', e.target.value) // Flow check if (!(e.target instanceof HTMLInputElement)) return if (isInnerChangeFromOnChange) { this.setState({ inputValue: e.target.value, innerValue: e.target.value }) isInnerChangeFromOnChange = false return } // when is on composition, change inputValue only // when not in composition change inputValue and innerValue both if (!isOnComposition) { this.setState({ inputValue: e.target.value, innerValue: e.target.value, }) } else { this.setState({ inputValue: e.target.value }) } }
在專門處理composition
事件的handleComposition
方法中,主要是爲了在compositionend
觸發時,進行刷新innerValue
所撰寫的一些代碼。在第一種狀況時,也就是在Chrome, IE, Edge, Opera瀏覽器時,只須要直接用e.target.value
刷新innerValue
便可。在第二種狀況是Firefox,它不知道爲何會掉值,因此還須要幫它再一併刷新innerValue
一次。第三種狀況,上面有說過了,特別的怪異狀況,因此對innerValue
的刷新改到compositionend
以後的那個change
事件去做了。代碼以下:
handleComposition = (e: Event) => { // console.log('type ', e.type, ', target ', e.target, ',target.value ', e.target.value, ', data', e.data) // Flow check if (!(e.target instanceof HTMLInputElement)) return if (e.type === 'compositionend') { // Chrome is ok for only setState innerValue // Opera, IE and Edge is like Chrome if (isChrome || isIE || isEdge || isOpera) { this.setState({ innerValue: e.target.value }) } // Firefox need to setState inputValue again... if (isFirefox) { this.setState({ innerValue: e.target.value, inputValue: e.target.value }) } // Safari think e.target.value in composition event is keyboard char, // but it will fire another change after compositionend if (isSafari) { // do change in the next change event isInnerChangeFromOnChange = true } isOnComposition = false } else { isOnComposition = true } }
注: 目前這個暫時的解決方式,其方式並非參考自react-composition項目,解決方式雖然有些相似,但react-composition用的是ES5的React工廠樣式組件語法,我對這種語法並不熟悉。在寫這篇文檔時,才仔細看了一下react-composition的代碼,只能說它的做者實際上也有測試過這個問題,也知道只有用另外一個state中的值才能解決這問題。
若是你是使用"Uncontrolled"(不受控制的)的組件,那麼解決方法很簡單,就如同上面所說的,像通常的網頁上的DOM元素的解決方式便可。
但對於"Controlled"(受控制的)的組件來講,目前的解決方案是一種try-and-error(試誤法)的暫時性解決方案,我目前只能按照已測試的平臺與瀏覽器去修正,沒測過的瀏覽器與平臺,就不得而知了。
關於這個"Controlled"(受控制的)的組件的事件觸發,目前看到有在不一樣瀏覽器上的事件觸發不連續狀況,我也有發一個議題(Issue)給React官方。或許比較好的治本方案,是須要從state更動方式的內部代碼,或是人造事件觸發的順序,進行一些調整,這超出個人能力範圍,就有待開發團隊的迴應了。
最後,若是你正好有須要到這個功能,或是你認爲這個功能有須要,你能夠幫忙測試看看或是提供一些建議。我已經把全部的代碼、演示、線上測試、解決方案都集中到這個Github庫的react-compositionevent中。或許你如今須要一個解決方案,你能夠用裏面目前的暫時性解決方式試試也能夠。