二、Vue 實例掛載的實現

參考Vue 實例掛載的實現

我們分析的是complier版本的$mount

第一步:入口文件src/platform/web/entry-runtime-with-compiler.js

// 獲取原型上的$mount方法,這個$mount方法在 import Vue from './runtime/index' ./runtime/index中定義的
const mount = Vue.prototype.$mount
// 重新定義一遍 這塊mount是給complier版本使用的
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 查詢el對應的demo 將demo對象 賦值給 el
  el = el && query(el)

  /* istanbul ignore if */
  // 這個demo對象不能是body 或者文檔標簽,否則報錯,因為vue將這個demo對象給覆蓋了 那么如果將body或者html給覆蓋了 就不對了
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) { // 實例化Vue時沒有定義render函數
    // 獲取實例化Vue中的template模板
    let template = options.template
    // 如果寫了template模板的話,對template進行處理
    if (template) {
      if (typeof template === 'string') { // 不按照template寫的模板報錯
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {  // 判斷template是不是一個節(jié)點
        // 獲取模板中的內容
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // 將el通過query獲取的dom對象轉換成template模板
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  // mount是上面緩存的Vue.prototype.$mount  這個$mount方法在 import Vue from './runtime/index' ./runtime/index中定義的
 // 看第二步分析
  return mount.call(this, el, hydrating)
}

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
// 將el通過query獲取的dom對象轉換成template模板
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    // 不存在dem的話創(chuàng)建一個新的div 將el的dom克隆并插入這個div中
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

這段代碼首先緩存了原型上的 $mount方法,再重新定義該方法,我們先來分析這段代碼。首先,它對 el 做了限制,Vue 不能掛載在 body、html 這樣的根節(jié)點上。接下來的是很關鍵的邏輯 —— 如果沒有定義 render 方法,則會把 el 或者 template 字符串轉換成 render 方法。這里我們要牢記,在 Vue 2.0 版本中,所有 Vue 的組件的渲染最終都需要 render 方法,無論我們是用單文件 .vue 方式開發(fā)組件,還是寫了 el 或者 template 屬性,最終都會轉換成 render 方法,那么這個過程是 Vue 的一個“在線編譯”的過程,它是調用 compileToFunctions 方法實現的,編譯過程我們之后會介紹。最后,調用原先原型上的 $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對應的dom
  el = el && inBrowser ? query(el) : undefined
  // 第三步解析 mountComponent
  return mountComponent(this, el, hydrating)
}

$mount 方法支持傳入 2 個參數,第一個是 el,它表示掛載的元素,可以是字符串,也可以是 DOM 對象,如果是字符串在瀏覽器環(huán)境下會調用 query 方法轉換成 DOM 對象的。第二個參數是和服務端渲染相關,在瀏覽器環(huán)境下我們不需要傳第二個參數。

第三步:$mount 方法實際上會去調用 mountComponent 方法,這個方法定義在 src/core/instance/lifecycle.js 文件中:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 將dom對象緩存
  vm.$el = el
  // 實例化時沒有render函數
  if (!vm.$options.render) {
    // 創(chuàng)建空的VNode
    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
        )
      }
    }
  }
  // 執(zhí)行鉤子函數
  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函數定義
    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
  // 渲染watch
  new Watcher(vm, updateComponent, noop, {  // noop 是一個空函數
    before () {
      if (vm._isMounted) {
        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
}

從上面的代碼可以看到,mountComponent 核心就是先調用 vm._render 方法先生成虛擬 Node,再實例化一個渲染Watcher,在它的回調函數中會調用 updateComponent 方法,最終調用 vm._update 更新 DOM。

Watcher 在這里起到兩個作用,一個是初始化的時候會執(zhí)行回調函數,另一個是當 vm 實例中的監(jiān)測的數據發(fā)生變化的時候執(zhí)行回調函數,這塊兒我們會在之后的章節(jié)中介紹。

函數最后判斷為根節(jié)點的時候設置 vm._isMounted 為 true, 表示這個實例已經掛載了,同時執(zhí)行 mounted 鉤子函數。 這里注意 vm.$vnode 表示 Vue 實例的父虛擬 Node,所以它為 Null 則表示當前是根 Vue 的實例。

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內容,還有我對于 Vue 1.0 印象不深的內容。關于...
    云之外閱讀 5,178評論 0 29
  • 深入響應式 追蹤變化: 把普通js對象傳給Vue實例的data選項,Vue將使用Object.defineProp...
    冥冥2017閱讀 4,960評論 6 16
  • Vue 中通過 $mount 實例方法掛載 vm,$mount 方法在多個文件中都有定義,比如: src/pla...
    阿暢_閱讀 17,456評論 2 9
  • 還未老去,我便只能千般情話,萬盅柔腸就只能說與自己。 有時候很怕,我穿的整齊帥氣,每天蹬著油亮的...
    子山上人閱讀 1,807評論 0 2
  • 初次讀《小王子》還是在大一的時候,那時候讀完有種懵懵懂懂的感覺。我不太懂小王子對那朵玫瑰花的感情到底是怎樣的,我不...
    BLESS思閱讀 428評論 0 0

友情鏈接更多精彩內容