日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

虚拟DOM详解

發(fā)布時間:2025/3/15 20 豆豆
生活随笔 收集整理的這篇文章主要介紹了 虚拟DOM详解 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

React為啥這么大?因為它實現(xiàn)了一個虛擬DOM(Virtual DOM)。虛擬DOM是干什么的?這就要從瀏覽器本身講起。

如我們所知,在瀏覽器渲染網(wǎng)頁的過程中,加載到HTML文檔后,會將文檔解析并構建DOM樹,然后將其與解析CSS生成的CSSOM樹一起結合產(chǎn)生愛的結晶——RenderObject樹,然后將RenderObject樹渲染成頁面(當然中間可能會有一些優(yōu)化,比如RenderLayer樹)。這些過程都存在與渲染引擎之中,渲染引擎在瀏覽器中是于JavaScript引擎(JavaScriptCore也好V8也好)分離開的,但為了方便JS操作DOM結構,渲染引擎會暴露一些接口供JavaScript調用。

由于這兩塊相互分離,通信是需要付出代價的,因此JavaScript調用DOM提供的接口性能不咋地。各種性能優(yōu)化的最佳實踐也都在盡可能的減少DOM操作次數(shù)。

而虛擬DOM干了什么?它直接用JavaScript實現(xiàn)了DOM樹(大致上)。組件的HTML結構并不會直接生成DOM,而是映射生成虛擬的JavaScript DOM結構,React又通過在這個虛擬DOM上實現(xiàn)了一個 diff 算法找出最小變更,再把這些變更寫入實際的DOM中。這個虛擬DOM以JS結構的形式存在,計算性能會比較好,而且由于減少了實際DOM操作次數(shù),性能會有較大提升。

?

  • 為什么需要虛擬DOM
  • 實現(xiàn)虛擬DOM
  • Diff算法
  • 映射成真實DOM

?

為什么需要虛擬DOM

先介紹瀏覽器加載一個HTML文件需要做哪些事,幫助我們理解為什么我們需要虛擬DOM。webkit引擎的處理流程,一圖勝千言:

?

所有瀏覽器的引擎工作流程都差不多,如上圖大致分5步:創(chuàng)建DOM tree –> 創(chuàng)建Style Rules -> 構建Render tree -> 布局Layout –> 繪制Painting

?

第一步,用HTML分析器,分析HTML元素,構建一顆DOM樹。

第二步:用CSS分析器,分析CSS文件和元素上的inline樣式,生成頁面的樣式表。

第三步:將上面的DOM樹和樣式表,關聯(lián)起來,構建一顆Render樹。這一過程又稱為Attachment。每個DOM節(jié)點都有attach方法,接受樣式信息,返回一個render對象(又名renderer)。這些render對象最終會被構建成一顆Render樹。

第四步:有了Render樹后,瀏覽器開始布局,會為每個Render樹上的節(jié)點確定一個在顯示屏上出現(xiàn)的精確坐標值。

第五步:Render數(shù)有了,節(jié)點顯示的位置坐標也有了,最后就是調用每個節(jié)點的paint方法,讓它們顯示出來。

當你用傳統(tǒng)的源生api或jQuery去操作DOM時,瀏覽器會從構建DOM樹開始從頭到尾執(zhí)行一遍流程。比如當你在一次操作時,需要更新10個DOM節(jié)點,理想狀態(tài)是一次性構建完DOM樹,再執(zhí)行后續(xù)操作。但瀏覽器沒這么智能,收到第一個更新DOM請求后,并不知道后續(xù)還有9次更新操作,因此會馬上執(zhí)行流程,最終執(zhí)行10次流程。顯然例如計算DOM節(jié)點的坐標值等都是白白浪費性能,可能這次計算完,緊接著的下一個DOM更新請求,這個節(jié)點的坐標值就變了,前面的一次計算是無用功。

即使計算機硬件一直在更新迭代,操作DOM的代價仍舊是昂貴的,頻繁操作還是會出現(xiàn)頁面卡頓,影響用戶的體驗。真實的DOM節(jié)點,哪怕一個最簡單的div也包含著很多屬性,可以打印出來直觀感受一下:

?

虛擬DOM就是為了解決這個瀏覽器性能問題而被設計出來的。例如前面的例子,假如一次操作中有10次更新DOM的動作,虛擬DOM不會立即操作DOM,而是將這10次更新的diff內容保存到本地的一個js對象中,最終將這個js對象一次性attach到DOM樹上,通知瀏覽器去執(zhí)行繪制工作,這樣可以避免大量的無謂的計算量。

?

實現(xiàn)虛擬DOM

我們來實現(xiàn)一個虛擬DOM。例如一個真實的DOM節(jié)點:

<div id="real-container">

??? <p>Real DOM</p>

??? <div>cannot update</div>

??? <ul>

??????? <li className="item">Item 1</li>

??????? <li className="item">Item 2</li>

??????? <li className="item">Item 3</li>

??? </ul>

</div>

用js對象來模擬DOM節(jié)點如下:

const tree = Element('div', { id: 'virtual-container' }, [

??? Element('p', {}, ['Virtual DOM']),

??? Element('div', {}, ['before update']),

??? Element('ul', {}, [

??????? Element('li', { class: 'item' }, ['Item 1']),

??????? Element('li', { class: 'item' }, ['Item 2']),

??????? Element('li', { class: 'item' }, ['Item 3']),

??? ]),

]);

?

const root = tree.render();

document.getElementById('virtualDom').appendChild(root);

用js對象模擬DOM節(jié)點的好處是,頁面的更新可以先全部反映在js對象上,操作內存中的js對象的速度顯然要快多了。等更新完后,再將最終的js對象映射成真實的DOM,交由瀏覽器去繪制。

那具體怎么實現(xiàn)呢?看一下Element方法的具體實現(xiàn):

function Element(tagName, props, children) {

??? if (!(this instanceof Element)) {

??????? return new Element(tagName, props, children);

??? }

?

??? this.tagName = tagName;

??? this.props = props || {};

??? this.children = children || [];

??? this.key = props ? props.key : undefined;

?

??? let count = 0;

??? this.children.forEach((child) => {

??????? if (child instanceof Element) {

??????????? count += child.count;

??????? }

??????? count++;

??? });

??? this.count = count;

}

第一個參數(shù)是節(jié)點名(如div),第二個參數(shù)是節(jié)點的屬性(如class),第三個參數(shù)是子節(jié)點(如ul的li)。除了這三個參數(shù)會被保存在對象上外,還保存了key和count。

?

有了js對象后,最終還需要將其映射成真實的DOM:

Element.prototype.render = function() {

??? const el = document.createElement(this.tagName);

??? const props = this.props;

?

??? for (const propName in props) {

??????? setAttr(el, propName, props[propName]);

??? }

?

??? this.children.forEach((child) => {

??????? const childEl = (child instanceof Element) ? child.render() : document.createTextNode(child);

?? ?????el.appendChild(childEl);

??? });

?

??? return el;

};

上面都是自解釋代碼,根據(jù)DOM名調用源生的createElement創(chuàng)建真實DOM,將DOM的屬性全都加到這個DOM元素上,如果有子元素繼續(xù)遞歸調用創(chuàng)建子元素,并appendChild掛到該DOM元素上。這樣就完成了從創(chuàng)建虛擬DOM到將其映射成真實DOM的全部工作。

?

Diff算法

我們已經(jīng)完成了創(chuàng)建虛擬DOM并將其映射成真實DOM的工作,這樣所有的更新都可以先反映到虛擬DOM上,如何反映呢?需要明確一下Diff算法。

兩棵樹如果完全比較時間復雜度是O(n^3),但參照《深入淺出React和Redux》一書中的介紹,React的Diff算法的時間復雜度是O(n)。要實現(xiàn)這么低的時間復雜度,意味著只能平層地比較兩棵樹的節(jié)點,放棄了深度遍歷。這樣做,似乎犧牲了一定的精確性來換取速度,但考慮到現(xiàn)實中前端頁面通常也不會跨層級移動DOM元素,所以這樣做是最優(yōu)的。

我們新創(chuàng)建一棵樹,用于和之前的樹進行比較.

const newTree = Element('div', { id: 'virtual-container' }, [

??? Element('h3', {}, ['Virtual DOM']),???????????????????? // REPLACE

??? Element('div', {}, ['after update']),?????????????????? // TEXT

??? Element('ul', { class: 'marginLeft10' }, [????????????? // PROPS

??????? Element('li', { class: 'item' }, ['Item 1']),

??????? // Element('li', { class: 'item' }, ['Item 2']),??? // REORDER remove

??????? Element('li', { class: 'item' }, ['Item 3']),

??? ]),

]);

只考慮平層地Diff的話,就簡單多了,只需要考慮以下4種情況:

第一種是最簡單的,節(jié)點類型變了,例如下圖中的P變成了h3。我們將這個過程稱之為REPLACE。直接將舊節(jié)點卸載(componentWillUnmount)并裝載新節(jié)點(componentWillMount)就行了。

?

(為簡單起見上圖隱藏了文本節(jié)點)

舊節(jié)點包括下面的子節(jié)點都將被卸載,如果新節(jié)點和舊節(jié)點僅僅是類型不同,但下面的所有子節(jié)點都一樣時,這樣做顯得效率不高。但為了避免O(n^3)的時間復雜度,這樣做是值得的。這也提醒了React開發(fā)者,應該避免無謂的節(jié)點類型的變化,例如運行時將div變成p就沒什么太大意義。

第二種也比較簡單,節(jié)點類型一樣,僅僅屬性或屬性值變了。

renderA: <ul>

renderB: <ul class: 'marginLeft10'>

=> [addAttribute class "marginLeft10"]

我們將這個過程稱之為PROPS。此時不會觸發(fā)節(jié)點的卸載(componentWillUnmount)和裝載(componentWillMount)動作。而是執(zhí)行節(jié)點更新(shouldComponentUpdate到componentDidUpdate的一系列方法)。

function diffProps(oldNode, newNode) {

??? const oldProps = oldNode.props;

??? const newProps = newNode.props;

?

??? let key;

??? const propsPatches = {};

??? let isSame = true;

?

??? // find out different props

??? for (key in oldProps) {

??????? if (newProps[key] !== oldProps[key]) {

??????????? isSame = false;

??????????? propsPatches[key] = newProps[key];

??????? }

??? }

?

??? // find out new props

??? for (key in newProps) {

??????? if (!oldProps.hasOwnProperty(key)) {

??????????? isSame = false;

??????????? propsPatches[key] = newProps[key];

??????? }

??? }

?

??? return isSame ? null : propsPatches;

}

第三種是文本變了,文本對也是一個Text Node,也比較簡單,直接修改文字內容就行了,我們將這個過程稱之為TEXT。

第四種是移動,增加,刪除子節(jié)點,我們將這個過程稱之為REORDER。具體可以看這篇虛擬DOM Diff算法解析。例如:

?

在中間插入一個節(jié)點,程序員寫代碼很簡單:$(B).after(F)。但如何高效地插入呢?簡單粗暴的做法是:卸載C,裝載F,卸載D,裝載C,卸載E,裝載D,裝載E。如下圖:

?

我們寫JSX代碼時,如果沒有給數(shù)組或枚舉類型定義一個key,就會看到下面這樣的warning。React提醒我們,沒有key的話,涉及到移動,增加,刪除子節(jié)點的操作時,就會用上面那種簡單粗暴的做法來更新。雖然程序運行不會有錯,但效率太低,因此React會給我們一個warning。

如果我們在JSX里為數(shù)組或枚舉型元素增加上key后,React就能根據(jù)key,直接找到具體的位置進行操作,效率比較高。如下圖:

?

常見的最小編輯距離問題,可以用Levenshtein Distance算法來實現(xiàn),時間復雜度是O(M*N),但通常我們只要一些簡單的移動就能滿足需要,降低點精確性,將時間復雜度降低到O(max(M, N)即可。具體可參照采用深度剖析:如何實現(xiàn)一個 Virtual DOM 算法里的一個算法一文。或自行閱讀例子中的源代碼

最終Diff出來的結果如下:

{

??? 1: [ {type: REPLACE, node: Element} ],

??? 4: [ {type: TEXT, content: "after update"} ],

??? 5: [ {type: PROPS, props: {class: "marginLeft10"}}, {type: REORDER, moves: [{index: 2, type: 0}]} ],

??? 6: [ {type: REORDER, moves: [{index: 2, type: 0}]} ],

??? 8: [ {type: REORDER, moves: [{index: 2, type: 0}]} ],

??? 9: [ {type: TEXT, content: "Item 3"} ],

}

映射成真實DOM

虛擬DOM有了,Diff也有了,現(xiàn)在就可以將Diff應用到真實DOM上了。深度遍歷DOM將Diff的內容更新進去:

function dfsWalk(node, walker, patches) {

??? const currentPatches = patches[walker.index];

?

??? const len = node.childNodes ? node.childNodes.length : 0;

??? for (let i = 0; i < len; i++) {

??????? walker.index++;

??????? dfsWalk(node.childNodes[i], walker, patches);

??? }

?

??? if (currentPatches) {

??????? applyPatches(node, currentPatches);

??? }

}

具體更新的代碼如下,其實就是根據(jù)Diff信息調用源生API操作DOM:

function applyPatches(node, currentPatches) {

??? currentPatches.forEach((currentPatch) => {

??????? switch (currentPatch.type) {

??????????? case REPLACE: {

??????????????? const newNode = (typeof currentPatch.node === 'string')

??????????????????? ? document.createTextNode(currentPatch.node)

??????????????????? : currentPatch.node.render();

??????????????? node.parentNode.replaceChild(newNode, node);

??????????????? break;

??????????? }

????????? ??case REORDER:

??????????????? reorderChildren(node, currentPatch.moves);

??????????????? break;

??????????? case PROPS:

??????????????? setProps(node, currentPatch.props);

??????????????? break;

??????????? case TEXT:

??????????????? if (node.textContent) {

??????????????????? node.textContent = currentPatch.content;

??????????????? } else {

??????????????????? // ie

??????????????????? node.nodeValue = currentPatch.content;

??????????????? }

??????????????? break;

??????????? default:

? ??????????????throw new Error(`Unknown patch type ${currentPatch.type}`);

??????? }

??? });

}

虛擬DOM的目的是將所有操作累加起來,統(tǒng)計計算出所有的變化后,統(tǒng)一更新一次DOM。其實即使不懂原理,業(yè)務代碼照樣寫,但理解原理后,出了什么新東東如React Fiber才能快速跟上。

?

?

轉載于:https://www.cnblogs.com/ranyonsue/p/9831434.html

總結

以上是生活随笔為你收集整理的虚拟DOM详解的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內容還不錯,歡迎將生活随笔推薦給好友。