摘要:本文為筆者通過實際操作,實現了一個非常簡單的,加深對現今主流前端框架中的理解。用對象表示樹是用對象表示,并存儲在內存中的。如果類型不一致,那么屬性一定是被更新的。如果有不相等的屬性,則認為發生改變,需要處理的變化。
眾所周知,對前端而言,直接操作 DOM 是一件及其耗費性能的事情,以 React 和 Vue 為代表的眾多框架普遍采用 Virtual DOM 來解決如今愈發復雜 Web 應用中狀態頻繁發生變化導致的頻繁更新 DOM 的性能問題。本文為筆者通過實際操作,實現了一個非常簡單的 Virtual DOM ,加深對現今主流前端框架中 Virtual DOM 的理解。
關于 Virtual DOM ,社區已經有許多優秀的文章,而本文是筆者采用自己的方式,并有所借鑒前輩們的實現,以淺顯易懂的方式,對 Virtual DOM 進行簡單實現,但不包含snabbdom的源碼分析,在筆者的最終實現里,參考了snabbdom的原理,將本文的Virtual DOM實現進行了改進,感興趣的讀者可以閱讀上面幾篇文章,并參考筆者本文的最終代碼進行閱讀。
本文閱讀時間約15~20分鐘。
概述本文分為以下幾個方面來講述極簡版本的 Virtual DOM 核心實現:
Virtual DOM 主要思想
用 JavaScript 對象表示 DOM 樹
將 Virtual DOM 轉換為真實 DOM
設置節點的類型
設置節點的屬性
對子節點的處理
處理變化
新增與刪除節點
更新節點
更新子節點
Virtual DOM 主要思想要理解 Virtual DOM 的含義,首先需要理解 DOM ,DOM 是針對 HTML 文檔和 XML 文檔的一個 API , DOM 描繪了一個層次化的節點樹,通過調用 DOM API,開發人員可以任意添加,移除和修改頁面的某一部分。而 Virtual DOM 則是用 JavaScript 對象來對 Virtual DOM 進行抽象化的描述。Virtual DOM 的本質是JavaScript對象,通過 Render函數,可以將 Virtual DOM 樹 映射為 真實 DOM 樹。
一旦 Virtual DOM 發生改變,會生成新的 Virtual DOM ,相關算法會對比新舊兩顆 Virtual DOM 樹,并找到他們之間的不同,盡可能地通過最少的 DOM 操作來更新真實 DOM 樹。
我們可以這么表示 Virtual DOM 與 DOM 的關系:DOM = Render(Virtual DOM)。
用 JavaScript 對象表示 DOM 樹Virtual DOM 是用 JavaScript 對象表示,并存儲在內存中的。主流的框架均支持使用 JSX 的寫法, JSX 最終會被 babel 編譯為JavaScript 對象,用于來表示Virtual DOM,思考下列的 JSX:
item
最終會被babel編譯為如下的 JavaScript對象:
{ type: "div", props: null, children: [{ type: "span", props: { class: "item", }, children: ["item"], }, { type: "input", props: { disabled: true, }, children: [], }], }
我們可以注意到以下兩點:
所有的 DOM 節點都是一個類似于這樣的對象:
{ type: "...", props: { ... }, children: { ... }, on: { ... } }
本文節點是用 JavaScript 字符串來表示
那么 JSX 又是如何轉化為 JavaScript 對象的呢。幸運的是,社區有許許多多優秀的工具幫助我們完成了這件事,由于篇幅有限,本文對這個問題暫時不做探討。為了方便大家更快速地理解 Virtual DOM ,對于這一個步驟,筆者使用了開源工具來完成。著名的 babel 插件babel-plugin-transform-react-jsx幫助我們完成這項工作。
為了更好地使用babel-plugin-transform-react-jsx,我們需要搭建一下webpack開發環境。具體過程這里不做闡述,有興趣自己實現的同學可以到simple-virtual-dom查看代碼。
對于不使用 JSX 語法的同學,可以不配置babel-plugin-transform-react-jsx,通過我們的vdom函數創建 Virtual DOM:
function vdom(type, props, ...children) { return { type, props, children, }; }
然后我們可以通過如下代碼創建我們的 Virtual DOM 樹:
const vNode = vdom("div", null, vdom("span", { class: "item" }, "item"), vdom("input", { disabled: true }) );
在控制臺輸入上述代碼,可以看到,已經創建好了用 JavaScript對象表示的 Virtual DOM 樹:
將 Virtual DOM 轉換為真實 DOM現在我們知道了如何用 JavaScript對象 來代表我們的真實 DOM 樹,那么, Virtual DOM 又是怎么轉換為真實 DOM 給我們呈現的呢?
在這之前,我們要先知道幾項注意事項:
在代碼中,筆者將以$開頭的變量來表示真實 DOM 對象;
toRealDom函數接受一個 Virtual DOM 對象為參數,將返回一個真實 DOM 對象;
mount函數接受兩個參數:將掛載 Virtual DOM 對象的父節點,這是一個真實 DOM 對象,命名為$parent;以及被掛載的 Virtual DOM 對象vNode;
下面是toRealDom的函數原型:
function toRealDom(vNode) { let $dom; // do something with vNode return $dom; }
通過toRealDom方法,我們可以將一個vNode對象轉化為一個真實 DOM 對象,而mount函數通過appendChild,將真實 DOM 掛載:
function mount($parent, vNode) { return $parent.appendChild(toRealDom(vNode)); }
下面,讓我們來分別處理vNode的type、props和children。
設置節點的類型首先,因為我們同時具有字符類型的文本節點和對象類型的element節點,需要對type做多帶帶的處理:
if (typeof vNode === "string") { $dom = document.createTextNode(vNode); } else { $dom = document.createElement(vNode.type); }
在這樣一個簡單的toRealDom函數中,對type的處理就完成了,接下來讓我們看看對props的處理。
設置節點的屬性我們知道,如果節點有props,那么props是一個對象。通過遍歷props,調用setProp方法,對每一類props多帶帶處理。
if (vNode.props) { Object.keys(vNode.props).forEach(key => { setProp($dom, key, vNode.props[key]); }); }
setProp接受三個參數:
$target,這是一個真實 DOM 對象,setProp將對這個節點進行 DOM 操作;
name,表示屬性名;
value,表示屬性的值;
讀到這里,相信你已經大概清楚setProp需要做什么了,一般情況下,對于普通的props,我們會通過setAttribute給 DOM 對象附加屬性。
function setProp($target, name, value) { return $target.setAttribute(name, value); }
但這遠遠不夠,思考下列的 JSX 結構:
console.log("item")}>item
從上面的 JSX 結構中,我們發現以下幾點:
由于class是 JavaScript 的保留字, JSX 一般使用className來表示 DOM 節點所屬的class;
一般以on開頭的屬性來表示事件;
除字符類型外,屬性還可能是布爾值,如disabled,當該值為true時,則添加這一屬性;
所以,setProp也同樣需要考慮上述情況:
function isEventProp(name) { return /^on/.test(name); } function extractEventName(name) { return name.slice(2).toLowerCase(); } function setProp($target, name, value) { if (name === "className") { // 因為class是保留字,JSX使用className來表示節點的class return $target.setAttribute("class", value); } else if (isEventProp(name)) { // 針對 on 開頭的屬性,為事件 return $target.addEventListener(extractEventName(name), value); } else if (typeof value === "boolean") { // 兼容屬性為布爾值的情況 if (value) { $target.setAttribute(name, value); } return $target[name] = value; } else { return $target.setAttribute(name, value); } }
最后,還有一類屬性是我們的自定義屬性,例如主流框架中的組件間的狀態傳遞,即通過props來進行傳遞的,我們并不希望這一類屬性顯示在 DOM 中,因此需要編寫一個函數isCustomProp來檢查這個屬性是否是自定義屬性,因為本文只是為了實現 Virtual DOM 的核心思想,為了方便,在本文中,這個函數直接返回false。
function isCustomProp(name) { return false; }
最終的setProp函數:
function setProp($target, name, value) { if (isCustomProp(name)) { return; } else if (name === "className") { // fix react className return $target.setAttribute("class", value); } else if (isEventProp(name)) { return $target.addEventListener(extractEventName(name), value); } else if (typeof value === "boolean") { if (value) { $target.setAttribute(name, value); } return $target[name] = value; } else { return $target.setAttribute(name, value); } }對子節點的處理
對于children里的每一項,都是一個vNode對象,在進行 Virtual DOM 轉化為真實 DOM 時,子節點也需要被遞歸轉化,可以想到,針對有子節點的情況,需要對子節點以此遞歸調用toRealDom,如下代碼所示:
if (vNode.children && vNode.children.length) { vNode.children.forEach(childVdom => { const realChildDom = toRealDom(childVdom); $dom.appendChild(realChildDom); }); }
最終完成的toRealDom如下:
function toRealDom(vNode) { let $dom; if (typeof vNode === "string") { $dom = document.createTextNode(vNode); } else { $dom = document.createElement(vNode.type); } if (vNode.props) { Object.keys(vNode.props).forEach(key => { setProp($dom, key, vNode.props[key]); }); } if (vNode.children && vNode.children.length) { vNode.children.forEach(childVdom => { const realChildDom = toRealDom(childVdom); $dom.appendChild(realChildDom); }); } return $dom; }處理變化
Virtual DOM 之所以被創造出來,最根本的原因是性能提升,通過 Virtual DOM ,開發者可以減少許多不必要的 DOM 操作,以達到最優性能,那么下面我們來看看 Virtual DOM 算法 是如何通過對比更新前的 Virtual DOM 樹和更新后的 Virtual DOM 樹來實現性能優化的。
注:本文是筆者的最簡單實現,目前社區普遍通用的算法是snabbdom,如 Vue 則是借鑒該算法實現的 Virtual DOM ,有興趣的讀者可以查看這個庫的源代碼,基于本文的 Virtual DOM 的小示例,筆者最終也參考了該算法實現,本文demo傳送門,由于篇幅有限,感興趣的讀者可以自行研究。
為了處理變化,首先聲明一個updateDom函數,這個函數接受以下四個參數:
$parent,表示將被掛載的父節點;
oldVNode,舊的VNode對象;
newVNode,新的VNode對象;
index,在更新子節點時使用,表示當前更新第幾個子節點,默認為0;
函數原型如下:
function updateDom($parent, oldVNode, newVNode, index = 0) { }新增與刪除節點
首先我們來看新增一個節點的情況,對于原本沒有該節點,需要添加新的一個節點到 DOM 樹中,我們需要通過appendChild來實現:
轉化為代碼表述為:
// 沒有舊的節點,添加新的節點 if (!oldVNode) { return $parent.appendChild(toRealDom(newVNode)); }
同理,對于刪除一個舊節點的情況,我們通過removeChild來實現,在這里,我們應該從真實 DOM 中將舊的節點刪掉,但問題是在這個函數中是直接取不到這一個節點的,我們需要知道這個節點在父節點中的位置,事實上,可以通過$parent.childNodes[index]來取到,這便是上面提到的為何需要傳入index,它表示當前更新的節點在父節點中的索引:
轉化為代碼表述為:
const $currentDom = $parent.childNodes[index]; // 沒有新的節點,刪除舊的節點 if (!newVNode) { return $parent.removeChild($currentDom); }更新節點
Virtual DOM 的核心在于如何高效更新節點,下面我們來看看更新節點的情況。
首先,針對文本節點,我們可以簡單處理,對于文本節點是否發生改變,只需要通過比較其新舊字符串是否相等即可,如果是相同的文本節點,是不需要我們更新 DOM 的,在updateDom函數中,直接return即可:
// 都是文本節點,都沒有發生變化 if (typeof oldVNode === "string" && typeof newVNode === "string" && oldVNode === newVNode) { return; }
接下來,考慮節點是否真的需要更新,如圖所示,一個節點的類型從span換成了div,顯而易見,這是一定需要我們去更新DOM的:
我們需要編寫一個函數isNodeChanged來幫助我們判斷舊節點和新節點是否真的一致,如果不一致,需要我們把節點進行替換:
function isNodeChanged(oldVNode, newVNode) { // 一個是textNode,一個是element,一定改變 if (typeof oldVNode !== typeof newVNode) { return true; } // 都是textNode,比較文本是否改變 if (typeof oldVNode === "string" && typeof newVNode === "string") { return oldVNode !== newVNode; } // 都是element節點,比較節點類型是否改變 if (typeof oldVNode === "object" && typeof newVNode === "object") { return oldVNode.type !== newVNode.type; } }
在updateDom中,發現節點類型發生變化,則將該節點直接替換,如下代碼所示,通過調用replaceChild,將舊的 DOM 節點移除,并將新的 DOM 節點加入:
if (isNodeChanged(oldVNode, newVNode)) { return $parent.replaceChild(toRealDom(newVNode), $currentDom); }
但這遠遠還沒有結束,考慮下面這種情況:
對比上面的新舊兩個節點,發現節點類型并沒有發生改變,即VNode.type都是"div",但是節點的屬性卻發生了改變,除了針對節點類型的變化更新 DOM 外,針對節點的屬性的改變,也需要對應把 DOM 更新。
與上述方法類似,我們編寫一個isPropsChanged函數,來判斷新舊兩個節點的屬性是否有發生變化:
function isPropsChanged(oldProps, newProps) { // 類型都不一致,props肯定發生變化了 if (typeof oldProps !== typeof newProps) { return true; } // props為對象 if (typeof oldProps === "object" && typeof newProps === "object") { const oldKeys = Object.keys(oldProps); const newkeys = Object.keys(newProps); // props的個數都不一樣,一定發生了變化 if (oldKeys.length !== newkeys.length) { return true; } // props的個數相同的情況,遍歷props,看是否有不一致的props for (let i = 0; i < oldKeys.length; i++) { const key = oldKeys[i] if (oldProps[key] !== newProps[key]) { return true; } } // 默認未改變 return false; } return false; }
因為當節點沒有任何屬性時,props為null,isPropsChanged首先判斷新舊兩個節點的props是否是同一類型,即是否存在舊節點的props為null,新節點有新的屬性,或者反之:新節點的props為null,舊節點的屬性被刪除了。如果類型不一致,那么屬性一定是被更新的。
接下來,考慮到節點在更新前后都有props的情況,我們需要判斷更新前后的props是否一致,即兩個對象是否全等,遍歷即可。如果有不相等的屬性,則認為props發生改變,需要處理props的變化。
現在,讓我們回到我們的updateDom函數,看看是把Virtual DOM 節點props的更新應用到真實 DOM 上的。
// 虛擬DOM的type未改變,對比節點的props是否改變 const oldProps = oldVNode.props || {}; const newProps = newVNode.props || {}; if (isPropsChanged(oldProps, newProps)) { const oldPropsKeys = Object.keys(oldProps); const newPropsKeys = Object.keys(newProps); // 如果新節點沒有屬性,把舊的節點的屬性清除掉 if (newPropsKeys.length === 0) { oldPropsKeys.forEach(propKey => { removeProp($currentDom, propKey, oldProps[propKey]); }); } else { // 拿到所有的props,以此遍歷,增加/刪除/修改對應屬性 const allPropsKeys = new Set([...oldPropsKeys, ... newPropsKeys]); allPropsKeys.forEach(propKey => { // 屬性被去除了 if (!newProps[propKey]) { return removeProp($currentDom, propKey, oldProps[propKey]); } // 屬性改變了/增加了 if (newProps[propKey] !== oldProps[propKey]) { return setProp($currentDom, propKey, newProps[propKey]); } }); } }
上面的代碼也非常好理解,如果發現props改變了,那么對舊的props的每項去做遍歷。把不存在的屬性清除,再把新增加的屬性加入到更新后的 DOM 樹中:
首先,如果新的節點沒有屬性,遍歷刪除所有舊的節點的屬性,在這里,我們通過調用removeProp刪除。removeProp與setProp相對應,由于本文篇幅有限,筆者在這里就不做過多闡述;
function removeProp($target, name, value) { if (isCustomProp(name)) { return; } else if (name === "className") { // fix react className return $target.removeAttribute("class"); } else if (isEventProp(name)) { return $target.removeEventListener(extractEventName(name), value); } else if (typeof value === "boolean") { $target.removeAttribute(name); $target[name] = false; } else { $target.removeAttribute(name); } }
如果新節點有屬性,那么拿到舊節點和新節點所有屬性,遍歷新舊節點的所有屬性,如果屬性在新節點中沒有,那么說明該屬性被刪除了。如果新的節點與舊的節點屬性不一致/或者是新增的屬性,則調用setProp給真實 DOM 節點添加新的屬性。
更新子節點在最后,與toRealDom類似的是,在updateDom中,我們也應當處理所有子節點,對子節點進行遞歸調用updateDom,一個一個對比所有子節點的VNode是否有更新,一旦VNode有更新,則真實 DOM 也需要重新渲染:
// 根節點相同,但子節點不同,要遞歸對比子節點 if ( (oldNode.children && oldNode.children.length) || (newNode.children && newNode.children.length) ) { for (let i = 0; i < oldNode.children.length || i < newNode.children.length; i++) { updateDom($currentDom, oldNode.children[i], newNode.children[i], i); } }遠遠沒有結束
以上是筆者實現的最簡單的 Virtual DOM 代碼,但這與社區我們所用到 Virtual DOM 算法是有天壤之別的,筆者在這里舉個最簡單的例子:
對于上述代碼中實現的updateDom函數而言,更新前后的 DOM 結構如上所示,則會觸發五個li節點全部重新渲染,這顯然是一種性能的浪費。而snabbdom則通過移動節點的方式較好地解決了上述問題,由于本文篇幅有限,并且社區也有許多對該 Virtual DOM 算法的分析文章,筆者就不在本文做過多闡述了,有興趣的讀者可以到自行研究。筆者也基于本文實例,參考snabbdom算法實現了最終的版本,有興趣的讀者可以查看本文示例最終版
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/104537.html
摘要:可實際上并不是創造的,將這個概念拿過來以后融會貫通慢慢地成為目前前端最炙手可熱的框架之一。則是將再抽象一層生成的簡化版對象,這個對象也擁有上的一些屬性,比如等,但它是完全脫離于瀏覽器而存在的。所以今天我要手把手教大家怎么從零開始實現。 假如你的項目使用了React,你知道怎么做性能優化嗎?你知道為什么React讓你寫shouldComponentUpdate或者React.PureCo...
摘要:上集回顧從零開始手把手教你實現一個一上一集我們介紹了什么是,為什么要用,以及我們要怎樣來實現一個。完成后,在命令行中輸入安裝下依賴。最后返回這個目標節點。明天,我們迎接挑戰,開始處理數據變動引起的重新渲染,我們要如何新舊,生成補丁,修改。 上集回顧 從零開始手把手教你實現一個Virtual DOM(一)上一集我們介紹了什么是VDOM,為什么要用VDOM,以及我們要怎樣來實現一個VDOM...
摘要:模塊化是隨著前端技術的發展,前端代碼爆炸式增長后,工程化所采取的必然措施。目前模塊化的思想分為和。特別指出,事件不等同于異步,回調也不等同于異步。將會討論安全的類型檢測惰性載入函數凍結對象定時器等話題。 Vue.js 前后端同構方案之準備篇——代碼優化 目前 Vue.js 的火爆不亞于當初的 React,本人對寫代碼有潔癖,代碼也是藝術。此篇是準備篇,工欲善其事,必先利其器。我們先在代...
摘要:函數依次做了這幾件事調用函數,對比新舊兩個,根據兩者的不同得到需要修改的補丁將補丁到真實上當計數器小于等于的時候,將加,再繼續下一次當計數器大于的時候,結束下面我們來實現函數和函數。 上集回顧 【React進階系列】從零開始手把手教你實現一個Virtual DOM(二) 上集我們實現了首次渲染從JSX=>Hyperscript=>VDOM=>DOM的過程,今天我們來看一下當數據變動的時...
摘要:圖在中應用三數據渲染過程數據綁定實現邏輯本節正式分析從到數據渲染到頁面的過程,在中定義了一個的構造函數。一、概述 vue已是目前國內前端web端三分天下之一,也是工作中主要技術棧之一。在日常使用中知其然也好奇著所以然,因此嘗試閱讀vue源碼并進行總結。本文旨在梳理初始化頁面時data中的數據是如何渲染到頁面上的。本文將帶著這個疑問一點點追究vue的思路。總體來說vue模版渲染大致流程如圖1所...
閱讀 2060·2021-11-23 09:51
閱讀 3353·2021-09-28 09:36
閱讀 1120·2021-09-08 09:35
閱讀 1758·2021-07-23 10:23
閱讀 3258·2019-08-30 15:54
閱讀 2998·2019-08-29 17:05
閱讀 438·2019-08-29 13:23
閱讀 1294·2019-08-28 17:51