控制項設計的方案權衡
來自專欄 餓了么前端
1. 雙向綁定
看到這個詞,相信在不少人的腦子裡都會冒出某種現代前端框架吧?但是扯到任何一種現代前端框架恐怕都會引戰,所以我還是喜歡從原生控制項說起。
原生控制項就是以雙向綁定的風格來設計的,比如最典型的 input[type=text]
。當用戶通過 UI 操作來修改其值的時候,它的 value
屬性會同時發生變化;當程序修改 value
屬性的時候,UI 也會發生相應變化。所以整體看起來,value
屬性和 UI 的變化就是雙向綁定在一起的。
我猜肯定會有人發出這樣的疑問:
雙向綁定的概念應該是 view 和 model 之間的綁定才對吧?
model 層應該是一個相對的概念,也許是因為寫多了業務代碼的緣故,在很多人心中 model 的概念都是業務上的。但作為公共控制項的設計者,控制項本身就是全部,所以可以認為 value
屬性就是這個控制項的 model 層。
接下來可能會遇到疑問二連擊:
如果這麼解釋,那所有控制項都是雙向綁定了?
這個問題同樣和看 model 層的角度有關。比如上面例子中的 input[type=text]
,如果你只關心用戶輸入的內容、只關心 value
屬性,那確實是雙向綁定的。但如果你還關心游標的位置呢?游標位置變化同樣屬於 UI 變化,早期的 IE 上並沒有 selectionStart
、selectionEnd
這樣的屬性,所以可以說當時並不是雙向綁定的。甚至早期 select
控制項的 value
都不是雙向綁定的。
現在大部分控制項確實都是雙向綁定了,因為這種設計是最便於理解和使用的。
2. 如何實現?
那麼如何實現雙向綁定的控制項呢?
有兩種方案(方向),我們通過一個例子來分析一下吧。
假如要實現一個計數按鈕控制項(純粹舉例子,沒啥應用場景),就是一個帶數字的按鈕,每次點擊後上面的數字自增,並且把這個數字綁定到 value
屬性上。
方案 1:依賴後代的數據綁定
<script>class Counter { // 從後代獲取值 get value() { return +this.button.textContent; }? // 將值設置到後代上 set value(value) { this.button.textContent = +value; }? constructor() { let button = this.button = document.createElement(button); button.type = button; this.value = 0;? // 用戶操作直接更新到後代的 UI 上 button.addEventListener(click, () => { this.button.textContent = +this.button.textContent + 1; }); }? renderTo(parent) { parent.appendChild(this.button); }}?addEventListener(load, () => { let counter = new Counter(); counter.renderTo(document.body);});</script>
這個方案的問題在於對 UI 的寫操作不止一處,當觸發 UI 變化的事件太多或 UI 變化前需要額外計算時,整個邏輯就會非常混亂。想像一下 input[type=number]
這種控制項,點擊和輸入都可以影響 UI,而且輸入的數據不僅要考慮類型,還要考慮 max
和 min
。
方案 2:單向數據流
<script>class Counter { get value() { return this.$value; } // 所有影響 UI 的操作都從 set value 開始 set value(value) { this.$value = +value; this.button.textContent = this.$value; } constructor() { let button = this.button = document.createElement(button); button.type = button; this.value = 0; // 用戶操作不直接影響 UI,而是反饋到 set value 上 button.addEventListener(click, () => { this.value++; }); } renderTo(parent) { parent.appendChild(this.button); }}?addEventListener(load, () => { let counter = new Counter(); counter.renderTo(document.body);});</script>
這個方案將所有 UI 操作都放在了 set value
中,並且 get value
直接將值返回,什麼也不用做,邏輯變得簡單很多。
但是這個方案也是有坑點的。因為任何一個局部的 UI 變化都要考慮全局的 UI 更新。如果全局的 UI 更新是一個高成本的操作(比如超大的樹形控制項),或者局部 UI 高頻變化(比如大量 input 事件)。那麼這個方案就會導致性能非常差。
3. 總結
曾經有一段時間我很迷信單向數據流,總覺得這樣可以簡化邏輯,降低維護成本。直到強行用單向數據流去設計複雜控制項(當時是要設計一個節點支持文本輸入的樹)遇到無法滿足的場景之後才開始反思這兩種控制項設計方案的適用場景。
這篇文章是想告訴大家,控制項設計的這兩個方案其實是兩個方向的極端,在這兩個極端的中間存在著無數種方案。必要時可以考慮同時使用兩種方案結合。比如在全局使用單向數據流,局部依賴後代的數據綁定加以補償。
推薦閱讀:
※做好了一個很簡單的前端驗證加上後端的跳轉
※React源碼分析 - 生命周期
※你該知道的前端模塊化
※(四)一份友好樣式的緣起與歸宿
※小爝的知乎Live-如何監控性能 & 分析數據
TAG:前端開發 |