一髓堪、背景介紹
目前前端項(xiàng)目開(kāi)發(fā)普遍會(huì)基于react踩娘、vue等框架,采用數(shù)據(jù)驅(qū)動(dòng)的模式钞护,通過(guò)虛擬dom減少與真是dom的交互來(lái)提高渲染性能盖喷。使用react、vue等框架開(kāi)發(fā)就需要按照它們的要求定義組件难咕、調(diào)用組件课梳、傳遞數(shù)據(jù),轉(zhuǎn)變之前的開(kāi)發(fā)思路步藕,這些框架非常完善惦界,但也具有很高約束性。對(duì)于一些沒(méi)有使用react咙冗、vue等框架開(kāi)發(fā)的項(xiàng)目沾歪,若它們自身也有比較完善的組件定義、組件調(diào)用雾消、數(shù)據(jù)傳遞的方式灾搏,且框架基本上也能滿足使用。對(duì)于這樣的項(xiàng)目若想采用數(shù)據(jù)驅(qū)動(dòng)和虛擬dom渲染立润,是將原有框架全部否定狂窑,花很多的時(shí)間使用react、vue等框進(jìn)行重構(gòu)桑腮?其實(shí)只需把存在用戶交互的組件調(diào)整為數(shù)據(jù)驅(qū)動(dòng)和虛擬dom渲染即可泉哈。那選擇混合react、vue等框架只重構(gòu)部分UI組件?這樣會(huì)導(dǎo)致在項(xiàng)目中維護(hù)兩種不同的組件定義丛晦、調(diào)用方式奕纫。本文將介紹如何基于snabbDom.js和jsx,在不改變?cè)蚣艿慕M件定義烫沙、組件調(diào)用匹层、數(shù)據(jù)傳遞的前提下,靈活的加入數(shù)據(jù)驅(qū)動(dòng)的思想锌蓄,并通過(guò)虛擬dom和diff算法簡(jiǎn)化dom操作優(yōu)化組件的渲染機(jī)制升筏。
二、snabbdom的使用
snabbdom是一個(gè)輕量級(jí)的虛擬dom框架瘸爽,具有高效的diff算法及靈活的擴(kuò)展性您访,vue就融合了snabbDom并在它的基礎(chǔ)上進(jìn)行了優(yōu)化與擴(kuò)展。
1蝶糯、創(chuàng)建Vnode
snabbDom的h方法用于創(chuàng)建Vnode對(duì)象洋只,Vnode的屬性可以用來(lái)描述真實(shí)的dom節(jié)點(diǎn),而虛擬dom就是一棵以 Vnode (virtual node)對(duì)象為基礎(chǔ)的樹(shù)昼捍。h方法接受三個(gè)參數(shù):元素標(biāo)簽 / 選擇器识虚、屬性對(duì)象、子節(jié)點(diǎn)數(shù)組:
由h方法創(chuàng)建的Vnode對(duì)象包含的屬性有:sel元素選擇器妒茬、data屬性對(duì)象担锤、children子節(jié)點(diǎn)數(shù)組、text文本內(nèi)容乍钻、elm真實(shí)dom的指針肛循、key標(biāo)識(shí),具體格式如下:
2银择、對(duì)比Vnode
Vnode是真實(shí)dom在內(nèi)存中的描述多糠,要實(shí)現(xiàn)數(shù)據(jù)改變時(shí)視圖更新,需要使用diff算法對(duì)新浩考、老Vnode進(jìn)行對(duì)比夹孔,將變動(dòng)映射到真實(shí)dom上才行。目前主流的diff算法思路上大體一致析孽,都是在深度優(yōu)先遍歷DFS算法基礎(chǔ)上搭伤,結(jié)合了真實(shí)dom的特性,在對(duì)比虛擬dom樹(shù)時(shí)增加了同級(jí)對(duì)比袜瞬、同類別對(duì)比怜俐、列表元素結(jié)合key標(biāo)識(shí)進(jìn)行對(duì)比的優(yōu)化,復(fù)雜度為O(n)具有很高的性能邓尤,而snabbdom在此基礎(chǔ)上拍鲤,又增加了列表元素的兩端對(duì)比贴谎,對(duì)比算法更為高效。
snabbdom的diff是通過(guò)patch方法來(lái)實(shí)現(xiàn)的殿漠,patch方法可以對(duì)比新赴精、老Vnode,把變動(dòng)的Vnode映射到真實(shí)dom上绞幌,也可以把Vnode掛載到html上,并替換指定的dom節(jié)點(diǎn)一忱。patch方法的調(diào)用方式如下:
3莲蜘、兼容ie低版本
使用snabbdom的項(xiàng)目代碼,想要在ie瀏覽器低版本中正常運(yùn)行帘营,需要額外做一些處理票渠,本文只介紹部分:
(1)在snabbdom的樣式處理模塊中,使用了dom元素的classList屬性芬迄,可以將classList的調(diào)用更改為className的字符串處理问顷。
(2)在snabbdom的自定義數(shù)據(jù)屬性處理模塊中,使用了dataset禀梳,可以更改為調(diào)用setAttribute和removeAttribute杜窄。
(3)在snabbdom的事件處理模塊中,綁定事件時(shí)使用了addEventListener算途,ie9以下需要使用attachEvent綁定事件塞耕。
三、jsx
在代碼中直接調(diào)用snabbdom的h方法來(lái)描述dom結(jié)構(gòu)嘴瓤,需要多層嵌套調(diào)用可讀性差不好維護(hù)扫外,我們可以像react一樣,在代碼中使用jsx描述dom結(jié)構(gòu)廓脆,然后使用轉(zhuǎn)譯工具將jsx轉(zhuǎn)換成h方法的調(diào)用筛谚。jsx是一個(gè)看起來(lái)很像XML的JavaScript語(yǔ)法擴(kuò)展,使用jsx表示dom的結(jié)構(gòu)與層次就像使用html的標(biāo)簽語(yǔ)法那樣清晰停忿,jsx的出現(xiàn)讓“ALL-in-JS”的思想得到進(jìn)一步實(shí)現(xiàn)驾讲。
1、jsx的轉(zhuǎn)換原理
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? jsx轉(zhuǎn)換為h方法調(diào)用的過(guò)程
編譯工具(如:Babel瞎嬉、JSTransform)先將jsx代碼解析轉(zhuǎn)換為AST(抽象語(yǔ)法樹(shù))蝎毡,在通過(guò)轉(zhuǎn)換插件(如plugin-transform-react-jsx、jsx-transform)對(duì)AST進(jìn)行遍歷氧枣,查找類型為JSXElement或者JSXFragment的節(jié)點(diǎn)沐兵,
轉(zhuǎn)換插件會(huì)將該類型節(jié)點(diǎn)替換為h方法對(duì)應(yīng)的內(nèi)容,等AST中所有的JSXElement或者JSXFragment節(jié)點(diǎn)轉(zhuǎn)換完成后便监,轉(zhuǎn)換插件會(huì)將處理后的AST編譯為js代碼輸出扎谎,此時(shí)jsx代碼就被轉(zhuǎn)換為js代碼中的h方法的調(diào)用了:
2蜕该、jsx的轉(zhuǎn)換配置
介紹兩種jsx轉(zhuǎn)換為h方法調(diào)用的方式及配置:
(1)在打包代碼之前,通過(guò)node腳本調(diào)用jsx-transform將jsx轉(zhuǎn)換為js代碼督暂,然后在打包轉(zhuǎn)換后的js蛮位。而jsx-transform是一個(gè)將jsx脫離于react,可以被使用于其他非react模塊中的可配置轉(zhuǎn)換工具预吆,jsx-transform的factory屬性可以用于設(shè)置要轉(zhuǎn)換成的方法名龙填,具體node腳本實(shí)現(xiàn)如下:
(2)使用webpack進(jìn)行打包構(gòu)建的項(xiàng)目,可以在webapck配置文件中設(shè)置Babel的plugin-transform-react-jsx插件拐叉,將pragma選項(xiàng)設(shè)置為你想要的函數(shù)名岩遗,即可實(shí)現(xiàn)jsx的轉(zhuǎn)換。
3凤瘦、不同的轉(zhuǎn)換插件帶來(lái)的差異
不同轉(zhuǎn)換插件轉(zhuǎn)換jsx代碼時(shí)宿礁,jsx的書(shū)寫格式會(huì)有一些差異,比如:使用plugin-transform-react-jsx和jsx-transform蔬芥,使用jsx描述嵌套多個(gè)子元素時(shí)梆靖,jsx的書(shū)寫格式就不太一樣。
使用不同的轉(zhuǎn)換插件除了jsx的書(shū)寫格式有所差異外笔诵,轉(zhuǎn)換為h方法的調(diào)用時(shí)返吻,傳參類型可能會(huì)與形參類型不同,比如列表類型的子元素使用jsx-transform轉(zhuǎn)換時(shí)嗤放,傳遞給h方法的children參數(shù)會(huì)被轉(zhuǎn)換為二維數(shù)組思喊,此時(shí)就需要增加兼容處理,讓h方法兼容children為二維數(shù)組的情況次酌。
在文中介紹的兩種轉(zhuǎn)換方式中恨课,jsx的書(shū)寫格式和轉(zhuǎn)換后的方法參數(shù)格式都細(xì)微需要兼容處理的部分。如果想要按照自己的風(fēng)格開(kāi)發(fā)jsx岳服,并讓jsx轉(zhuǎn)換成你想要的方法調(diào)用和傳參格式剂公,可以自己開(kāi)發(fā)一個(gè)轉(zhuǎn)換插件,插件的開(kāi)發(fā)可以參考Babel plugins 和 jsx PrimaryExpression吊宋。
4纲辽、支持自定義組件的jsx寫法?
snabbdom目前只支持將html的標(biāo)簽元素如:div璃搜、span拖吼、svg等標(biāo)簽用Vnode進(jìn)行描述并更新到頁(yè)面,如果想像react中那樣能夠使用jsx語(yǔ)法表示自定義組件和組件的嵌套關(guān)系这吻。自定義組件的jsx寫法:
為了保證統(tǒng)一的定義吊档、調(diào)用方式,組件的整體結(jié)構(gòu)設(shè)計(jì)沒(méi)有改動(dòng)唾糯,只是將組件中對(duì)dom結(jié)構(gòu)的描述方式做了調(diào)整怠硼,由原本的js字符串拼接html標(biāo)簽調(diào)整為jsx語(yǔ)法來(lái)表示鬼贱,snabbdom中的基本標(biāo)簽類型就能夠滿足我們的需求,所以本文目前沒(méi)有支持自定義組件的jsx寫法香璃。若想實(shí)現(xiàn)這部分功能可以在h方法的基礎(chǔ)上進(jìn)行擴(kuò)展这难,使snabbdom的h方法能夠處理sel為自定義組件類型的情況,比如增加自定義組件類型的判斷葡秒、自定義組件Vnode格式的定義與創(chuàng)建姻乓、自定義組實(shí)例的創(chuàng)建、描述組件間嵌套關(guān)系的數(shù)據(jù)結(jié)構(gòu)的處理等等眯牧,實(shí)現(xiàn)思路可以參考reactjs源碼分析這篇文章糖权。
四、在業(yè)務(wù)中的應(yīng)用與實(shí)踐
1炸站、原項(xiàng)目框架特點(diǎn)
項(xiàng)目采用了MVC的開(kāi)發(fā)模式,其中Model為一個(gè)配置文件疚顷,設(shè)置每個(gè)組件的狀態(tài)數(shù)據(jù)旱易,比如組件類型、默認(rèn)顯示隱藏腿堤、正則規(guī)則阀坏,并控制組件的嵌套關(guān)系。View對(duì)應(yīng)的就是UI組件部分笆檀。Controller控制模塊負(fù)責(zé)循環(huán)遍歷配置文件忌堂,根據(jù)配置文件中的組件狀態(tài)數(shù)據(jù)、嵌套關(guān)系酗洒,創(chuàng)建組件實(shí)例士修,控制組件的渲染,對(duì)內(nèi)存中組件對(duì)象進(jìn)行維護(hù)管理樱衷。除此外還有base組件棋嘲、車輛信息校驗(yàn)?zāi)K,其中UI組件都繼承自base組件矩桂。項(xiàng)目框架如下:
2沸移、為引入數(shù)據(jù)驅(qū)動(dòng)和虛擬dom所做的調(diào)整
只有UI組件部分才存在用戶交互,所以我們只給UI組件增加數(shù)據(jù)驅(qū)動(dòng)和虛擬dom侄榴,其他層級(jí)的組件不進(jìn)行改動(dòng)雹锣,降低改動(dòng)成本。而且我們并不打算一次性把二手車發(fā)布pc端項(xiàng)目中所有的UI組件進(jìn)行優(yōu)化癞蚕,而是隨著業(yè)務(wù)需求將涉及到的組件進(jìn)行調(diào)整優(yōu)化蕊爵。
UI組件繼承了base組件,統(tǒng)一定義了用于存儲(chǔ)狀態(tài)數(shù)據(jù)的opts對(duì)象涣达、拼接dom字符串createElem方法在辆、將jq對(duì)象渲染到頁(yè)面上的render方法证薇。
為支持虛擬dom需要做如下的調(diào)整:
(1)將UI組件的createElem方法調(diào)整為使用jsx描述dom結(jié)構(gòu),初始化渲染時(shí)調(diào)用createElem創(chuàng)建組件的Vnode匆篓。且將原本用于存儲(chǔ)組件jquery對(duì)象的container屬性浑度,調(diào)整為存儲(chǔ)組件的Vnode對(duì)象,在數(shù)據(jù)更新diff新老Vnode時(shí)作為老的Vnode鸦概。
(2)新增JSXComnent組件箩张,它繼承了base組件來(lái)保持與不改動(dòng)組件的統(tǒng)一性,同時(shí)所有為了使用虛擬dom而增加的特有方法都在JSXComnent中窗市,以免影響base組件和不改動(dòng)的UI組件先慷。使用虛擬dom的UI組件需繼承JSXComnent組件來(lái)使用特有的方法,這樣既保留了共性又可以靈活的增加使用虛擬dom時(shí)的特性咨察。
(3)在JSXComnent組件中重寫base的render方法论熙,未改動(dòng)的組件仍繼承base,且container值仍為jq對(duì)象且渲染邏輯不變摄狱。而支持虛擬dom組件的container為Vnode脓诡,重寫后的render方法邏輯調(diào)整為調(diào)用snabbdom的patch方法將Vnode掛載到頁(yè)面中。
(4)在JSXComnent組件增加setOpts方法媒役,當(dāng)數(shù)據(jù)改變時(shí)調(diào)用setOpts方法更新opts對(duì)象祝谚,并調(diào)用createElem方法根據(jù)新的opts創(chuàng)建新的Vnode,在調(diào)用patch方法進(jìn)行新老Vnode對(duì)比并將變動(dòng)映射到真實(shí)的dom上酣衷。為了避免連續(xù)多次調(diào)setOpts會(huì)頻繁的生成Vnode和渲染dom交惯,需要將多次數(shù)據(jù)更新進(jìn)行合并批量處理,目前這部分處理還不夠完善穿仪,之后會(huì)參考react狀態(tài)合并和多任務(wù)方案進(jìn)行優(yōu)化席爽。最終的渲染流程如下:
3、結(jié)合了snabbdom和jsx的組件定義與使用
(1)使用虛擬dom組件的定義:
(2)組件的調(diào)用保持不變牡借,通過(guò)new創(chuàng)建對(duì)象實(shí)例并初始化組件的opts屬性拳昌。以車身顏色為例:
(3)組件的嵌套保持不變,仍通過(guò)配置文件控制父子組件的嵌套關(guān)系钠龙,然后通過(guò)controller組件循環(huán)遍歷初始化組件炬藤。
五、總結(jié)
本文結(jié)合項(xiàng)目的特點(diǎn)基于snabbdom和jsx在項(xiàng)目中增加數(shù)據(jù)驅(qū)動(dòng)和虛擬dom的思想來(lái)優(yōu)化組件的開(kāi)發(fā)碴里,將我們從頻繁的dom操作中解脫出來(lái)沈矿。由于snabbdom自身的靈活性,雖然可以便捷的接入到項(xiàng)目中咬腋,但接入時(shí)需要考慮組件的設(shè)計(jì)羹膳、對(duì)象數(shù)據(jù)結(jié)構(gòu)等因素來(lái)制定適合自身項(xiàng)目的方案來(lái)達(dá)到靈活、快速接入的目的根竿,若投入較大的接入和維護(hù)成本那就不如直接使用react陵像、vue的框架進(jìn)行開(kāi)發(fā)了就珠。希望通過(guò)本文能為大家提供一種優(yōu)化組件開(kāi)發(fā)的參考方案。