Vue源碼學習(持續更新中)

先佔個位置,看看學習完整理成博客需要耗時多久
vue版本:2.6.11
開始時間: 2020-04-13
學習內容來自 黃軼老師
爲了分析 Vue 的編譯過程,我們這裏分析的源碼是 Runtime + Compiler 的 Vue.js。
調試代碼在:node_modules\vue\dist\vue.esm.js 裏添加

準備工作

認識Flow

Flow 是 facebook 出品的 JavaScript 靜態類型檢查工具。Vue.js 的源碼利用了 Flow 做了靜態類型檢查,所以瞭解 Flow 有助於我們閱讀源碼。

爲什麼用 Flow

JavaScript 是動態類型語言,它的靈活性有目共睹,但是過於靈活的副作用是很容易就寫出非常隱蔽的隱患代碼,在編譯期甚至看上去都不會報錯,但在運行階段就可能出現各種奇怪的 bug。

類型檢查是當前動態類型語言的發展趨勢,所謂類型檢查,就是在編譯期儘早發現(由類型錯誤引起的)bug,又不影響代碼運行(不需要運行時動態檢查類型),使編寫 JavaScript 具有和編寫 Java 等強類型語言相近的體驗。

項目越複雜就越需要通過工具的手段來保證項目的維護性和增強代碼的可讀性。 Vue.js 在做 2.0 重構的時候,在 ES2015 的基礎上,除了 ESLint 保證代碼風格之外,也引入了 Flow 做靜態類型檢查。之所以選擇 Flow,主要是因爲 Babel 和 ESLint 都有對應的 Flow 插件以支持語法,可以完全沿用現有的構建配置,非常小成本的改動就可以擁有靜態類型檢查的能力。

Flow 的工作方式

通常類型檢查分成 2 種方式:

  • 類型推斷:通過變量的使用上下文來推斷出變量類型,然後根據這些推斷來檢查類型。
  • 類型註釋:事先註釋好我們期待的類型,Flow 會基於這些註釋來判斷。
類型推斷

不需要任何代碼修改即可進行類型檢查,最小化開發者的工作量。它不會強制你改變開發習慣,因爲它會自動推斷出變量的類型。這就是所謂的類型推斷,Flow 最重要的特性之一。

通過一個簡單例子說明一下:

/*@flow*/

function split(str) {
  return str.split(' ')
}

split(11)

Flow 檢查上述代碼後會報錯,因爲函數 split 期待的參數是字符串,而我們輸入了數字。

類型註釋

如上所述,類型推斷是 Flow 最有用的特性之一,不需要編寫類型註釋就能獲取有用的反饋。但在某些特定的場景下,添加類型註釋可以提供更好更明確的檢查依據。

考慮如下代碼:

/*@flow*/

function add(x, y){
  return x + y
}

add('Hello', 11)

Flow 檢查上述代碼時檢查不出任何錯誤,因爲從語法層面考慮, + 既可以用在字符串上,也可以用在數字上,我們並沒有明確指出 add() 的參數必須爲數字。

在這種情況下,我們可以藉助類型註釋來指明期望的類型。類型註釋是以冒號 : 開頭,可以在函數參數返回值變量聲明中使用。

如果我們在上段代碼中添加類型註釋,就會變成如下:

/*@flow*/

function add(x:number, y:number):number {
	return x + y
}

add('Hello', 11)

現在 Flow 就能檢查出錯誤,因爲函數參數的期待類型爲數字,而我們提供了字符串。

上面的例子是針對函數的類型註釋。接下來我們來看看 Flow 能支持的一些常見的類型註釋。

數組
/*@flow*/

var arr: Array<number> = [1, 2, 3]

arr.push('hello')

數組類型註釋的格式是Array<T>T表示數組中每項的數據類型
在上述代碼中,arr 是每項均爲數字的數組。如果我們給這個數組添加了一個字符串,Flow 能檢查出錯誤。

類和對象
/*@flow*/

class Bar {
	x: string; // x 是字符串
	y: string | number; // y 可以是字符串或者數字
	z: boolean;

	constructor(x: string, y: string | number) {
		this.x = x
		this.y = y
		this.z = false
	}
}

var bar: Bar = new Bar('hello', 4)

var obj:  {
	a: string,
	b: number,
	c: Array<string>,
	d: Bar
} = {
	a: 'hello',
	b: 11,
	c: ['hello', 'world'],
	d: new Bar('hello', 3)
}

類的類型註釋格式如上,可以對類自身的屬性做類型檢查,也可以對構造函數的參數做類型檢查。這裏需要注意的是,屬性 y 的類型中間用 | 做間隔,表示 y 的類型即可以是字符串也可以是數字。

對象的註釋類型類似於類,需要指定對象屬性的類型。

null

若想任意類型 T可以爲 null 或者 undefined,只需類似如下寫成 ?T的格式即可。

/*@flow*/

var foo: ?string = null

此時,foo 可以爲字符串也可以爲 null

目前我們只列舉了 Flow 的一些常見的類型註釋。如果想了解所有類型註釋,請移步 Flow 的官方文檔

Flow 在 Vue.js 源碼中的應用

有時候我們想引用第三方庫,或者自定義一些類型,但 Flow 並不認識,因此檢查的時候會報錯。爲了解決這類問題,Flow 提出了一個 libdef 的概念,可以用來識別這些第三方庫或者是自定義類型,而 Vue.js 也利用了這一特性。

在 Vue.js 的主目錄下有 .flowconfig 文件, 它是mermaid flowchat 的配置文件,感興趣的同學可以看官方文檔。這其中的 [libs] 部分用來描述包含指定庫定義的目錄,默認是名爲 flow-typed 的目錄。

這裏 [libs] 配置的是 flow,表示指定的庫定義都在 flow 文件夾內。我們打開這個目錄,會發現文件如下:

flow
├── compiler.js        # 編譯相關
├── component.js       # 組件數據結構
├── global-api.js      # Global API 結構
├── modules.js         # 第三方庫定義
├── options.js         # 選項相關
├── ssr.js             # 服務端渲染相關
├── vnode.js           # 虛擬 node 相關

可以看到,Vue.js 有很多自定義類型的定義,在閱讀源碼的時候,如果遇到某個類型並想了解它完整的數據結構的時候,可以回來翻閱這些數據結構的定義。

總結

通過對 Flow 的認識,有助於我們閱讀 Vue 的源碼,並且這種靜態類型檢查的方式非常有利於大型項目源碼的開發和維護。類似 Flow 的工具還有如 TypeScript,感興趣的同學也可以自行去了解一下。

Vue.js 源碼目錄設計

Vue.js 的源碼都在 src 目錄下,其目錄結構如下。

src
├── compiler        # 編譯相關 
├── core            # 核心代碼 
├── platforms       # 不同平臺的支持
├── server          # 服務端渲染
├── sfc             # .vue 文件解析
├── shared          # 共享代碼

compiler

compiler 目錄包含 Vue.js 所有編譯相關的代碼。它包括把模板解析成 ast 語法樹,ast 語法樹優化,代碼生成等功能。

編譯的工作可以在構建時做(藉助 webpack、vue-loader 等輔助插件);也可以在運行時做,使用包含構建功能的 Vue.js。顯然,編譯是一項耗性能的工作,所以更推薦前者——離線編譯。

core

core 目錄包含了 Vue.js 的核心代碼,包括內置組件、全局 API 封裝,Vue 實例化、觀察者、虛擬 DOM、工具函數等等。

這裏的代碼可謂是 Vue.js 的靈魂

platform

Vue.js 是一個跨平臺的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 native 客戶端上。platform 是 Vue.js 的入口,2 個目錄代表 2 個主要入口,分別打包成運行在 web 上和 weex 上的 Vue.js。

這邊博客重點分析 web 入口打包後的 Vue.js

server

Vue.js 2.0 支持了服務端渲染,所有服務端渲染相關的邏輯都在這個目錄下。注意:這部分代碼是跑在服務端的 Node.js,不要和跑在瀏覽器端的 Vue.js 混爲一談。

服務端渲染主要的工作是把組件渲染爲服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最後將靜態標記"混合"爲客戶端上完全交互的應用程序。

sfc

通常我們開發 Vue.js 都會藉助 webpack 構建, 然後通過 .vue 單文件來編寫組件。

這個目錄下的代碼邏輯會把 .vue 文件內容解析成一個 JavaScript 的對象。

shared

Vue.js 會定義一些工具方法,這裏定義的工具方法都是會被瀏覽器端的 Vue.js 和服務端的 Vue.js 所共享的。

總結

從 Vue.js 的目錄設計可以看到,作者把功能模塊拆分的非常清楚,相關的邏輯放在一個獨立的目錄下維護,並且把複用的代碼也抽成一個獨立目錄。

這樣的目錄設計讓代碼的閱讀性和可維護性都變強,非常值得學習和推敲。

從入口開始

我們之前提到過 Vue.js 構建過程,在 web 應用下,我們來分析 Runtime + Compiler 構建出來的 Vue.js,它的入口是 src/platforms/web/entry-runtime-with-compiler.js:

src/platforms/web/entry-runtime-with-compiler.js:

/* @flow */

import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'

import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  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) {
    let template = options.template
    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
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        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')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
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
  }
}

Vue.compile = compileToFunctions

export default Vue

那麼,當我們的代碼執行import Vue from 'vue'的時候,就是從這個入口執行代碼來初始化 Vue, 那麼 Vue 到底是什麼,它是怎麼初始化的,我們來一探究竟。

Vue的入口

在這個入口 JS 的上方我們可以找到 Vue 的來源:import Vue from ‘./runtime/index’,我們先來看一下這塊兒的實現,它定義在 src/platforms/web/runtime/index.js 中

src/platforms/web/runtime/index.js

import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser, isChrome } from 'core/util/index'

import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index'

import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

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

// ...

export default Vue

這裏關鍵的代碼是 import Vue from ‘core/index’,之後的邏輯都是對 Vue 這個對象做一些擴展,可以先不用看,我們來看一下真正初始化 Vue 的地方,在 src/core/index.js 中:

src/core/index.js

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

initGlobalAPI(Vue)

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue

這裏有 2 處關鍵的代碼,import Vue from './instance/index' 和 initGlobalAPI(Vue),初始化全局 Vue API(我們稍後介紹),
我們先來看第一部分,在 src/core/instance/index.js 中:

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

在這裏,我們終於看到了Vue 的廬山真面目,它實際上就是一個用 Function 實現的類,我們只能通過 new Vue 去實例化它。

有些同學看到這不禁想問,爲何 Vue 不用 ES6 的 Class 去實現呢?我們往後看這裏有很多 xxxMixin 的函數調用,並把 Vue 當參數傳入它們的功能都是給 Vue 的 prototype 上擴展一些方法(這裏具體的細節會在之後的文章介紹,這裏不展開),Vue 按功能把這些擴展分散到多個模塊中去實現,而不是在一個模塊裏實現所有,這種方式是用 Class 難以實現的。這麼做的好處是非常方便代碼的維護和管理,這種編程技巧也非常值得我們去學習。

initGlobalAPI

Vue.js 在整個初始化過程中,除了給它的原型 prototype 上擴展方法,還會給 Vue 這個對象本身擴展全局的靜態方法,它的定義在 src/core/global-api/index.js 中:

src/core/global-api/index.js

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

這裏就是在 Vue 上擴展的一些全局方法的定義,Vue 官網中關於全局 API 都可以在這裏找到,這裏不會介紹細節,會在之後的章節我們具體介紹到某個 API 的時候會詳細介紹。有一點要注意的是,Vue.util 暴露的方法最好不要依賴,因爲它可能經常會發生變化,是不穩定的。

總結

那麼至此,Vue 的初始化過程基本介紹完畢。這一節的目的是讓同學們對 Vue 是什麼有一個直觀的認識,
它本質上就是一個用 Function 實現的 Class
然後它的原型 prototype 以及它本身都擴展了一系列的方法和屬性
那麼 Vue 能做什麼,它是怎麼做的,我們會在後面的章節一層層幫大家揭開 Vue 的神祕面紗。

數據驅動

Vue.js一個核心思想數據驅動。所謂數據驅動,是指視圖是由數據驅動生成的,我們對視圖的修改,不會直接操作 DOM,而是通過修改數據。

它相比我們傳統的前端開發,如使用 jQuery 等前端庫直接修改 DOM,大大簡化了代碼量。特別是當交互複雜的時候,只關心數據的修改讓代碼的邏輯變的非常清晰,因爲 DOM 變成了數據的映射,我們所有的邏輯都是對數據的修改,而不用碰觸 DOM,這樣的代碼非常利於維護

在 Vue.js 中我們可以採用簡潔的模板語法來聲明式的將數據渲染爲 DOM:

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

最終它會在頁面上渲染出 Hello Vue。接下來,我們會從源碼角度來分析 Vue 是如何實現的,分析過程會以主線代碼爲主重要的分支邏輯會放在之後單獨分析。數據驅動還有一部分是數據更新驅動視圖變化,這一塊內容我們也會在之後分析

現在我們的目標是弄清楚模板和數據如何渲染成最終的 DOM

new Vue()發生了什麼

從入口代碼開始分析,我們先來分析 new Vue 背後發生了哪些事情。
我們都知道,new 關鍵字在 Javascript 語言中代表實例化是一個對象,而 Vue 實際上是一個類,類在 Javascript 中是用 Function 來實現的,來看一下源碼,在src/core/instance/index.js 中。

這裏看源碼思路: 重點看new Vue整體主流程,
然後看initState()方法,重點看裏面的initData()的過程
initData()裏的getData中的pushTarget()方法先不看,後面響應式原理的部分再仔細看
我們能通過this.message去訪問data選項裏定義的message,是通過proxy(vm, _data, key)方法實現
observe()方法也先不看,跟響應式相關,後面再看

src\core\instance\index.js

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)
}

可以看到 Vue 只能通過 new 關鍵字初始化,然後會調用 this._init 方法, 該方法在 src/core/instance/init.js 中定義。

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不少人在項目中用到 具體從這裏來
    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)
  }
}

Vue 初始化主要就幹了幾件事情,合併配置初始化生命週期初始化事件中心初始化渲染初始化 data、props、computed、watcher 等等。

總結-new Vue()

Vue 的初始化邏輯寫的非常清楚,把不同的功能邏輯拆成一些單獨的函數執行,讓主線邏輯一目瞭然,這樣的編程思想是非常值得借鑑和學習的。

由於我們這一章的目標是弄清楚模板和數據如何渲染成最終的 DOM,所以各種初始化邏輯我們先不看。在初始化的最後,檢測到如果有 el 屬性,則調用 vm.$mount 方法掛載 vm,掛載的目標就是把模板渲染成最終的 DOM,那麼接下來我們來分析 Vue 的掛載過程。

Vue實例掛載的實現

Vue 中我們是通過$mount 實例方法去掛載 vm 的,$mount方法在多個文件中都有定義,如src/platform/web/entry-runtime-with-compiler.jssrc/platform/web/runtime/index.jssrc/platform/weex/runtime/index.js。因爲 $mount這個方法的實現和平臺、構建方式都相關的。

接下來我們重點分析帶 compiler 版本的 $mount 實現,因爲拋開 webpack 的 vue-loader,我們在純前端瀏覽器環境分析 Vue 的工作原理,有助於我們對原理理解的深入。

這裏看源碼思路: 重點看vm.$amount()方法的主要流程,不細看各個方法實現
首先是對做解析,返回dom對象,
接着判斷有沒有render方法,沒有的話,會拿到template,最終通過編譯得到render()方法,編譯過程這裏先不看,後面會專門講
重點分析mountComponent方法,vm._render()方法和vm._update()方法這裏先粗略看看,後面會專門講解

compiler 版本的 $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)

  /* istanbul ignore if */
  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) {
    let template = options.template
    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
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      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')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

這段代碼首先緩存了原型上的$mount方法,再重新定義該方法,我們先來分析這段代碼。首先,它對 el 做了限制,Vue 不能掛載在 body、html 這樣的根節點上
接下來的是很關鍵的邏輯 —— 如果沒有定義 render 方法,則會把 el 或者 template 字符串轉換成 render 方法。這裏我們要牢記,在 Vue 2.0 版本中,所有 Vue 的組件的渲染最終都需要 render 方法,無論我們是用單文件 .vue 方式開發組件,還是寫了 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 = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

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

$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
    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) {
        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核心就是先實例化一個渲染Watcher,在它的回調函數中會調用updateComponent方法,在此方法中調用vm._render方法先生成虛擬 Node,最終調用vm._update 更新 DOM

Watcher在這裏起到兩個作用,一個是初始化的時候會執行回調函數,另一個是當 vm 實例中的監測的數據發生變化的時候執行回調函數,這塊兒我們會在之後的章節中介紹。

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

總結-Vue實例掛載的實現

mountComponent方法的邏輯也是非常清晰的,它會完成整個渲染工作,接下來我們要重點分析其中的細節,也就是最核心的 2 個方法:vm._rendervm._update

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