關(guān)于React中的聲明式渲染框架問題
在學習React源碼之前,我們先搞清楚框架的范式都有哪些??蚣芊妒街饕袃煞N:命令式和聲明式,目前大部份流行框架都采用聲明式渲染,為什么都選擇聲明式渲染呢?對比命令式它有什么優(yōu)勢呢?為了搞清楚這些問題,我們先從動態(tài)渲染頁面的三種方式:純JS運算,innerHTML,虛擬DOM,分別比較他們的性能、可維護性和心智負擔,來闡明基于虛擬DOM聲明式渲染的優(yōu)勢。然后會說到與聲明式框架密切相關(guān)的運行時和編譯時。相信看完你會對React、Vue這一類采用虛擬DOM的聲明式框架有自己的理解。
1. 命令式和聲明式
在對比之前,我們先了解一下什么是聲明式,什么是命令式,它們各有什么優(yōu)缺點。作為框架學習者,了解這兩種范式的框架對學習框架思想很有幫助。 我們先看看命令式和聲明式框架的概念和具體形式。
1.1 命令式
什么是命令式?早年間大范圍流行的JQuery
就是典型的命令式框架,命令式框架最大的特點是關(guān)注過程,例如我要做如下DOM操作:
- 獲取id為app的div元素
- 把元素的顯示文本設置為 hello world
- 給他綁定點擊事件
- 事件內(nèi)容是彈窗提示ok
用jQuery可以寫出如下代碼:
$("#app") // 1 .text("hello world") // 2 .on('click', function(){ // 3 alert("ok") // 4 })
可以看到自然語言描述能夠跟實際寫代碼一一對應起來,寫代碼本身就是在描 述做事的過程,這很符合日常生活的直覺和邏輯。而且整個過程沒有任何其他的性能開銷,因此命令式框架的性能一搬都非常不錯。
1.2 聲明式
什么是聲明式?與命令式框架不同關(guān)注過程不同,聲明式框架更 關(guān)注結(jié)果 ,所見即所得。按照框架的規(guī)范,聲明出用戶想要的結(jié)果,具體怎么實現(xiàn)無需關(guān)心,都交給框架處理。 同樣上面的那個例子里面,用React這種聲明式的框架可以這樣寫:
<div id="app" onClick={()=>alert("ok")}>hello world</div>
在react里面一般都是用JSX描述頁面的dom結(jié)構(gòu)??梢钥吹轿覀冎恍杼峁┮粋€最終的“結(jié)果”,至于具體怎么實現(xiàn)這個結(jié)果的過程,我們并不需要關(guān)心。換句話說React框架幫我們封裝了實現(xiàn)的過程,因此應該能夠猜到React框架內(nèi)部實現(xiàn)一定是命令式的,但是暴露給用戶的是更加直觀的聲明式。
1.3 兩種范式的性能和易維護性
我們首先拋出一個結(jié)論:聲明式代碼的性能不優(yōu)于命令式代碼的新能。為什么呢?還是拿上面的例子來說,如果要把文本內(nèi)容改為:“react”,用命令式代碼就很簡單,因為用戶明確的知道要修改的是什么,直接調(diào)用相關(guān)api即可:
app.textContent = 'react'
試想一下,有沒有其他的實現(xiàn)方式比這個代碼性能更好的?答案是沒有,因為我們明確的知道是哪些地方發(fā)生了變化,直接修改變化的地方就行, 因此命令式的代碼能做到極致的性能優(yōu)化。但是聲明式的代碼目前還做不到這一點,因為它表述的是結(jié)果:
// 之前
<div id="app" onClick={()=>alert("ok")}>hello world</div>
// 之后
<div id="app" onClick={()=>alert("ok")}>react</div>
對于框架來說,為了實現(xiàn)最好的更新性能,框架需要找到新舊DOM的差異,并且只更新有差異的地方,最終還是用命令式的代碼完成這次變更:
app.textContent = 'react'
如果把修改文本的性能消耗為A,找出新舊內(nèi)容差異的性能損耗為B,那么會有如下公式:
- 命令式的代碼更新性能為:A
- 聲明式的代碼更新性能為:B + A
可以看出,聲明式代碼比命令式代碼多了找出差異的性能消耗,最理想的情況是查找差異性能的消耗為0,此時命令式代碼和聲明式代碼的性能相同,但是無法超過命令式代碼。因為框架本身封裝了命令式的代碼才實現(xiàn)了面向用戶的聲明式,這也側(cè)面印證了之前的結(jié)論:聲明式代碼的性能不優(yōu)于命令式代碼的新能。
既然命令式代碼性能這么好,又直接,為啥還有類似React,Vue這樣的聲明是框架呢?原因是聲明式代碼的可維護性更強。從之前的例子可以看出,采用命令式代碼實現(xiàn)的時候,我們需要關(guān)注整個實現(xiàn)過程的每一步,包括DOM元素的創(chuàng)建、獲取、更新、刪除等操作,過程繁瑣而且抽象,心智負擔高。而聲明式代碼展示就是最終我們想要的結(jié)果,更加直觀,只關(guān)注結(jié)果效率高,而實現(xiàn)結(jié)果的命令式的代碼框架內(nèi)部已經(jīng)實現(xiàn),不需要用戶關(guān)心。
但是聲明式代碼在提升可讀性和維護性的同時,面臨的問題是性能上有一部分損耗,所以框架要做的是:保持可維護性的同時讓性能損耗最小。在這種前提下,就有人提出了 虛擬節(jié)點(Virtual DOM) 這種找出新舊差異的方案,并被廣泛運用于React,Vue這類框架之中。那么虛擬DOM的性能到底如何呢?
2. 虛擬DOM的性能如何
說到這里相信大家有一個基本的了解,那就是采用虛擬DOM的框架更新新時,理論上性能不會比原生JS操作dom性能更好,理論上是指用戶寫的命令式代碼是絕對優(yōu)化的。在實際場景中這很難,可能需要投入巨大的精力,所以投入產(chǎn)出比不高,目前只談理論性能。
那么有沒有一種辦法既不需要投入太大的精力,又能保證代碼程序的性能下限,不至于讓應用程序性能太差。甚至經(jīng)過一定的優(yōu)化處理,接近命令式代碼的性能呢?其實這就是虛擬DOM要解決的問題。
上文說的原生JS操作dom的命令式代碼,指的是document.createElement等方法,不包括innerHTML這個方法,它比較特殊,需要單獨探討它。在早年使用JQuery或直接寫原生JS代碼的時候,innerHTML操作dom是非常常見的。那么我們可以考慮以下幾個問題:
- innerHTML的渲染流程是什么樣的?
- innerHTML的性能相比較虛擬DOM誰的性能好?
首先對于第一個問題,對于innerHTML創(chuàng)建頁面,需要先構(gòu)造一段HTML字符串:
let htmlStr = '<ul>' for(let i=0; i<data.length; i++) { htmlStr += `<li>${data[i].name}</li>` } htmlStr += '</ul>'
然后把這個字符串賦值給dom元素的innerHTML屬性:
app.innerHTML = htmlStr
在賦值之后,由于要渲染出頁面,首先要吧字符串解析成DOM樹,這一步是DOM層面的計算。然而,涉及DOM的運算性能要遠比JS層面的計算性能差很多,我們可以在jsbench.me這個網(wǎng)站上給它們跑個分,比較創(chuàng)建10000個js對象和10000個dom元素的性能,結(jié)果如下:
我們可以看出,純JS運算要比操作DOM快得多,他們不在一個數(shù)量級上?;谶@個前提,我們可以得出innerHTML創(chuàng)建頁面的性能為:拼接HTML字符串的計算量 + innerHTML 的 DOM計算量。
我們再看第二個問題,innerHTML的性能相比較虛擬DOM誰的性能好?我們再看一下虛擬dom創(chuàng)建頁面的過程。第一步,先創(chuàng)建JS對象,這個對象是對真實DOM的描述,也就是大家說的虛擬DOM,第二部是遞歸JS對象并創(chuàng)建所有對應的真實DOM。我們也可以用一個公式來表述他們的性能消耗:創(chuàng)建JS對象的計算量 + 創(chuàng)建真實DOM的計算量。
1.比方說有這樣一個虛擬dom對象:
const vdom = { type:'ul', children: { type: 'li', } }
2.遞歸對象創(chuàng)建真實DOM渲染到頁面
function render(vdom, anchor){ const el = document.createElement(vdom.type) anchor.appendChild(el) if(vdom.children){ render(children, el) } } render(vdom)
我們列一個表格對比一下純JS運算、虛擬DOM和innerHTML創(chuàng)建頁面時所消耗的性能:
純JS運算 | 虛擬DOM | innerHTML |
---|---|---|
創(chuàng)建js對象(vdom) | 渲染HTML字符串 | |
DOM運算 | 創(chuàng)建所有DOM元素 | 創(chuàng)建所有DOM元素 |
我們可以看出虛擬DOM和innerHTML創(chuàng)建頁面時流程差不多,性能兩者差別不大。在相同數(shù)量級上面,基本上沒有什么區(qū)別,因為都要新建所有的DOM元素。
看到這里可能有人會說,性能都差不多那還要虛擬DOM干嘛,這不是多此一舉嘛。別急,上面說的的新創(chuàng)建DOM。在都是新創(chuàng)建所有的DOM元素來說虛擬DOM對比innerHTML在性能上確實沒有任何優(yōu)勢可言。但是在我們更新頁面的時候,哪怕我們只改了一個字,用innerHTML這種方式更新頁面時,要先銷毀之前所有的DOM元素,然后根據(jù)新的html字符串重新創(chuàng)建所有的DOM。我們再看看虛擬DOM是怎么更新頁面的,它需要重新創(chuàng)建js對象(vdom),然后比較新舊虛擬DOM,找到變化的元素然后更新它。如下面這個表格所示:
純JS運算 | 虛擬DOM | innerHTML |
---|---|---|
1. 創(chuàng)建js對象(vdom) 2. Diff找出變化的部分 | 渲染HTML字符串 | |
DOM運算 | 只更新變化的部分DOM | 1. 創(chuàng)建所有的新DOM元素 2. 創(chuàng)建所有的新DOM元素 |
在頁面更新的時候,采用虛擬DOM更新頁面,由于經(jīng)過JS計算出哪些DOM元素需要更新,只需要更新對應的DOM元素即可。而采用innerHTML這種方式需要先銷毀所有的DOM元素,然后又創(chuàng)建所有DOM。綜合之前的JS運算比DOM運算的性能快的多的結(jié)論下,這時候虛擬DOM的優(yōu)勢就提現(xiàn)出來了。
此外,當頁面頁面更新時,影響虛擬DOM的性能因素與影響innerHTML的性能因素補貼。對于虛擬DOM來說,無論頁面多大,只更新變化的內(nèi)容,所以性能跟變化內(nèi)容的大小有關(guān)。對innerHTML這種方式來說,就不關(guān)系變化內(nèi)容的大小了,只關(guān)心要渲染性能跟html字符串的大小有關(guān)。
純JS運算 | 虛擬DOM | innerHTML |
---|---|---|
1. 創(chuàng)建js對象(vdom) 2. Diff找出變化的部分 | 渲染HTML字符串 | |
DOM運算 性能因素 | 1. 只更新變化的部分DOM 2. 與數(shù)據(jù)變化量相關(guān) | 1. 創(chuàng)建所有的新DOM元素 2. 創(chuàng)建所有的新DOM元素 3. 與模板大小相關(guān) |
基于上面的描述,我們可以總結(jié)一下原生JS(指createElement等方法)、虛擬DOM、innerHTML這三個方法在更新頁面時候的性能,如下表所示:
純JS運算 | 虛擬DOM | innerHTML |
---|---|---|
心智負擔大 | 心智負擔小 | 心智負擔小中等 |
性能最好 | 性能不錯 | 性能差 |
可維護性差 | 可維護性強 | 可維護性一版 |
我們分了一個維度去考量:心智負擔、可維護性、性能:
- 對于純JS運算,毫無疑問原生JS的DOM操作這種方式心智負擔最大,因為需要手動增刪改查大量的DOM元素。但它的性能是最好的,不過要承受巨大的心智負擔,而且代碼可能讀性很差,不便于后期維護。
- 對于innerHTML,由于有一部分是拼接字符串來實現(xiàn)的,有點類似于聲明式的代碼了,但也存在著一定的心智負擔,而且其他的DOM操作(綁事件,增加屬性等)還是得通過原生JS來處理。此外如果html字符串如果很大的話還可能有性能問題。
- 對于虛擬DOM:由于虛擬DOM是聲明式的,心智負擔比較小,可維護性強,性能雖然比不上極致優(yōu)化的原生JS,但是在頁面更新的時候也有著不錯的性能。
一番權(quán)衡之后,發(fā)現(xiàn)虛擬 DOM 是個還不錯的選擇。這也是大部份流行框架采用虛擬DOM的原因。
可能有的人要問了,有沒有一種方法能做到:既可以聲明式的描述UI結(jié)構(gòu),同時又具備原生JS的性能呢?這些問題在下一節(jié)討論。
3. 運行時和編譯時
我們先來說一下純運行時的框架。假如我們設計了一個框架,它提供了一個Render函數(shù),用戶只要傳入虛擬DOM,Render函數(shù)就會遞歸創(chuàng)建真實DOM把它插入到對應的節(jié)點,還是拿之前的代碼為例:
3.1 運行時
1.虛擬dom對象:
const vdom = { type:'ul', children: { type: 'li', } }
2.創(chuàng)建真實DOM:
function render(vdom, anchor){ const el = document.createElement(vdom.type) anchor.appendChild(el) if(vdom.children){ render(children, el) } } render(vdom)
3.2 運行時 + 編譯時
在瀏覽器上運行這段代碼可以看到預期的結(jié)果。但是有人會說,寫這樣的dom描述對象太不直觀了,而且手寫起來很麻煩。有沒有一種方式能夠支持寫HTML就能得到dom描述對象呢?答案是有的,我們可以引入編譯手段,將寫好的HTML編譯成dom描述對象,再把這個對象交給Render函數(shù),將他渲染到頁面上。流程如下:
- 寫了一個Compiler程序,將HTML聲明式的代碼變成了產(chǎn)出dom描述對象的函數(shù):
const el = <div id="app" onClick={()=>alert("ok")}>react</div> const vdom = Compiler(el)
- 上面的
vdom
就會編譯成如下結(jié)果:
{ type: 'div', props: { click: () => alert("ok"), chilren: ['react'] } }
- Render函數(shù)傳入編譯得到的dom描述對象就可以把之前聲明式的dom節(jié)點渲染到頁面上了。
Render(vdom, container)
實際上面就是 運行時 + 編譯時 框架的基本工作流程。用戶可以選擇提供dom描述對象或者寫HTML片段,來描述UI界面,如果是dom描述對象就直接渲染,如果是HTML片段就先編譯再渲染。代碼運行起來才進行編譯,叫做運行編譯時,這會產(chǎn)生一定的性能開銷,所以有些框架可以在構(gòu)建的時候執(zhí)行Compiler將提供的內(nèi)容 提前編譯好,運行的時候就無需編譯了,這對程序性能也有一部分提升。像React,Vue就是這么做的。
3.3 編譯時
有人可能會問了,既然能把能把HTML片段編譯成dom描述對象,那為啥不直接HTML片段編譯成命令式的代碼呢?答案是可以的,這樣就不支持任何運行時內(nèi)容,用戶的代碼需要編譯才能運行,它就是純編譯時框架了。目前就有一些框架把聲明式的代碼編譯成命令式代碼,例如:sveltejs、solidjs等框架,它既保持了聲明式的易維護特性,又保證了程序的性能。
4. 總結(jié)
我們先討論了命令式和聲明式這兩種范式的差異,其中命令式更加關(guān)注過程,而 聲明式更加關(guān)注結(jié)果。命令式在理論上可以做到極致優(yōu)化,但是用戶要承受巨大的心智負擔;而 聲明式能夠有效減輕用戶的心智負擔,但是性能上有一定的犧牲,框架要想辦法盡量使性 能損耗最小化。
后面,我們討論了虛擬 DOM 的性能,并給出了一個公式:聲明式的更新性能消耗 = 找出 差異的性能消耗 + 直接修改的性能消耗。虛擬DOM 的意義就在于使找出差異的性能消耗最小 化。我們發(fā)現(xiàn),用原生JavaSoript操作DOM 的方法(如 document.createElement )、虛擬 DOM 和 tnnerHTML 三者操作頁面的性能,不可以簡單地下定論,這與頁面大小、變更部分的大小都有關(guān) 系,除此之外,與創(chuàng)建頁面還是更新頁面也有關(guān)系,選擇哪種更新策略,需要我們結(jié)合心智負擔、 可維護性等因素綜合考慮。
再后面了解了運行時和編譯時的相關(guān)知識和各自的特點。
下一節(jié)我們著重來說一下React聲明式框架是如何將JSX創(chuàng)建虛擬DOM,以及虛擬DOM是怎么渲染到頁面上的。
到此這篇關(guān)于React中的聲明式渲染框架的文章就介紹到這了,更多相關(guān)React渲染框架內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React-hook-form-mui基本使用教程(入門篇)
react-hook-form-mui可以幫助開發(fā)人員更輕松地構(gòu)建表單,它結(jié)合了React?Hook?Form和Material-UI組件庫,使用react-hook-form-mui,開發(fā)人員可以更快速地構(gòu)建表單,并且可以輕松地進行表單驗證和數(shù)據(jù)處理,本文介紹React-hook-form-mui基本使用,感興趣的朋友一起看看吧2024-02-02React中獲取數(shù)據(jù)的3種方法及優(yōu)缺點
這篇文章主要介紹了React中獲取數(shù)據(jù)的3種方法及優(yōu)缺點,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-02-02React Native中實現(xiàn)動態(tài)導入的示例代碼
隨著業(yè)務的發(fā)展,每一個 React Native 應用的代碼數(shù)量都在不斷增加。作為一個前端想到的方案自然就是動態(tài)導入(Dynamic import)了,本文介紹了React Native中實現(xiàn)動態(tài)導入的示例代碼,需要的可以參考一下2022-06-06