Vue中的虛擬dom分享
一、什么是虛擬DOM
通過js創(chuàng)建一個Object對象來模擬真實DOM結(jié)構(gòu),這個對象包含標簽名 (tag)、屬性 (attrs) 和子元素對象 (children) 三個屬性,通過vue中的render()函數(shù)把虛擬dom編譯成真實dom,在通過appendChild()添加到頁面中。
先來看看下面的這兩個例子
- 第一個:
class Element {
constructor(tagName, attrs, children) {
this.tagName = tagName;
this.attrs = attrs || {};
this.children = children || [];
}
render() {
//這個函數(shù)是用來生成真實DOM的,最后會把return的結(jié)果添加到頁面中去
}
}- 第二個:
<!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(); //生成真實Dom
document.body.appendChild(ulDom)
</script>
</html>創(chuàng)建虛擬DOM就是為了更好將虛擬的節(jié)點渲染到頁面視圖中,所以虛擬DOM對象的節(jié)點與真實DOM的屬性一一照應(yīng)
在vue中如何應(yīng)用虛擬DOM的
- 定義真實DOM
<div id="app">
<p class="p">節(jié)點內(nèi)容</p>
<h3>{{ foo }}</h3>
</div>- 實例化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é)點內(nèi)容")]),_v(" "),_c('h3',[_v(_s(foo))])])}})通過VNode,vue可以對這顆抽象樹進行創(chuàng)建節(jié)點,刪除節(jié)點以及修改節(jié)點的操作,經(jīng)過diff算法得出一些需要修改的最小單位,再更新視圖,減少了dom操作,提高了性能
二、為什么需要虛擬DOM
DOM是很慢的,其元素非常龐大,頁面的性能問題,大部分都是由DOM操作引起的
真實的DOM節(jié)點,哪怕一個最簡單的div也包含著很多屬性,可以打印出來直觀感受一下:

由此可見,操作DOM的代價仍舊是昂貴的,頻繁操作還是會出現(xiàn)頁面卡頓,影響用戶的體驗。
舉個例子
你用傳統(tǒng)的原生api或jQuery去操作DOM時,瀏覽器會從構(gòu)建DOM樹開始從頭到尾執(zhí)行一遍流程
當你在一次操作時,需要更新10個DOM節(jié)點,瀏覽器沒這么智能,收到第一個更新DOM請求后,并不知道后續(xù)還有9次更新操作,因此會馬上執(zhí)行流程,最終執(zhí)行10次流程
而通過VNode,同樣更新10個DOM節(jié)點,虛擬DOM不會立即操作DOM,而是將這10次更新的diff內(nèi)容保存到本地的一個js對象中,最終將這個js對象一次性attach到DOM樹上,避免大量的無謂計算
- vue中虛擬 DOM 最大的優(yōu)勢是 diff 算法,減少 JavaScript 操作真實 DOM 的帶來的性能消耗。雖然這一個虛擬 DOM帶來的一個優(yōu)勢,但并不是全部。
- 其它框架中也有虛擬 DOM概念, 最大的優(yōu)勢在于抽象了原本的渲染過程,實現(xiàn)了跨平臺的能力,而不僅僅局限于瀏覽器的 DOM,可以是安卓和 IOS 的原生組件,可以是近期很火熱的小程序,也可以是各種GUI。
三、如何實現(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
) {
/*當前節(jié)點的標簽名*/
this.tag = tag
/*當前節(jié)點對應(yīng)的對象,包含了具體的一些數(shù)據(jù)信息,是一個VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息*/
this.data = data
/*當前節(jié)點的子節(jié)點,是一個數(shù)組*/
this.children = children
/*當前節(jié)點的文本*/
this.text = text
/*當前虛擬節(jié)點對應(yīng)的真實dom節(jié)點*/
this.elm = elm
/*當前節(jié)點的名字空間*/
this.ns = undefined
/*編譯作用域*/
this.context = context
/*函數(shù)化組件作用域*/
this.functionalContext = undefined
/*節(jié)點的key屬性,被當作節(jié)點的標志,用以優(yōu)化*/
this.key = data && data.key
/*組件的option選項*/
this.componentOptions = componentOptions
/*當前節(jié)點對應(yīng)的組件的實例*/
this.componentInstance = undefined
/*當前節(jié)點的父節(jié)點*/
this.parent = undefined
/*簡而言之就是是否為原生HTML或只是普通文本,innerHTML的時候為true,textContent的時候為false*/
this.raw = false
/*靜態(tài)節(jié)點標志*/
this.isStatic = false
/*是否作為跟節(jié)點插入*/
this.isRootInsert = true
/*是否為注釋節(jié)點*/
this.isComment = false
/*是否為克隆節(jié)點*/
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進行稍微的說明:
- 所有對象的 context 選項都指向了 Vue 實例
- elm 屬性則指向了其相對應(yīng)的真實 DOM 節(jié)點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 方法實際上是對 _createElement 方法的封裝,對參數(shù)的傳入進行了判斷
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個參數(shù):
- context 表示 VNode 的上下文環(huán)境,是 Component 類型
- tag 表示標簽,它可以是一個字符串,也可以是一個 Component
- data 表示 VNode 的數(shù)據(jù),它是一個 VNodeData 類型
- children 表示當前 VNode的子節(jié)點,它是任意類型的
- normalizationType 表示子節(jié)點規(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 的時候會產(chǎn)生嵌套數(shù)組
無論是simpleNormalizeChildren還是normalizeChildren都是對children進行規(guī)范(使children 變成了一個類型為 VNode 的 Array),這里就不展開說了
規(guī)范化children的源碼位置在:src/core/vdom/helpers/normalzie-children.js
在規(guī)范化children后,就去創(chuàng)建VNode
let vnode, ns
// 對tag進行判斷
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// 如果是內(nèi)置的節(jié)點,則直接創(chuàng)建一個普通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é)點
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)
//實例化一個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的三個關(guān)鍵流程:
- 構(gòu)造子類構(gòu)造函數(shù)Ctor
- installComponentHooks安裝組件鉤子函數(shù)
- 實例化 vnode
心得:
- createElement 創(chuàng)建 VNode 的過程,每個 VNode 有 children,children
- 每個元素也是一個VNode,這樣就形成了一個虛擬樹結(jié)構(gòu),用于描述真實的DOM樹結(jié)構(gòu)
理論
想要理解虛擬dom首先要知道什么是虛擬dom?
虛擬dom可以簡單的用一句話概括,就是用普通的js對象來描述DOM結(jié)構(gòu),因為不是真實DOM,所以稱之為虛擬DOM。
為什么要用虛擬DOM來描述真實的DOM呢?
創(chuàng)建真實DOM成本比較高,如果用 js對象來描述一個dom節(jié)點,成本比較低,另外我們在頻繁操作dom是一種比較大的開銷。所以建議用虛擬dom來描述真實dom。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
vue.js動畫中的js鉤子函數(shù)的實現(xiàn)
這篇文章主要介紹了vue.js動畫中的js鉤子函數(shù)的實現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-07-07
vue中axios利用blob實現(xiàn)文件瀏覽器下載方式
這篇文章主要介紹了vue中axios利用blob實現(xiàn)文件瀏覽器下載方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05
Vue 開發(fā)音樂播放器之歌手頁右側(cè)快速入口功能
這篇文章主要介紹了Vue 開發(fā)音樂播放器之歌手頁右側(cè)快速入口功能,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2018-08-08
Vue腳手架搭建及創(chuàng)建Vue項目流程的詳細教程
Vue腳手架指的是vue-cli,它是一個快速構(gòu)建**單頁面應(yīng)用程序(SPA)**環(huán)境配置的工具,cli是(command-line-interfac)命令行界面,下面這篇文章主要給大家介紹了關(guān)于Vue腳手架搭建及創(chuàng)建Vue項目流程的相關(guān)資料,需要的朋友可以參考下2022-09-09

