vue 原始碼學習 - 例項掛載

三隻萌新發表於2018-11-10

前言

在學習vue原始碼之前需要先了解原始碼目錄設計(瞭解各個模組的功能)丶Flow語法。

src
├── compiler    # 把模板解析成 ast 語法樹,ast 語法樹優化,程式碼生成等功能。
├── core        # 核心程式碼  Vue.js 的靈魂
├── platforms   # 不同平臺的支援 web 和 weex
├── server      # 服務端渲染這部分程式碼是跑在服務端的 Node.js
├── sfc         # .vue 檔案解析
├── shared      # 工具方法
複製程式碼

flow語法可以參照 v-model原始碼學習中提到的flow語法介紹,以及到官網瞭解更多。

vue 例項化

vue 本質上就是一個用 Function 實現的 Class,然後它的原型 prototype 以及它本身都擴充套件了一系列的方法和屬性

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
複製程式碼

通過原始碼我們可以看到,它實際上就是一個建構函式。我們往後看這裡有很多 xxxMixin 的函式呼叫,並把 Vue 當引數傳入,它們的功能都是給 Vue 的 prototype 上擴充套件一些方法。

階段

  1. 首先通過new Vue例項化,過程可以參考之前寫的vue 生命週期梳理
  2. vue 例項掛載的實現 Vue中是通過$mount例項方法去掛載vm,$mount方法再多個檔案中都有定義,和平臺,構建方式相關。 首先來看 src/platform/web/entry-runtime-with-compiler.js檔案中
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  // A-> ..... 代表後面省略的程式碼從A-> 處接下去
}
複製程式碼

1.這段程式碼首先快取了原型上的$mount 方法,再重新定義該方法
為了對比前後方法的差別,我們可以先看

compiler 版本的 $mount

\$mount 方法
2. $mount方法支援傳入兩個引數,第一個是el,它表示掛載的元素,可以是字串,可以是DOM物件,會呼叫query方法轉換成DOM物件,在瀏覽器環境下我們不需要傳第二個引數,它是一個可選引數。
接下來繼續看後面的程式碼

// <-A ..... 代表接前面的程式碼繼續寫
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
  // A-> ..... 代表後面省略的程式碼從A-> 處接下去
複製程式碼

首先對 el 做了限制,Vue 不能掛載在 body、html 這樣的根節點上。如果是其中一個則返回this。this就是vue例項本身

vuethis

定義option物件(new Vue中傳入的資料)

// <-A ..... 代表接前面的程式碼繼續寫
if (!options.render) {
    let template = options.template
     if (template) {
        // B-> ..... 代表後面省略的程式碼從B-> 處接下去
     }else if(el){
        // C-> ..... 代表後面省略的程式碼從C-> 處接下去
     }
      if (template) {
        // D-> ..... 代表後面省略的程式碼從D-> 處接下去
      }
    return mount.call(this, el, hydrating)
}
複製程式碼
  1. 判斷有沒有定義render方法,沒有則會把el或者template字串轉換成render方法。在 Vue 2.0 版本中,所有 Vue 的元件的渲染最終都需要 render 方法
// <-B ..... 代表接前面的程式碼繼續寫
if(template){
    if (typeof template === 'string') {
        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 = template.innerHTML
          }
}

複製程式碼
  1. 判斷template 是否為字串,取字串的第一位判斷是否是# 如果是#開頭代表節點字串,並呼叫idToTemplate方法如下
const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})
複製程式碼

接受一個引數,對這個引數進行query方法,前面提到query是將字串轉化成DOM,並且返回DOM的innerHTML

// <-C ..... 代表接前面的程式碼繼續寫
  template = getOuterHTML(el)
複製程式碼

如果沒有render和template的情況下,使用getOuterHTML方法重新定義template

function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}
複製程式碼
  1. 掛在DOM元素的HTML會被提取出來用作模板

outerHTML
總結 : render函式優先順序最高,template和el次之

模板型別

  1. render : 型別function 接收一個 createElement 方法作為第一個引數用來建立 VNode
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // 標籤名稱
      this.$slots.default // 子元素陣列
    )
  },
複製程式碼
  1. template:型別string 一個字串模板作為 Vue 例項的標識使用。模板將會 替換 掛載的元素。
  2. el:型別string | HTMLElement 提供一個在頁面上已存在的 DOM 元素作為 Vue 例項的掛載目標。可以是 CSS 選擇器,也可以是一個 HTMLElement 例項

runtime only 版本的$mount

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
複製程式碼

開始和compiler版本的$mount 實現相同,只不過多加了一個inBrowser判斷是否在瀏覽器環境下。
$mount 方法實際上會去呼叫 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
    }
    callHook(vm, 'beforeMount')
    // A-> ..... 代表後面省略的程式碼從A-> 處接下去
}
複製程式碼
  1. mountComponent接收到Vue.prototype.$mount方法中vue例項物件,和el字串(經過query處理已經轉成DOM)
  2. 更新vm例項上的$el
  3. 判斷vm上有無render模板,如果沒有建立一個空的虛擬VNode
  4. 插入beforeMount鉤子
// <-A ..... 代表接前面的程式碼繼續寫
 let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  }else{
     updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }  
  }
 vm._watcher = new Watcher(vm, updateComponent, noop)
 // A-> ..... 代表後面省略的程式碼從A-> 處接下去
複製程式碼
  1. mountComponent 核心就是先呼叫vm._render方法先生成虛擬 Node 將 vm._update方法作為返回值賦值給updateComponent
  2. 例項化Watcher建構函式,將updateComponent作為回撥函式,也就是說在例項化Watcher後最終呼叫vm._update 更新 DOM。

watcher的作用

  1. 例項化的過程後執行回撥,將呼叫vm._update 更新 DOM。
  2. vm 例項中的監測的資料發生變化的時候執行回撥函式實現更新DOM
// <-A ..... 代表接前面的程式碼繼續寫
hydrating = false
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
複製程式碼

這裡vm.$vnode的值是什麼,檔案定義在src/core/instance/render.js 中,這裡只關注vm.$vnode所以貼出相關程式碼

export function renderMixin (Vue: Class<Component>) {
    Vue.prototype._render = function (): VNode {
        const vm: Component = this
        const { render, _parentVnode } = vm.$options
        vm.$vnode = _parentVnode
    }
}
複製程式碼

renderMixin函式接收Vue例項引數,在vue原型上的內部_render方法需要返回一個VNode,並且通過結構賦值的方法取出例項中$options的屬性和方法。
我們來看看vm.$options物件具體有些什麼

parent

  1. 物件中有render函式,但是還未定義_parentVnode。可以知道vm.$vnode 表示 Vue 例項的父虛擬 Node,而且在mountComponent 函式中值還未定義。
  2. 由於未定義vm.$vnode值為undefined 所以vm.$vnode==null結果也為真
    mount
  3. 我們也可以通過生命週期圖來理解, VNode render 是發生在beforeUPdate 之後updated之前這個環節
  4. 流程 :(1) new Vue ==> (2) init ==> (3) $mount ==> (4) compile ==> (5) render ==> (6) vnode ==> (7) patch ==> (8) DOM
  5. 最後設定 vm._isMounted 為 true作為之後判斷是否經歷了mounted生命週期的條件

總結

  1. 判斷掛載的節點不能掛載在 body、html 上。
  2. 模板優先順序render>template>el 並且最終都會轉換成render方法
  3. 知道mountComponent方法 做了什麼,先是呼叫了vm._render 方法先生成虛擬 Node,然後例項化Watcher 執行它,並監聽資料變化,實時更新。
  4. 設定vm._isMounted標誌,作為判斷依據

相關文章