前言
我曾見過許多人盲目地使用像 React,Angular 或 Vue 這樣的現代框架。這些框架提供了許多有趣的東西,但通常人們會忽略它們存在的根本原因。
並不是我們所想的以下原因:
- 它們基於組件;
- 它們有強大的社區;
- 它們有很多第三方庫來解決問題;
- 它們有很多第三方組件;
- 它們有瀏覽器擴展工具來幫助調試;
- 它們適合做單頁應用。
最基本的、最根本的、最深刻的原因是:
UI 與狀態同步非常困難
為什麼
假設你在開發一個這樣需求:
用戶可以通過發送郵件來邀請其他用戶。
UI 交互設計如下:
- 輸入框有一個空狀態(帶有提示信息)
- 輸入郵箱後展示相應的 郵箱,每個地址的右側都有一個刪除按鈕。
原型如下:
這個表單是一個包含電子郵件地址和唯一標識符的對象數組。最初它將是空的。輸入郵件回車後,向該數組中添加一項並更新 UI。當用戶點擊刪除時,刪除對應的項並更新 UI。
感受到了嗎?每次更改狀態時,都需要更新 UI。
我聽到你再說,那又怎樣?OK,讓我們看看如何在不用框架的情況下實現它。
原生實現相對複雜的 UI
// html
// js
class AddressList {
constructor(root) {
// state variables
this.state = []
// UI variables
this.root = root
this.form = root.querySelector('form')
this.input = this.form.querySelector('input')
this.help = this.form.querySelector('.help')
this.ul = root.querySelector('ul')
this.items = {} // id -> li element
// event handlers
this.form.addEventListener('submit', e => {
e.preventDefault()
const address = this.input.value
this.input.value = ''
this.addAddress(address)
})
this.ul.addEventListener('click', e => {
const id = e.target.getAttribute('data-delete-id')
if (!id) return // user clicked in something else
this.removeAddress(id)
})
}
addAddress(address) {
// state logic
const id = String(Date.now())
this.state = this.state.concat({ address, id })
// UI logic
this.updateHelp()
const li = document.createElement('li')
const span = document.createElement('span')
const del = document.createElement('a')
span.innerText = address
del.innerText = 'delete'
del.setAttribute('data-delete-id', id)
this.ul.appendChild(li)
li.appendChild(del)
li.appendChild(span)
this.items[id] = li
}
removeAddress(id) {
// state logic
this.state = this.state.filter(item => item.id !== id)
// UI logic
this.updateHelp()
const li = this.items[id]
this.ul.removeChild(li)
}
// utility method
updateHelp() {
if (this.state.length > 0) {
this.help.classList.add('hidden')
} else {
this.help.classList.remove('hidden')
}
}
}
const root = document.getElementById('addressList')
new AddressList(root);
以上代碼很好地說明了使用原生 JavaScript 實現一個相對複雜的 UI 所需的工作量。
在這個例子中,HTML 負責創建靜態頁面,JavaScript 通過 document.createElement 改變 DOM 結構。
這引來了第一個問題:
構建 UI 相關的 JavaScript 代碼比較複雜,而且 UI 構建分為了兩部分。我們本可以用 innerHTML,雖然它有更高的可讀性,但降低了頁面的性能,同時可能存在 CSRF 漏洞。
我們也可以使用模板引擎,但如果是大面積地修改 DOM,會面臨兩個問題:效率不高與需要重新綁定事件處理器。
但這不是最大問題。最大的問題是每當狀態發生改變時都要手動更新 UI。每次狀態更新時,都需要很多代碼來改變 UI。當添加電子郵件地址時,只需要兩行代碼來更新狀態,但要十三行代碼更新 UI。而且我們已經讓 UI 儘可能簡單了!
它不僅難以編寫而且難以推理,更重要的是:它也非常脆弱。
假設我們我們需要實現將列表與服務器同步的功能,我們需要將數據同服務器返回的數據作對比。
我們需要寫大量代碼,使 DOM 更新更加高效。但如果有任何微小的錯誤,視圖將與數據不再同步。
因此,為了保持視圖與狀態同步,我們需要寫大量乏味且脆弱的代碼。
響應式拯救一切
之所以使用框架不是因為社區,不是因為工具,不是因為生態,不是因為第三方庫......
目前為止,框架最大的改進是保證 UI 和數據同步。
只要你清楚框架的使用規則,就可以很愉快的使用他們。
We define the UI in a single shot, not having to write particular UI code in every action, and we always get the same output due to a particular state: the framework automatically updates it after the state changes.
框架是如何工作的呢?
有兩個基本的策略:
- 重新渲染整個組件,如 React。當組件中的狀態發生改變時,在內存中計算出新的 DOM 結構後與已有的 DOM 結構進行對比。實際上,這是非常昂貴的。因而採取虛擬 DOM ,通過對比狀態變化前後虛擬 DOM 的不同,計算出變化後再改變真實 DOM 結構。這個過程稱為調和(reconciliation)。
- 通過觀察者監測變化,如 Angular 和 Vue。應用中狀態的屬性會被監測,當它們發生變化時,相應的 DOM 元素會重新渲染。
Web components 怎麼樣
很多情況,人們會把 React、 Angular 和 Vue 與 Web components 進行對比。這些人顯然不理解這些框架所提供的最大好處:保持 UI 與狀態同步。
Web components 並不提供這種同步機制。它只是提供了一個<template> 標籤。如果你在應用中使用 Web components 時,想保持 UI 與狀態同步,則需要開發者手工完成,或者使用相關庫。/<template>
自己開發一個框架?
如果熱衷於瞭解底層原理,想知道虛擬 DOM 的具體實現。那,為何不試著在不使用框架的情況下,僅使用虛擬 DOM 來重寫原生 UI呢?
這裡是框架的核心,所有組件的基礎類。
我喜歡學習事物的原理 —— 虛擬 DOM 實現。那麼,為什麼我們學習 Virtual DOM 的實現呢?
這是框架的核心,是任何組件的基類。
這裡是重寫後的 AddressList 組件(使用 babel 來支持 JSX )。
現在 UI 是聲明式的,沒有使用任何框架。我們添加新邏輯來改變狀態的同時,不再需要編寫額外的代碼來保持 UI 同步。
結論
- 現代 JavaScript 框架解決的主要問題是保持 UI 與狀態同步。
- 使用原生 JavaScript 編寫複雜、高效而又易於維護的 UI 界面幾乎是不可能的。
- Web components 並沒有提供解決 UI 與狀態同步的方案。
- 使用現有的虛擬 DOM 庫去開發自己的框架並不困難,但不建議。
作者:小生方勤
鏈接:https://juejin.im/post/5d1df53fe51d4510a5033627
來源:掘金 著作權歸作者所有。
閱讀更多 Tingno記前端 的文章