vue 原始碼學習 - 例項掛載
在學習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 上擴充套件一些方法。
階段
- 首先通過new Vue例項化,過程可以參考之前寫的vue 生命週期梳理
- 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

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例項本身

定義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) } 複製程式碼
- 判斷有沒有定義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 } } 複製程式碼
- 判斷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 } } 複製程式碼
- 掛在DOM元素的HTML會被提取出來用作模板

總結 : render函式優先順序最高,template和el次之
模板型別
- render : 型別function 接收一個 createElement 方法作為第一個引數用來建立 VNode
render: function (createElement) { return createElement( 'h' + this.level,// 標籤名稱 this.$slots.default // 子元素陣列 ) }, 複製程式碼
- template:型別string 一個字串模板作為 Vue 例項的標識使用。模板將會 替換 掛載的元素。
- 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-> 處接下去 } 複製程式碼
- mountComponent接收到Vue.prototype.$mount方法中vue例項物件,和el字串(經過query處理已經轉成DOM)
- 更新vm例項上的$el
- 判斷vm上有無render模板,如果沒有建立一個空的虛擬VNode
- 插入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-> 處接下去 複製程式碼
- mountComponent 核心就是先呼叫
vm._render
方法先生成虛擬 Node 將 vm._update方法作為返回值賦值給updateComponent
- 例項化
Watcher
建構函式,將updateComponen
t作為回撥函式
,也就是說在例項化Watcher後最終呼叫vm._update
更新 DOM。
watcher的作用
vm._update
// <-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物件具體有些什麼

- 物件中有render函式,但是還未定義_parentVnode。可以知道vm.$vnode 表示 Vue 例項的父虛擬 Node,而且在mountComponent 函式中值還未定義。
- 由於未定義vm.$vnode值為undefined 所以vm.$vnode==null結果也為真
- 我們也可以通過生命週期圖來理解, VNode render 是發生在beforeUPdate 之後updated之前這個環節
- 流程 :(1) new Vue ==> (2) init ==> (3) $mount ==> (4) compile ==> (5) render ==> (6) vnode ==> (7) patch ==> (8) DOM
- 最後設定 vm._isMounted 為 true作為之後判斷是否經歷了mounted生命週期的條件
總結
- 判斷掛載的節點不能掛載在 body、html 上。
- 模板優先順序render>template>el 並且最終都會轉換成render方法
- 知道mountComponent方法 做了什麼,先是呼叫了vm._render 方法先生成虛擬 Node,然後例項化Watcher 執行它,並監聽資料變化,實時更新。
- 設定vm._isMounted標誌,作為判斷依據