深刻vue -- Virtual Dom是如何被建立的

本文將經過解讀render函數的源碼,來分析vue中的vNode是如何建立的。在vue2.x的版本中,不管是直接書寫render函數,仍是使用template或el屬性,或是使用.vue單文件的形式,最終都須要編譯成render函數進行vnode的建立,最終再渲染成真實的DOM。 若是對vue源碼的目錄還不是很瞭解,推薦先閱讀下 深刻vue -- 源碼目錄和編譯過程javascript

render函數

render方法定義在文件 src/core/instance/render.js 中html

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    // ... 
    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }
}
複製代碼

_render定義在vue的原型上,會返回vnode,vnode經過代碼render.call(vm._renderProxy, vm.$createElement)進行建立。
在建立vnode過程當中,若是出現錯誤,就會執行catch中代碼作降級處理。
_render中最核心的代碼就是vue

vnode = render.call(vm._renderProxy, vm.$createElement)
複製代碼

接下來,分析下這裏的render,vm._renderProxy,vm.$createElement分別是什麼java

render函數

const { render, _parentVnode } = vm.$options
render方法是從$options中提取的。render方法有兩種途徑得來node

  1. 在組件中開發者直接手寫的render函數
  2. 經過編譯template屬性生成

參數 vm._renderProxy

vm._renderProxy定義在 src/core/instance/init.js 中,是call的第一個參數,指定render函數執行的上下文。es6

/* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
複製代碼
生產環境

vm._renderProxy = vm,也就是說,在生產環境,render函數執行的上下文就是當前vue實例,即當前組件的this。數組

開發環境

開發環境會執行initProxy(vm),initProxy定義在文件 src/core/instance/proxy.js 中。瀏覽器

let initProxy
  // ...
  initProxy = function initProxy (vm) {
    if (hasProxy) {
      // determine which proxy handler to use
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }
}
複製代碼

hasProxy的定義以下bash

const hasProxy =
    typeof Proxy !== 'undefined' && isNative(Proxy)
複製代碼

用來判斷瀏覽器是否支持es6的Proxy。app

Proxy做用是在訪問一個對象時,對其進行攔截,new Proxy的第一個參數表示所要攔截的對象,第二個參數是用來定製攔截行爲的對象。

開發環境,若是支持Proxy就會對vm實例進行攔截,不然和生產環境相同,直接將vm賦值給vm._renderProxy。具體的攔截行爲經過handlers對象指定。
當手寫render函數時,handlers = hasHandler,經過template生成的render函數,handlers = getHandler。 hasHandler代碼:

const hasHandler = {
    has (target, key) {
      const has = key in target
      const isAllowed = allowedGlobals(key) ||
        (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
      if (!has && !isAllowed) {
        if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      return has || !isAllowed
    }
  }
複製代碼

getHandler代碼

const getHandler = {
    get (target, key) {
      if (typeof key === 'string' && !(key in target)) {
        if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      return target[key]
    }
  }
複製代碼

hasHandler,getHandler分別是對vm對象的屬性的讀取和propKey in proxy的操做進行攔截,並對vm的參數進行校驗,再調用 warnNonPresent 和 warnReservedPrefix 進行Warn警告。
可見,initProxy方法的主要做用就是在開發時,對vm實例進行攔截髮現問題並拋出錯誤,方便開發者及時修改問題。

參數 vm.$createElement

vm.createElement是render函數的參數,手寫render函數時,這裏的vm.createElement就是使用render時傳入的createElement函數,它定義在initRender方法中,initRender在new Vue初始化時執行,參數是實例vm。

export function initRender (vm: Component) {
  // ...
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  // ...
}
複製代碼

從代碼的註釋能夠看出: vm.$createElement是爲開發者手寫render函數提供的方法,vm._c是爲經過編譯template生成的render函數使用的方法。它們都會調用createElement方法。

createElement方法

createElement方法定義在 src/core/vdom/create-element.js 文件中

const SIMPLE_NORMALIZE = 1
  const ALWAYS_NORMALIZE = 2
// wrapper function for providing a more flexible interface
// without getting yelled at by flow
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方法建立vnode。
下面看一下vue文檔中createElement能接收的參數。

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一個 HTML 標籤字符串,組件選項對象,或者
  // 解析上述任何一種的一個 async 異步函數。必需參數。
  'div',

  // {Object}
  // 一個包含模板相關屬性的數據對象
  // 你能夠在 template 中使用這些特性。可選參數。
  {
  },

  // {String | Array}
  // 子虛擬節點 (VNodes),由 `createElement()` 構建而成,
  // 也可使用字符串來生成「文本虛擬節點」。可選參數。
  [
    '先寫一些文字',
    createElement('h1', '一則頭條'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)
複製代碼

文檔中除了第一個參數是必選參數,其餘都是可選參數。也就是說使用createElement方法的時候,能夠不傳第二個參數,只傳第一個參數和第三個參數。剛剛說的參數處理就是對這種狀況作處理。

if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
複製代碼

經過判斷data是不是數組或者是基礎類型,若是知足這個條件,說明這個位置傳的參數是children,而後對參數依次從新賦值。這種方式被稱爲重載。

重載:函數名相同,函數的參數列表不一樣(包括參數個數和參數類型),至於返回類型可同可不一樣。

處理好參數後調用_createElement方法建立vnode。下面是_createElement方法的核心代碼。

export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> {
  // ...
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    // ...
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
複製代碼

方法開始會作判斷,若是data是響應式的數據,component的is屬性不是真值的時候,都會去調用createEmptyVNode方法,建立一個空的vnode。 接下來,根據normalizationType的值,調用normalizeChildren或simpleNormalizeChildren方法對參數children進行處理。這兩個方法定義在 src/core/vdom/helpers/normalize-children.js 文件下。

// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}
複製代碼

normalizeChildren和simpleNormalizeChildren的目的都是將children數組扁平化處理,最終返回一個vnode的一維數組。
simpleNormalizeChildren是針對函數式組件作處理,因此只須要考慮children是二維數組的狀況。 normalizeChildren方法會考慮children是多層嵌套的數組的狀況。normalizeChildren開始會判斷children的類型,若是children是基礎類型,直接建立文本vnode,若是是數組,調用normalizeArrayChildren方法,並在normalizeArrayChildren方法裏面進行遞歸調用,最終將children轉成一維數組。
接下來,繼續看_createElement方法,若是tag參數的類型不是String類型,是組件的話,調用createComponent建立vnode。若是tag是String類型,再去判斷tag是不是html的保留標籤,是不是不認識的節點,經過調用new VNode(),傳入不一樣的參數來建立vnode實例。

不管是哪一種狀況,最終都是經過VNode這個class來建立vnode,下面是類VNode的源碼,在文件 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
  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

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag // 標籤名
    this.data = data // 當前節點數據
    this.children = children // 子節點
    this.text = text // 文本
    this.elm = elm // 對應的真實DOM節點
    this.ns = undefined // 命名空間
    this.context = context // 當前節點上下文
    this.fnContext = undefined // 函數化組件上下文
    this.fnOptions = undefined // 函數化組件配置參數
    this.fnScopeId = undefined // 函數化組件ScopeId
    this.key = data && data.key // 子節點key屬性
    this.componentOptions = componentOptions // 組件配置項 
    this.componentInstance = undefined // 組件實例
    this.parent = undefined // 父節點
    this.raw = false // 是不是原生的HTML片斷或只是普通文本
    this.isStatic = false // 靜態節點標記
    this.isRootInsert = true // 是否做爲根節點插入
    this.isComment = false // 是否爲註釋節點
    this.isCloned = false // 是否爲克隆節點
    this.isOnce = false // 是否有v-once指令
    this.asyncFactory = asyncFactory // 異步工廠方法 
    this.asyncMeta = undefined // 異步Meta
    this.isAsyncPlaceholder = false // 是否異步佔位
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}
複製代碼

VNode類定義的數據,都是用來描述VNode的。
至此,render函數建立vdom的源碼就分析完了,咱們簡單的總結梳理一下。

_render 定義在 Vue.prototype 上,_render函數執行會調用方法render,在開發環境下,會對vm實例進行代理,校驗vm實例數據正確性。render函數內,會執行render的參數createElement方法,createElement會對參數進行處理,處理參數後調用_createElement, _createElement方法內部最終會直接或間接調用new VNode(), 建立vnode實例。

vnode && vdom

createElement 返回的vnode並非真正的dom元素,VNode的全稱叫作「虛擬節點 (Virtual Node)」,它所包含的信息會告訴 Vue 頁面上須要渲染什麼樣的節點,及其子節點。咱們常說的「虛擬 DOM(Virtual Dom)」是對由 Vue 組件樹創建起來的整個 VNode 樹的稱呼。

心得

讀源碼切忌只看源碼,必定要結合具體的使用一塊兒分析,這樣才能更清楚的瞭解某段代碼的意圖。就像本文render函數,若是歷來沒有使用過render函數,直接就閱讀這塊源碼可能會比較吃力,不妨先看看文檔,寫個demo,看看具體的使用,再對照使用來分析源碼,這樣不少比較困惑的問題就迎刃而解了。

本文是本身閱讀源碼的一些理解,若是有不對的地方,感謝指出。

相關文章
相關標籤/搜索