【Vue源碼學習】vue實例化到掛載到dom(上)

作者:王聰
本篇目的是介紹vue實例化到掛載到dom的整體路線,一些細節會被省略。

從new Vue()開始

  • 所有的一切都是從 new Vue()開始的,所以從這個點開始探尋這個過程發生了什麼。

從源碼中找到Vue構造函數的聲明,src/core/instance/index.js

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

是一個很簡潔的function工廠模式聲明的一個構造函數。
內部做了邏輯判斷:構造函數調用必須有 new 關鍵字。
然後執行了this._init(options),該初始化函數就是Vue 初始化的開始。

Vue.prototype._init()

this._init()是在何時聲明的呢?通過下邊5個初始化函數的執行,在Vue原型鏈中添加了大量的的屬性與函數。this._init()實際就是調用了原型鏈上的Vue.prototype._init()函數。

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

Vue.prototype._init函數是在initMixin(Vue)中去添加到原型鏈上的。在src/core/instance/init.js中定義

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

_init函數內部又通過上邊的這種調用初始化函數的模式完成了具體模塊的初始化。聲明鉤子的調用也首次在這裏出現,通過分析這些函數調用順序,能更好的理解官方文檔中提及的各個生命週期鉤子函數的觸發時機。

initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

_init()函數的最後,執行了vm.$mount()。vm代表的是當前vue實例也就是構造函數被調用後的this指向。
**$mount**是何時在vue上聲明的呢?這次並不是在上邊的初始化函數中完成聲明的。因爲 $mount 這個方法的實現是和平臺、構建方式都相關,所以在不同構建入口文件中有不同的定義。

Vue.prototype.$mount

原型上聲明的 $mount方法在 src/platform/web/runtime/index.js 中定義,這個方法會被runtime only版本和含編譯器完整版中複用,vue版本說明

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

現在接着上邊Vue.prototype._init函數中調用了vm.$mount函數,實際上是執行了mountComponent(this, el, hydrating)函數。
mountComponent定義在目錄src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

拋開源碼中對性能測試和異常警告部分的代碼,這個函數內部做了以下事情:

  • callHook(vm, 'beforeMount'),調用生命週期鉤子beforeMount
  • 聲明瞭updateComponent函數
  • new Watcher (vm, updateComponent, noop, {... callHook(vm, 'beforeUpdate')}})
  • callHook(vm, 'mounted'),調用生命週期鉤子mounted

這裏的new Watcher()實例化了Watcher類,內部邏輯先不去深入,這裏僅僅需要知道的是在這個實例化的過程中調用了作爲參數傳入的updateComponent函數,而從這個函數的聲明來看,它實際上執行的是vm._update(vm._render(), hydrating)這個函數。
首先vm._update和vm._render這兩個方法是定義在Vue原型上的。

Vue.prototype._render

Vue 的 _render 方法是用來把實例渲染成一個虛擬 Node。是在renderMixin(Vue)執行時聲明的。它的定義在 src/core/instance/render.js 文件中

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

  // 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 {
    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') {
      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
}

函數內部對不同邏輯有不同的處理,但最終返回的都是VNode。
Virtual DOM 就是用一個原生的 JS 對象去描述一個 DOM 節點。

Vue.prototype._update函數

_update是在lifecycleMixin(Vue)函數執行時添加的。在目錄src/core/instance/lifecycle.js

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // 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.
  }

通過源碼看接受的參數:

  • vnode: VNode
  • hydrating?: boolean

接受的第一個參數類型是VNode(用來描述dom節點的虛擬節點),第二個布爾類型的參數是跟ssr相關。
函數內部最關鍵的執行了vm.$el = vm.__patch__(...)
調用 vm.__patch__ 去把 VNode 轉換成真正的 DOM 節點

現在回顧總結一下目前的流程:

  • new Vue()操作後,會調用Vue.prototype._init()方法。完成一系列初始化(原型上添加方法和屬性)

執行Vue.prototype.$mount

  • vm._render()獲取描述當前實例的VNode
  • vm._update(VNode),調用 vm.__patch__轉換成真正的 DOM 節點

流程圖:

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