vue 入口
從vue的構建過程可以知道,web環境下,入口檔案在 src/platforms/web/entry-runtime-with-compiler.js
(以Runtime + Compiler模式構建,vue直接執行在瀏覽器進行編譯工作)
import Vue from `./runtime/index`
下一步,找到./runtime/index
,發現:
import Vue from `core/index`
下一步,找到core/index
,發現:
import Vue from `./instance/index`
按照這個思路找,最後發現:Vue是在`core/index`下定義的
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
引入方法,用function
定義了Vue類
,再以Vue
為引數,呼叫了5個方法,最後匯出了vue
。
可以進入這5個檔案檢視相關方法,主要就是在Vue
原型上掛載方法,可以看到,Vue
是把這5個方法按功能放入不同的模組中,這很利於程式碼的維護和管理
initGlobalAPI
回到core/index.js
, 看到除了引入已經在原型上掛載方法後的 Vue
外,還匯入initGlobalAPI 、 isServerRendering、FunctionalRenderContext
,執行initGlobalAPI(Vue)
,在vue.prototype
上掛載$isServer、$ssrContext、FunctionalRenderContext
,在vue
上掛載 version
屬性,
看到initGlobalAPI
的定義,主要是往vue.config、vue.util
等上掛載全域性靜態屬性和靜態方法(可直接通過Vue呼叫,而不是例項呼叫),再把builtInComponents 內建元件
擴充套件到Vue.options.components
下。此處大致瞭解下它是做什麼的即可,後面用到再做具體分析。
new Vue()
一般我們用vue
都採用模板語法來宣告:
<div id="app">
{{ message }}
</div>
var app = new Vue({
el: `#app`,
data: {
message: `Hello Vue!`
}
})
當new Vue()
時,vue
做了哪些處理?
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
例項化,否則報錯。例項化vue
後,執行了this._init()
,該方法在通過initMixin(Vue)
掛載在Vue
原型上的,找到定義檔案core/instance/init.js
檢視該方法。
_init()
一開始在this
物件上定義_uid、_isVue
,判斷options._isComponent
,此次先不考慮options._isComponent
為true
的情況,走else
,合併options
,接著安裝proxy
, 初始化生命週期,初始化事件、初始化渲染、初始化data、鉤子函式等,最後判斷有vm.$options.el
則執行vm.$mount()
,即是把el
渲染成最終的DOM
。
初始化data
資料繫結
_init()
中通過initState()
來繫結資料到vm上,看下initState
的定義:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
獲取options
,初始化props
、methods
、data
、計算屬性、watch
繫結到vm
上,先來看下initData()
是如何把繫結data
的:
- 先判斷
data
是不是function
型別,是則呼叫getData
,返回data的自呼叫,不是則直接返回data
,並將data
賦值到vm._data
上 - 對
data、props、methods
,作個校驗,防止出現重複的key
,因為它們最終都會掛載到vm
上,都是通過vm.key
來呼叫 -
通過
proxy(vm, `_data`, key)
把每個key
都掛載在vm
上export function proxy (target: Object, sourceKey: string, key: string) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) } const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }
proxy()
定義了一個get/set
函式,再通過Object.defineProperty
定義修改屬性(不瞭解Object.defineProperty()
的同學可以先看下文件,通過Object.defineProperty()
定義的屬性,通過描述符的設定可以進行更精準的控制物件屬性),將對target的key訪問加了一層get/set
,即當訪問vm.key
時,實際上是呼叫了sharedPropertyDefinition.get
,返回this._data.key
,這樣就實現了通過vm.key
來呼叫vm._data
上的屬性 - 最後,
observe(data, true /* asRootData */)
觀察者,對資料作響應式處理,這也是vue
的核心之一,此處先不分析
$mount()
例項掛載
Vue
的核心思想之一是資料驅動,在vue
下,我們不會直接操作DOM
,而是通過js修改資料,所有邏輯只需要考慮對資料的修改,最後再把資料渲染成DOM。其中,$mount()
就是負責把資料掛載到vm
,再渲染成最終DOM
。
接下來將會分析下 vue
是如何把javaScript物件渲染成dom
元素的,和之前一樣,主要分析主線程式碼
預處理
還是從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)
···
}
首先將原先原型上的$mount
方法快取起來,再重新定義$mount
:
- 先判斷
el
,el
不能是body, html
,因為渲染出來的DOM
最後是會替換掉el
的 - 判斷
render
方法, 有的話直接呼叫mount.call(this, el, hydrating)
- 沒有
render
方法時:
- 判斷有沒有
template
,有則用compileToFunctions
將其編譯成render方法 - 沒有
template
時,則檢視有沒有el
,有轉換成template
,再用compileToFunctions
將其編譯成render
方法 - 將
render
掛載到options下 - 最後呼叫
mount.call(this, el, hydrating)
,即是呼叫原先原型上的mount方法
我們發現這一系列呼叫都是為了生成render
函式,說明在vue
中,所有的元件渲染最終都需要render
方法(不管是單檔案.vue還是el/template
),vue
文件裡也提到:
Vue
選項中的 render 函式若存在,則Vue
建構函式不會從template
選項或通過 el 選項指定的掛載元素中提取出的HTML
模板編譯渲染函式。
原先原型上的mount
方法
找到原先原型上的mount
方法,在src/platform/web/runtime/index.js
中:
// 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
方法,這麼設計使得這個方法可以被 runtime only
和runtime+compiler
版本共同使用
$mount
第一個引數el
, 表示掛載的元素,在瀏覽器環境會通過query(el)
獲取到dom
物件,第二個引數和服務端渲染相關,不進行深入分析,此處不傳。接著呼叫mountComponent()
看下query()
,比較簡單,當el
是string
時,找到該選擇器返回dom
物件,否則新建立個div
dom物件,el
是dom
物件直接返回el
.
mountComponent
mountComponent
定義在src/core/instance/lifecycle.js
中,傳入vm,el
,
- 將
el
快取在vm.$el
上 - 判斷有沒有
render
方法,沒有則直接把createEmptyVNode
作為render
函式 - 開發環境警告(沒有
Render
但有el/template
不能使用runtime-only
版本、render
和template
必須要有一個) - 掛載
beforeMount
鉤子 -
定義
updateComponent
, 渲染相關updateComponent = () => { vm._update(vm._render(), hydrating) }
-
new Watcher()
例項化一個渲染watcher,簡單看下定義,this.getter = expOrFn
把updateComponent
掛載到this.getter
上this.value = this.lazy ? undefined : this.get()
get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) {...} return value }
執行
this.get()
,則執行了this.getter
,即updateComponent
,所以new Watcher()
時會執行updateComponent
,也就會執行到vm._update、vm._render
方法。因為之後不止初始化時需要渲染頁面,資料發生變化時也是要更新到dom上的,例項watcher可以實現對資料進行監聽以及隨後的更新
dom
處理,watcher
會在初始化執行回撥,也會在資料變化時執行回撥,此處先簡單介紹為什麼要使用watcher
,不深入分析watcher
實現原理。 - 最後判斷有無根節點,無則表示首次掛載,新增
mounted
鉤子函式 ,返回vm
總結
例項初始化:new Vue()->掛載方法屬性->this._init->初始化data->$mount
掛載過程:(在complier
版本,生成render
函式)對el作處理,執行mountComponent
,mountComponent
中定義了updateComponent
,通過例項化watcher
的回撥執行updateComponent
,執行updateComponent
,即呼叫了vm._update、vm._render
真實渲染成dom
物件。