vue源碼分析系列三:render的執行過程和Virtual DOM的產生

render

手寫 render 函數,仔細觀察下面這段代碼,試想一下這裏的 createElement 參數是什麼 。

new Vue({
    el: '#application',
    render(createElement) {
        return createElement('div', {
            attrs: {
                id: 'app1' //注意這裏的id是app1了不是index.html中的application了 
            }
        }, this.value)
    },
    data() {
        return {
            value: 'render function'
        }
    }
})

頁面效果:
在這裏插入圖片描述
我們可以看到app1已經替換掉了application,所以我們爲什麼不能將元素綁定在body/html這些元素上就知道了吧,因爲它會替換掉頁面上的元素。

源碼分析:

Vue 的 _render 方法是實例的一個私有方法,它用來把實例渲染成一個虛擬 Node。它的定義在 src/core/instance/render.js 文件中:

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    // 下面這兩個 if 先不用看
    // reset _rendered flag on slots for duplicate slot check
    if (process.env.NODE_ENV !== 'production') {
      for (const key in vm.$slots) {
        // $flow-disable-line
        vm.$slots[key]._rendered = false
      }
    }

    if (_parentVnode) {
      vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
    }

    // 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 {
      // 分析主線
      // 從這裏我們可以看出,手寫render方法中的createElement參數就是 vm.$createElement方法
      vnode = render.call(vm._renderProxy, vm.$createElement)
      
      // 這個render 是 vm.$options.render
      // vm._renderProxy 如果在生產環境下,其實就是 vm 
      // 如果在開發環境下,就是 Proxy 對象(ES6中的API,不瞭解的話可以去看看)
      // vm.$createElement 定義在 initRender 函數中,初始化的時候定義的
      // 手寫 render 函數創建 vnode 的方法
      // vm.$createElement = function (a, b, c, d) { 
      //    return createElement(vm, a, b, c, d, true);
      // };
      
      // 如果是編譯生成的render函數,創建vnode的方法則是下面這個方法
      // vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
      
    } 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') {
        if (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
        }
      } else {
        vnode = vm._vnode
      }
    }
    // 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
  }
}

Flowfacebook 出品的 JavaScript 靜態類型檢查工具。Vue.js 的源碼利用了 Flow 做了靜態類型檢查,function (): VNode表示這個方法的返回值是一個 vnode
再回到 _render 函數中的 render 方法的調用:

vnode = render.call(vm._renderProxy, vm.$createElement)

可以看到,render 函數中的 createElement 方法就是 vm.$createElement 方法:

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 方法定義是在執行 initRender 方法的時候,可以看到除了 vm.$createElement 方法,還有一個 vm._c 方法,它是被模板編譯成的 render 函數使用,而 vm.$createElement 是用戶手寫 render 方法使用的, 這倆個方法支持的參數相同,並且內部都調用了 createElement 方法。

vm._render 最終是通過執行 createElement 方法並返回的是 vnode,它是一個虛擬 Node。Vue 2.0 相比 Vue 1.0 最大的升級就是利用了 Virtual DOM。因此在分析 createElement 的實現前,我們先了解一下 Virtual DOM 的概念。

Virtual DOM

Virtual DOM 這個概念相信大部分人都不會陌生,它產生的前提是瀏覽器中的 DOM 是很“昂貴"的,爲了更直觀的感受,我們可以簡單的把一個簡單的 div 元素的屬性都打印出來,如圖所示:
在這裏插入圖片描述
可以看到,真正的 DOM 元素是非常龐大的,因爲瀏覽器的標準就把 DOM 設計的非常複雜。當我們頻繁的去做 DOM 更新,會產生一定的性能問題。

Virtual DOM 就是用一個原生的 JS 對象去描述一個 DOM 節點,所以它比創建一個 DOM 的代價要小很多。在 Vue.js 中,Virtual DOM 是用 VNode 這麼一個 Class 去描述,它是定義在 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
  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
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

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

可以看到 Vue.js 中的 Virtual DOM 的定義還是略微複雜一些的,因爲它這裏包含了很多 Vue.js 的特性。這裏千萬不要被這些茫茫多的屬性嚇到,實際上 Vue.jsVirtual DOM 是借鑑了一個開源庫 snabbdom 的實現,然後加入了一些 Vue.js 特色的東西。我建議大家如果想深入瞭解 Vue.jsVirtual DOM 前不妨先閱讀這個庫的源碼,因爲它更加簡單和純粹。

總結

其實 VNode 是對真實 DOM 的一種抽象描述,它的核心定義無非就幾個關鍵屬性,標籤名、數據、子節點、鍵值等,其它屬性都是都是用來擴展 VNode 的靈活性以及實現一些特殊 feature 的。由於 VNode 只是用來映射到真實 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常輕量和簡單的。

Virtual DOM 除了它的數據結構的定義,映射到真實的 DOM 實際上要經歷 VNodecreate、diff、patch 等過程。那麼在 Vue.js 中,VNodecreate 是通過之前提到的 createElement 方法創建的,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章