Vue中的虛擬dom分享
一、什么是虛擬DOM
通過js創(chuàng)建一個(gè)Object對象來模擬真實(shí)DOM結(jié)構(gòu),這個(gè)對象包含標(biāo)簽名 (tag)、屬性 (attrs) 和子元素對象 (children) 三個(gè)屬性,通過vue中的render()函數(shù)把虛擬dom編譯成真實(shí)dom,在通過appendChild()添加到頁面中。
先來看看下面的這兩個(gè)例子
- 第一個(gè):
class Element { constructor(tagName, attrs, children) { this.tagName = tagName; this.attrs = attrs || {}; this.children = children || []; } render() { //這個(gè)函數(shù)是用來生成真實(shí)DOM的,最后會把return的結(jié)果添加到頁面中去 } }
- 第二個(gè):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <ul id="list"> <li class="a">txt_a</li> <li class="a">txt_b</li> </ul> </body> <script> //創(chuàng)建Virtual Dom Tree let ul = Element('ul', { id: 'list'}, [ Element('li', {class: 'a'}, ['txt_a']), Element('li', {class: 'b'}, ['txt_b']) ]); let ulDom=ul.render(); //生成真實(shí)Dom document.body.appendChild(ulDom) </script> </html>
創(chuàng)建虛擬DOM就是為了更好將虛擬的節(jié)點(diǎn)渲染到頁面視圖中,所以虛擬DOM對象的節(jié)點(diǎn)與真實(shí)DOM的屬性一一照應(yīng)
在vue中如何應(yīng)用虛擬DOM的
- 定義真實(shí)DOM
<div id="app"> <p class="p">節(jié)點(diǎn)內(nèi)容</p> <h3>{{ foo }}</h3> </div>
- 實(shí)例化vue
const app = new Vue({ el:"#app", data:{ foo:"foo" } })
- vue源碼中render函數(shù)渲染虛擬DOM
(function anonymous() { with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',{staticClass:"p"}, [_v("節(jié)點(diǎn)內(nèi)容")]),_v(" "),_c('h3',[_v(_s(foo))])])}})
通過VNode,vue可以對這顆抽象樹進(jìn)行創(chuàng)建節(jié)點(diǎn),刪除節(jié)點(diǎn)以及修改節(jié)點(diǎn)的操作,經(jīng)過diff算法得出一些需要修改的最小單位,再更新視圖,減少了dom操作,提高了性能
二、為什么需要虛擬DOM
DOM是很慢的,其元素非常龐大,頁面的性能問題,大部分都是由DOM操作引起的
真實(shí)的DOM節(jié)點(diǎn),哪怕一個(gè)最簡單的div也包含著很多屬性,可以打印出來直觀感受一下:
由此可見,操作DOM的代價(jià)仍舊是昂貴的,頻繁操作還是會出現(xiàn)頁面卡頓,影響用戶的體驗(yàn)。
舉個(gè)例子
你用傳統(tǒng)的原生api或jQuery去操作DOM時(shí),瀏覽器會從構(gòu)建DOM樹開始從頭到尾執(zhí)行一遍流程
當(dāng)你在一次操作時(shí),需要更新10個(gè)DOM節(jié)點(diǎn),瀏覽器沒這么智能,收到第一個(gè)更新DOM請求后,并不知道后續(xù)還有9次更新操作,因此會馬上執(zhí)行流程,最終執(zhí)行10次流程
而通過VNode,同樣更新10個(gè)DOM節(jié)點(diǎn),虛擬DOM不會立即操作DOM,而是將這10次更新的diff內(nèi)容保存到本地的一個(gè)js對象中,最終將這個(gè)js對象一次性attach到DOM樹上,避免大量的無謂計(jì)算
- vue中虛擬 DOM 最大的優(yōu)勢是 diff 算法,減少 JavaScript 操作真實(shí) DOM 的帶來的性能消耗。雖然這一個(gè)虛擬 DOM帶來的一個(gè)優(yōu)勢,但并不是全部。
- 其它框架中也有虛擬 DOM概念, 最大的優(yōu)勢在于抽象了原本的渲染過程,實(shí)現(xiàn)了跨平臺的能力,而不僅僅局限于瀏覽器的 DOM,可以是安卓和 IOS 的原生組件,可以是近期很火熱的小程序,也可以是各種GUI。
三、如何實(shí)現(xiàn)虛擬DOM
首先可以看看vue中VNode的結(jié)構(gòu)
- vnode.js
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope functionalContext: Component | void; // only for functional component root nodes key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions ) { /*當(dāng)前節(jié)點(diǎn)的標(biāo)簽名*/ this.tag = tag /*當(dāng)前節(jié)點(diǎn)對應(yīng)的對象,包含了具體的一些數(shù)據(jù)信息,是一個(gè)VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息*/ this.data = data /*當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn),是一個(gè)數(shù)組*/ this.children = children /*當(dāng)前節(jié)點(diǎn)的文本*/ this.text = text /*當(dāng)前虛擬節(jié)點(diǎn)對應(yīng)的真實(shí)dom節(jié)點(diǎn)*/ this.elm = elm /*當(dāng)前節(jié)點(diǎn)的名字空間*/ this.ns = undefined /*編譯作用域*/ this.context = context /*函數(shù)化組件作用域*/ this.functionalContext = undefined /*節(jié)點(diǎn)的key屬性,被當(dāng)作節(jié)點(diǎn)的標(biāo)志,用以優(yōu)化*/ this.key = data && data.key /*組件的option選項(xiàng)*/ this.componentOptions = componentOptions /*當(dāng)前節(jié)點(diǎn)對應(yīng)的組件的實(shí)例*/ this.componentInstance = undefined /*當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)*/ this.parent = undefined /*簡而言之就是是否為原生HTML或只是普通文本,innerHTML的時(shí)候?yàn)閠rue,textContent的時(shí)候?yàn)閒alse*/ this.raw = false /*靜態(tài)節(jié)點(diǎn)標(biāo)志*/ this.isStatic = false /*是否作為跟節(jié)點(diǎn)插入*/ this.isRootInsert = true /*是否為注釋節(jié)點(diǎn)*/ this.isComment = false /*是否為克隆節(jié)點(diǎn)*/ this.isCloned = false /*是否有v-once指令*/ this.isOnce = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next https://github.com/answershuto/learnVue*/ get child (): Component | void { return this.componentInstance } }
這里對VNode進(jìn)行稍微的說明:
- 所有對象的 context 選項(xiàng)都指向了 Vue 實(shí)例
- elm 屬性則指向了其相對應(yīng)的真實(shí) DOM 節(jié)點(diǎn)vue是通過createElement生成VNode
源碼create-element.js
export function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode | Array<VNode> { if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE } return _createElement(context, tag, data, children, normalizationType) }
上面可以看到createElement 方法實(shí)際上是對 _createElement 方法的封裝,對參數(shù)的傳入進(jìn)行了判斷
export function _createElement( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { if (isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV !== 'production' && warn( `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` + 'Always create fresh vnode data objects in each render!', context` ) return createEmptyVNode() } // object syntax in v-bind if (isDef(data) && isDef(data.is)) { tag = data.is } if (!tag) { // in case of component :is set to falsy value return createEmptyVNode() } // support single function children as default scoped slot if (Array.isArray(children) && typeof children[0] === 'function' ) { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if ( === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } // 創(chuàng)建VNode }
可以看到_createElement接收5個(gè)參數(shù):
- context 表示 VNode 的上下文環(huán)境,是 Component 類型
- tag 表示標(biāo)簽,它可以是一個(gè)字符串,也可以是一個(gè) Component
- data 表示 VNode 的數(shù)據(jù),它是一個(gè) VNodeData 類型
- children 表示當(dāng)前 VNode的子節(jié)點(diǎn),它是任意類型的
- normalizationType 表示子節(jié)點(diǎn)規(guī)范的類型,類型不同規(guī)范的方法也就不一樣,主要是參考 render 函數(shù)是編譯生成的還是用戶手寫的
根據(jù)normalizationType 的類型,children會有不同的定義
if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if ( === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) }
simpleNormalizeChildren方法調(diào)用場景是 render 函數(shù)是編譯生成的
normalizeChildren方法調(diào)用場景分為下面兩種:
render 函數(shù)是用戶手寫的
編譯 slot、v-for 的時(shí)候會產(chǎn)生嵌套數(shù)組
無論是simpleNormalizeChildren還是normalizeChildren都是對children進(jìn)行規(guī)范(使children 變成了一個(gè)類型為 VNode 的 Array),這里就不展開說了
規(guī)范化children的源碼位置在:src/core/vdom/helpers/normalzie-children.js
在規(guī)范化children后,就去創(chuàng)建VNode
let vnode, ns // 對tag進(jìn)行判斷 if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // 如果是內(nèi)置的節(jié)點(diǎn),則直接創(chuàng)建一個(gè)普通VNode vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component // 如果是component類型,則會通過createComponent創(chuàng)建VNode節(jié)點(diǎn) vnode = createComponent(Ctor, data, context, children, tag) } else { vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) }
createComponent同樣是創(chuàng)建VNode
源碼位置:src/core/vdom/create-component.js
export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { if (isUndef(Ctor)) { return } // 構(gòu)建子類構(gòu)造函數(shù) const baseCtor = context.$options._base // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) } // if at this stage it's not a constructor or an async component factory, // reject. if (typeof Ctor !== 'function') { if (process.env.NODE_ENV !== 'production') { warn(`Invalid Component definition: ${String(Ctor)}`, context) } return } // async component let asyncFactory if (isUndef(Ctor.cid)) { asyncFactory = Ctor Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context) if (Ctor === undefined) { return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } } data = data || {} // resolve constructor options in case global mixins are applied after // component constructor creation resolveConstructorOptions(Ctor) // transform component v-model data into props & events if (isDef(data.model)) { transformModel(Ctor.options, data) } // extract props const propsData = extractPropsFromVNodeData(data, Ctor, tag) // functional component if (isTrue(Ctor.options.functional)) { return createFunctionalComponent(Ctor, propsData, data, context, children) } // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) { const slot = data.slot data = {} if (slot) { data.slot = slot } } // 安裝組件鉤子函數(shù),把鉤子函數(shù)合并到data.hook中 installComponentHooks(data) //實(shí)例化一個(gè)VNode返回。組件的VNode是沒有children的 const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) if (__WEEX__ && isRecyclableComponent(vnode)) { return renderRecyclableComponentTemplate(vnode) } return vnode }
這里在稍微填一下createComponent生成VNode的三個(gè)關(guān)鍵流程:
- 構(gòu)造子類構(gòu)造函數(shù)Ctor
- installComponentHooks安裝組件鉤子函數(shù)
- 實(shí)例化 vnode
心得:
- createElement 創(chuàng)建 VNode 的過程,每個(gè) VNode 有 children,children
- 每個(gè)元素也是一個(gè)VNode,這樣就形成了一個(gè)虛擬樹結(jié)構(gòu),用于描述真實(shí)的DOM樹結(jié)構(gòu)
理論
想要理解虛擬dom首先要知道什么是虛擬dom?
虛擬dom可以簡單的用一句話概括,就是用普通的js對象來描述DOM結(jié)構(gòu),因?yàn)椴皇钦鎸?shí)DOM,所以稱之為虛擬DOM。
為什么要用虛擬DOM來描述真實(shí)的DOM呢?
創(chuàng)建真實(shí)DOM成本比較高,如果用 js對象來描述一個(gè)dom節(jié)點(diǎn),成本比較低,另外我們在頻繁操作dom是一種比較大的開銷。所以建議用虛擬dom來描述真實(shí)dom。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
vue.js動(dòng)畫中的js鉤子函數(shù)的實(shí)現(xiàn)
這篇文章主要介紹了vue.js動(dòng)畫中的js鉤子函數(shù)的實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-07-07vue中axios利用blob實(shí)現(xiàn)文件瀏覽器下載方式
這篇文章主要介紹了vue中axios利用blob實(shí)現(xiàn)文件瀏覽器下載方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05Vue 開發(fā)音樂播放器之歌手頁右側(cè)快速入口功能
這篇文章主要介紹了Vue 開發(fā)音樂播放器之歌手頁右側(cè)快速入口功能,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-08-08Vue腳手架搭建及創(chuàng)建Vue項(xiàng)目流程的詳細(xì)教程
Vue腳手架指的是vue-cli,它是一個(gè)快速構(gòu)建**單頁面應(yīng)用程序(SPA)**環(huán)境配置的工具,cli是(command-line-interfac)命令行界面,下面這篇文章主要給大家介紹了關(guān)于Vue腳手架搭建及創(chuàng)建Vue項(xiàng)目流程的相關(guān)資料,需要的朋友可以參考下2022-09-09Vue3 axios配置以及cookie的使用方法實(shí)例演示
這篇文章主要介紹了Vue3 axios配置以及cookie的使用方法,需要的朋友可以參考下2023-02-02