Vue源碼閱讀(五):虛擬DOM的引入

接着前文,咱們詳細研究了數據初始化的過程,也瞭解了數據更新的幾個步驟。如今進入到詳細的update過程,這個過程涉及到虛擬DOM與更新DOM操做的patch算法。node

虛擬DOM

在現代UI結構設計中(統稱MV*框架,V是現代的標記語言),數據驅動已經成爲一個核心。而引入虛擬DOM,則是數據驅動的一種實現方式。虛擬DOM(Virtual DOM)是對DOM的JS抽象表示,它們是JS對象,可以描述DOM結構和關係。應用 的各類狀態變化會做用於虛擬DOM,最終映射到DOM上。工做流程圖以下:算法

屏幕快照 2019-11-23 下午7.42.48.png-21.6kB

優勢

  1. 虛擬DOM輕量、快速:當數據發生變化時,引起虛擬DOM的變化。經過新舊虛擬DOM比對能夠獲得最小DOM操做量(真實DOM操做很是昂貴),從而提高性能和用戶體驗。
  2. 跨平臺:將虛擬dom更新轉換爲不一樣運行時特殊操做實現跨平臺(Vue的源碼結構中就區分了Web平臺與Platform平臺)。
  3. 兼容性:還能夠加入兼容性代碼加強操做的兼容性。

Vue引入虛擬DOM的必要性

Vue 1.x中有細粒度的數據變化偵測,它是不須要虛擬DOM的,可是細粒度形成了大量開銷,這對於大 型項目來講是不可接受的。所以,Vue 2.x選擇了中等粒度的解決方案,每個組件一個Watcher實例, 這樣狀態變化時只能通知到組件,再經過引入虛擬DOM去進行比對和渲染。數組

能夠說,Vue2.x中引入虛擬DOM是必然的。設計結構發生了變化:組件與Watcher之間一一對應。這就要求必須引入虛擬DOM來應對該變化。bash

源碼

咱們先來看看Vue2.x中的虛擬DOM長啥樣。它的名字叫VNode:框架

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 key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal 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? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching devtoolsMeta: ?Object; // used to store functional render context for devtools fnScopeId: ?string; // functional scope id support 複製代碼

裏面的變量不少。可知這是一顆樹結構,children裏面是Array<VNode>。這是怎麼生成的呢?咱們回憶以前解讀源碼中的$mount過程,找一個切入點。dom

core/instance/lifecycle.js中的mountComponent()開始:async

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  ...//省略
  let updateComponent = () => {
      //更新 Component的定義,主要作了兩個事情:render(生成vdom)、update(轉換vdom爲dom)
      vm._update(vm._render(), hydrating)
  }

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
 ...//省略
}
複製代碼

看到在new Watcher實例時,與updateComponent建立了關聯。重點關注vm._render()函數

core/instance/render.js內:oop

import { createElement } from '../vdom/create-element'

export function initRender (vm: Component) {
  ...//省略
  //編譯器使用的render
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  //用戶編寫的render,典型的柯里化
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  
  ...//省略
}

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    ...//省略
    //從選項中獲取render函數
    const { render, _parentVnode } = vm.$options
    // 最終計算出的虛擬DOM
    let vnode
    // 執行render函數,傳入參數是$createElement (經常使用的render()方法中的h參數)
    let vnode = render.call(vm._renderProxy, vm.$createElement)
    ...//省略
    return vnode
  }
複製代碼

看到了VNode。這個過程執行了render函數,用到了createElement()方法。看來建立VNode的核心在這個方法裏面。關聯到core/vdom/create-element.js性能

//返回VNode或者由VNode構成的數組
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  ...//省略
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  ...//省略
  //核心: vnode的生成過程
  //傳入tag多是原生的HTML標籤,也多是用戶自定義標籤
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    //是原生保留標籤,直接建立VNode
    if (config.isReservedTag(tag)) {
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    }else if((!data||!data.pre)&&isDef(Ctor=resolveAsset(context.$options, 'components',tag))) {
      // 自定義組件,區別對待,須要先建立組件,再建立VNode
      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)
  }
  ...//省略
}
複製代碼

彙總

整個流程串起來,咱們看到:render函數經過調用createElement()方法,對不一樣傳入的參數類型進行加工,最終獲得了VNode樹。流程圖以下:

屏幕快照 2019-11-24 上午9.03.12.png-37kB

那麼新舊VNode之間如何比較變化,進而以最小代價執行真實DOM的更新呢?咱們下篇文章的patch算法將會講到。

相關文章
相關標籤/搜索