在《拋開 Vue、React、JQuery 這類第三方js,我們該怎麼寫程式碼?》文章中提到了我不使用任何第三方js庫來開發專案的經歷。 從零開發專案確實很有挑戰性,開發中碰到了一些比較麻煩的問題,這篇文章就來記錄一下我在封閉DOM操作時碰到的問題以及解決方式。
主流框架與資料繫結
關於DOM操作就不得不提到一個js庫——JQuery。JQuery是成也DOM(強大的選擇器,鏈式操作方式)敗也DOM(資料繫結取代了DOM操作)。 業務程式碼中嵌入大量的DOM操作會帶來一些問題:
1.作用域。DOM操作沒有作用域,也就是說可以被任何程式碼操作,這樣導致變化不可追溯,出現問題難以除錯。雖然shadow DOM具有一定的作用域,但其它程式碼也是可以操作的。 2.效能。頻繁或大量地操作DOM通常容易引起渲染效能問題,原生操作DOM的方式優化起來需要一定的經驗和技巧,所以容易導致不同水平的開發者寫出效能不同的程式碼。 3.耦合度。JavaScript邏輯和DOM操作混合的程式碼耦合性很高,可讀性低且難以測試。
所以封裝DOM操作是必要的,借鑑現有的主流檢視框架思想,可以採用資料繫結。 即建立一個資料模型,通過修改資料物件屬性來操作檢視。 資料繫結的實現形式主要有3種:
髒值檢測
髒值檢測的實現原理是建立一個待檢測佇列,在解析檢視模板的時候,將需要進行繫結的資料模型屬性放入佇列中。代表框架:AngularJS。 在需要檢測的時候遍歷佇列,當屬性發生變化時修改檢視。 那麼什麼時候進行檢測呢? 大致可分為兩類
- 同步操作,比如元件例項化的時候。
- 非同步操作,包括ajax請求、事件監聽、setTimeout、setInterval等。
這種方式缺陷很明顯
- 需要對所有的可能引起資料變化的操作進行封裝,而且在編寫業務程式碼的時候必須使用封裝後的函式。
- 每次檢測會遍歷整個佇列,隨著繫結屬性增多,效能會受到影響。
狀態提交
資料模型修改時(後),呼叫函式來觸發檢視修改。代表框架:React。 這種方式在進行批量操作的時候非常有優勢,這就和SQL資料庫中使用事務來提交批量操作有些類似。 缺陷也比較明顯,就是每次修改資料都要進行提交,程式碼寫起來略嫌麻煩。
資料劫持
資料劫持就監聽資料模型屬性的變動,然後觸發對應的檢視修改。代表框架:Vue。
可以通過Object.defineProperty
或者在不考慮相容的情況下使用Proxy
。
但是在處理陣列資料的時候有一些問題:呼叫陣列函式如push
、pop
等不會觸發屬性監聽事件。
所以需要一些hack手段將這些函式進行封裝。
實現資料繫結
選擇
個人的程式設計習慣比較偏向於“onDemand”,在編寫程式碼的時候的體現為按需編寫和呼叫程式碼,在編譯後的程式碼中喜歡按需載入程式碼。
既然如此,AngularJS那種監聽屬性全部遍歷的粗放做法肯定不是我的首選。
然後手動提交更新的方式一來會增加程式碼來進行提交操作,另一方面也容易忘記提交導致檢視不更新產生bug,所以最後的選擇只剩下資料劫持了。
思路
如果按照Vue的實現過程,需要解析檢視模板,然後建立vdom樹,同時對於需要繫結的資料進行監聽,然後通過操作vdom樹來更新檢視。
鑑於專案本身並不複雜,而且也沒有必要完全照搬其實現思路,所以精簡一下實現思路:
- “解析”檢視模板。
- 對需要繫結的資料進行監聽。
- 在監聽函式中執行對應的DOM操作。
“解析”模板
一般來說“解析”這種操作是會將原有的程式碼或資料進行轉化,比如“詞法解析”就會把原始碼轉化成一個一個的token。
而這裡“解析”模板的目的只是為了識別字串中的需要資料繫結的語法(我們暫且稱之“指令”)。所以可以在例項化之後直接使用選擇器來進行操作。
比如要進行文字屬性的繫結,使用了x-bind
指令,那麼我們可以直接在shadowDOM中進行查詢
this.shadowRoot.querySelectorAll('[x-bind]')
找到這些DOM元素之後,可以通過getAttribute('x-bind')
來獲取需要繫結的屬性。
資料監聽
在建立資料監聽之前我們需要建立一個資料模型,用來和檢視建立對映關係,即當我們修改這個資料模型的時候能同步到檢視上。
假設我們的資料模型變數名為state
。然後通過Object.defineProperty(this.state, 'xxx', ...)
對state變數的指定屬性進行監聽。
這時候需要注意的是,一個屬性可能和多個檢視元素進行繫結,但是我們監聽資料屬性只能編寫一次,所以需要對監聽屬性建立一個佇列,當資料模型資料發生變化時,遍歷佇列中的執行函式並呼叫。
操作DOM
在執行函式中我們傳入其繫結的DOM,然後執行函式根據各個指令的功能來操作DOM了。
比如x-bind
指令的執行邏輯會是這樣:
this.textContent = undefined === value ? '' : value;
當然到這一步還只能算完成了一半,因為只實現了資料 ==> 檢視
的操作,檢視 ==> 資料
還沒有完成。因此我們需要進行事件繫結。
事件繫結是不是也可以用指令的方式呢?比如繫結單擊事件:
<button x-click="click">click me</button>
這樣能滿足一部分業務場景,但是更多的時候我們不僅要觸發事件,而且還要傳入引數。而被傳入的引數有可能是變數名,也有可能是常量。比如:
<button x-click="click(name, true)">click me</button>
name
為資料模型上的屬性名,而true
為一個布林值常量。所以需要對事件繫結進行簡單的語法解析,並在呼叫對應函式的時候傳入正確的引數。
優化
資料繫結
基於上面的實現,還可以將表單元素的事件繫結和資料繫結封裝一下,實現雙向資料繫結,這樣能進一步減少業務程式碼。
假定這個指令的名稱為x-model
。那麼在“解析”模板的時候要編寫一個執行函式來同步DOM的value
值和模型資料屬性。同時建立事件監聽來將資料模型屬性同步到DOM中。
變化檢測
因為資料監聽是在資料被賦值的時候就會觸發,為了減少更新DOM,可以在呼叫DOM更新的執行函式時進行判斷:只有屬性發生變化時才觸發DOM更新。
未實現的功能
- 陣列函式未封裝,所以現在更新陣列只能通過賦值的方式操作。
- 資料繫結不支援表示式等複雜的形式。
相關原始碼:github.com/yalishizhud…
作者資訊:朱德龍,人和未來高階前端工程師。