什么是虛擬DOM?如何實(shí)現(xiàn)一個虛擬DOM飞涂?說說你的思路

一旦部、什么是虛擬DOM

虛擬 DOM (Virtual DOM )這個概念相信大家都不陌生,從 ReactVue 封拧,虛擬 DOM 為這兩個框架都帶來了跨平臺的能力(React-NativeWeex

實(shí)際上它只是一層對真實(shí)DOM的抽象志鹃,以JavaScript 對象 (VNode 節(jié)點(diǎn)) 作為基礎(chǔ)的樹夭问,用對象的屬性來描述節(jié)點(diǎn)泽西,最終可以通過一系列操作使這棵樹映射到真實(shí)環(huán)境上

Javascript對象中,虛擬DOM 表現(xiàn)為一個 Object對象缰趋。并且最少包含標(biāo)簽名 (tag)捧杉、屬性 (attrs) 和子元素對象 (children) 三個屬性,不同框架對這三個屬性的名命可能會有差別

創(chuàng)建虛擬DOM就是為了更好將虛擬的節(jié)點(diǎn)渲染到頁面視圖中秘血,所以虛擬DOM對象的節(jié)點(diǎn)與真實(shí)DOM的屬性一一照應(yīng)

vue中同樣使用到了虛擬DOM技術(shù)

定義真實(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"
    }
})

觀察renderrender味抖,我們能得到虛擬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))])])}})

通過VNodevue可以對這顆抽象樹進(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)霞揉,哪怕一個最簡單的div也包含著很多屬性旬薯,可以打印出來直觀感受一下:

由此可見,操作DOM的代價仍舊是昂貴的适秩,頻繁操作還是會出現(xiàn)頁面卡頓绊序,影響用戶的體驗(yàn)

舉個例子:

你用傳統(tǒng)的原生apijQuery去操作DOM時,瀏覽器會從構(gòu)建DOM樹開始從頭到尾執(zhí)行一遍流程

當(dāng)你在一次操作時秽荞,需要更新10個DOM節(jié)點(diǎn)骤公,瀏覽器沒這么智能,收到第一個更新DOM請求后扬跋,并不知道后續(xù)還有9次更新操作淋样,因此會馬上執(zhí)行流程,最終執(zhí)行10次流程

而通過VNode胁住,同樣更新10個DOM節(jié)點(diǎn)趁猴,虛擬DOM不會立即操作DOM,而是將這10次更新的diff內(nèi)容保存到本地的一個js對象中彪见,最終將這個js對象一次性attachDOM樹上儡司,避免大量的無謂計算

很多人認(rèn)為虛擬 DOM 最大的優(yōu)勢是 diff 算法,減少 JavaScript 操作真實(shí) DOM 的帶來的性能消耗余指。雖然這一個虛擬 DOM 帶來的一個優(yōu)勢捕犬,但并不是全部。虛擬 DOM 最大的優(yōu)勢在于抽象了原本的渲染過程酵镜,實(shí)現(xiàn)了跨平臺的能力碉碉,而不僅僅局限于瀏覽器的 DOM,可以是安卓和 IOS 的原生組件淮韭,可以是近期很火熱的小程序垢粮,也可以是各種GUI

三、如何實(shí)現(xiàn)虛擬DOM

首先可以看看vueVNode的結(jié)構(gòu)

源碼位置:src/core/vdom/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ù)信息蜡吧,是一個VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息*/
    this.data = data
    /*當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)占键,是一個數(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的時候?yàn)閠rue君仆,textContent的時候?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

源碼位置:src/core/vdom/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個參數(shù):

  • context 表示 VNode 的上下文環(huán)境,是 Component 類型

  • tag 表示標(biāo)簽返咱,它可以是一個字符串氮帐,也可以是一個 Component

  • data 表示 VNode 的數(shù)據(jù),它是一個 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 的時候會產(chǎn)生嵌套數(shù)組

無論是simpleNormalizeChildren還是normalizeChildren都是對children進(jìn)行規(guī)范(使children 變成了一個類型為 VNodeArray),這里就不展開說了

規(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)建一個普通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í)例化一個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ù)
  • 實(shí)例化 vnode

小結(jié)

createElement 創(chuàng)建 VNode 的過程,每個 VNodechildren裹纳,children 每個元素也是一個VNode择葡,這樣就形成了一個虛擬樹結(jié)構(gòu),用于描述真實(shí)的DOM樹結(jié)構(gòu)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末剃氧,一起剝皮案震驚了整個濱河市敏储,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌朋鞍,老刑警劉巖已添,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異滥酥,居然都是意外死亡更舞,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門坎吻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缆蝉,“玉大人,你說我怎么就攤上這事禾怠》捣睿” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵吗氏,是天一觀的道長。 經(jīng)常有香客問我雷逆,道長弦讽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮往产,結(jié)果婚禮上被碗,老公的妹妹穿的比我還像新娘。我一直安慰自己仿村,他們只是感情好锐朴,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蔼囊,像睡著了一般焚志。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上畏鼓,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天酱酬,我揣著相機(jī)與錄音,去河邊找鬼云矫。 笑死膳沽,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的让禀。 我是一名探鬼主播挑社,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼巡揍!你這毒婦竟也來了滔灶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤吼肥,失蹤者是張志新(化名)和其女友劉穎录平,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缀皱,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡斗这,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了啤斗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片表箭。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖钮莲,靈堂內(nèi)的尸體忽然破棺而出免钻,到底是詐尸還是另有隱情,我是刑警寧澤崔拥,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布极舔,位于F島的核電站,受9級特大地震影響链瓦,放射性物質(zhì)發(fā)生泄漏拆魏。R本人自食惡果不足惜盯桦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望渤刃。 院中可真熱鬧拥峦,春花似錦、人聲如沸卖子。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽洋闽。三九已至玄柠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間喊递,已是汗流浹背随闪。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留骚勘,地道東北人铐伴。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像俏讹,于是被迫代替她去往敵國和親当宴。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容