VueJS 的編譯階段到掛載節點

老臘肉學長發表於2018-10-20

概述

為了實現響應式模式,Vue用render函式來生成vnode,並使用diff演算法對比新舊vnode,最後更新到真實DOM上。

由於是在編譯階段而不是在監聽階段,所以vnode沒有對比的物件,直接通過vnode生成真實DOM。

Vnode是Vdom上的一個節點,是對真實DOM的抽象,在Vue中,我們可以通過對比新舊Vnode和Vdom來得到需要更新真實DOM的操作,並通過Vue框架來執行這些操作。於是我們可以把更多的精力投放到業務邏輯上。

編譯階段

該階段會解析template,把template轉化為render函式會經過三個過程:

  1. parse,將 template 模板中進行字串解析,得到指令、class、style等資料,形成 AST
  2. optimize,這個階段用於優化patch階段,標記節點的 static 屬性是否是靜態的
  3. generate,將 AST 轉化成 render funtion 字串,最終得到 render 的字串以及 staticRenderFns 字串

如果使用vue-cli工具的話,藉助webpack可以在打包過程中把template轉化為render函式和staticRenderFns函式

render 的字串與render 函式的關係

render函式內部包含render字串:

function render(vm) {
  with(vm) {
    eval(render_string)
  }
}
複製程式碼

掛載節點

Vue例項化的最後一步就是掛載節點。該階段會分為兩步:

  1. 通過render函式獲得vnode
  2. 通過傳入vnode給patch函式生成真實DOM並掛載到頁面上

render函式被執行時機

那麼render函式在什麼時候會被再次執行呢?

在解釋VueJS 響應式原理的時候有提到過,Render-Watcher例項的getter就是執行render函式的:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
複製程式碼

所以,render函式會被執行的時機有:

  1. Vue初始化的時候,會執行一次
  2. 當template(模板)中需要觀察的資料物件更新值的時候,也會觸發render函式(render-watcher)執行

render函式的關鍵是_createElement,負責返回VNode,它會根據標籤名是否存在已註冊的元件中,返回普通VNode或是元件VNode:

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
    // .......
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if (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()
  }
}
複製程式碼

patch函式執行時機

和render函式的一樣,因為patch函式就在vm._update(vm._render(), hydrating)中的_update裡。

  1. 在Vue初始化的時候,會生成真實DOM並掛載到document上
  2. 當template(模板)中需要觀察的資料物件更新值的時候,會對比新舊vnode,並返回新vnode對應的真實DOM
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = vm
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // 初始化渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // 更新渲染
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  activeInstance = prevActiveInstance
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}
複製程式碼

更新的patch函式的核心是diff演算法,類似git的diff指令,大致邏輯如下:

通過對比新舊vnode,找到更新真實DOM需要的所有操作,比如新增、刪除、替換節點的操作。然後通過Vue框架來執行這些更新DOM的操作,最後返回更新的DOM。

參考

template 模板是怎樣通過 Compile 編譯的 Vue.js 技術揭祕

相關文章