前言
在學習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 : 型別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
更新 DOM。 - 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物件具體有些什麼
- 物件中有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標誌,作為判斷依據