vue 入口
從vue的構(gòu)建過程可以知道,web環(huán)境下,入口文件在 src/platforms/web/entry-runtime-with-compiler.js(以Runtime + Compiler模式構(gòu)建,vue直接運(yùn)行在瀏覽器進(jìn)行編譯工作)
import Vue from './runtime/index'
下一步,找到./runtime/index,發(fā)現(xiàn):
import Vue from 'core/index'
下一步,找到core/index,發(fā)現(xiàn):
import Vue from './instance/index'
按照這個(gè)思路找,最后發(fā)現(xiàn):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為參數(shù),調(diào)用了5個(gè)方法,最后導(dǎo)出了vue。
可以進(jìn)入這5個(gè)文件查看相關(guān)方法,主要就是在Vue原型上掛載方法,可以看到,Vue 是把這5個(gè)方法按功能放入不同的模塊中,這很利于代碼的維護(hù)和管理
initGlobalAPI
回到core/index.js, 看到除了引入已經(jīng)在原型上掛載方法后的 Vue 外,還導(dǎo)入initGlobalAPI 、 isServerRendering、FunctionalRenderContext,執(zhí)行initGlobalAPI(Vue),在vue.prototype上掛載$isServer、$ssrContext、FunctionalRenderContext,在vue 上掛載 version 屬性,
看到initGlobalAPI的定義,主要是往vue.config、vue.util等上掛載全局靜態(tài)屬性和靜態(tài)方法(可直接通過Vue調(diào)用,而不是實(shí)例調(diào)用),再把builtInComponents 內(nèi)置組件擴(kuò)展到Vue.options.components下。此處大致了解下它是做什么的即可,后面用到再做具體分析。
new Vue()
一般我們用vue都采用模板語法來聲明:
<div id="app">
{{ message }}
</div>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
當(dāng)new Vue()時(shí),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實(shí)例化,否則報(bào)錯(cuò)。實(shí)例化vue后,執(zhí)行了this._init(),該方法在通過initMixin(Vue)掛載在Vue原型上的,找到定義文件core/instance/init.js 查看該方法。
_init()
一開始在this對(duì)象上定義_uid、_isVue,判斷options._isComponent,此次先不考慮options._isComponent為true的情況,走else,合并options,接著安裝proxy, 初始化生命周期,初始化事件、初始化渲染、初始化data、鉤子函數(shù)等,最后判斷有vm.$options.el則執(zhí)行vm.$mount(),即是把el渲染成最終的DOM。
初始化data 數(shù)據(jù)綁定
_init()中通過initState()來綁定數(shù)據(jù)到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、計(jì)算屬性、watch綁定到vm上,先來看下initData()是如何把綁定data的:
先判斷
data是不是function類型,是則調(diào)用getData,返回data的自調(diào)用,不是則直接返回data,并將data賦值到vm._data上對(duì)
data、props、methods,作個(gè)校驗(yàn),防止出現(xiàn)重復(fù)的key,因?yàn)樗鼈冏罱K都會(huì)掛載到vm上,都是通過vm.key來調(diào)用-
通過
proxy(vm, `_data`, key)把每個(gè)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()定義了一個(gè)get/set函數(shù),再通過Object.defineProperty定義\修改屬性(不了解Object.defineProperty()的同學(xué)可以先看下文檔,通過Object.defineProperty()定義的屬性,通過描述符的設(shè)置可以進(jìn)行更精準(zhǔn)的控制對(duì)象屬性),將對(duì)target的key訪問加了一層get/set,即當(dāng)訪問vm.key時(shí),實(shí)際上是調(diào)用了sharedPropertyDefinition.get,返回this._data.key,這樣就實(shí)現(xiàn)了通過vm.key來調(diào)用vm._data上的屬性 最后,
observe(data, true /* asRootData */)觀察者,對(duì)數(shù)據(jù)作響應(yīng)式處理,這也是vue的核心之一,此處先不分析
$mount() 實(shí)例掛載
Vue的核心思想之一是數(shù)據(jù)驅(qū)動(dòng),在vue下,我們不會(huì)直接操作DOM,而是通過js修改數(shù)據(jù),所有邏輯只需要考慮對(duì)數(shù)據(jù)的修改,最后再把數(shù)據(jù)渲染成DOM。其中,$mount()就是負(fù)責(zé)把數(shù)據(jù)掛載到vm,再渲染成最終DOM。
接下來將會(huì)分析下vue是如何把javaScript對(duì)象渲染成dom元素的,和之前一樣,主要分析主線代碼
預(yù)處理
還是從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,因?yàn)殇秩境鰜淼?DOM最后是會(huì)替換掉el的 - 判斷
render方法, 有的話直接調(diào)用mount.call(this, el, hydrating) - 沒有
render方法時(shí):
- 判斷有沒有
template,有則用compileToFunctions將其編譯成render方法 - 沒有
template時(shí),則查看有沒有el,有轉(zhuǎn)換成template,再用compileToFunctions將其編譯成render方法 - 將
render掛載到options下 - 最后調(diào)用
mount.call(this, el, hydrating),即是調(diào)用原先原型上的mount方法
我們發(fā)現(xiàn)這一系列調(diào)用都是為了生成render函數(shù),說明在vue中,所有的組件渲染最終都需要render方法(不管是單文件.vue還是el/template),vue 文檔里也提到:
Vue選項(xiàng)中的 render 函數(shù)若存在,則Vue構(gòu)造函數(shù)不會(huì)從template選項(xiàng)或通過 el 選項(xiàng)指定的掛載元素中提取出的HTML模板編譯渲染函數(shù)。
原先原型上的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)
}
這個(gè)是公用的$mount方法,這么設(shè)計(jì)使得這個(gè)方法可以被 runtime only和runtime+compiler 版本共同使用
$mount 第一個(gè)參數(shù)el, 表示掛載的元素,在瀏覽器環(huán)境會(huì)通過query(el)獲取到dom對(duì)象,第二個(gè)參數(shù)和服務(wù)端渲染相關(guān),不進(jìn)行深入分析,此處不傳。接著調(diào)用mountComponent()
看下query(),比較簡(jiǎn)單,當(dāng)el是string時(shí),找到該選擇器返回dom對(duì)象,否則新創(chuàng)建個(gè)div dom對(duì)象,el是dom對(duì)象直接返回el.
mountComponent
mountComponent定義在src/core/instance/lifecycle.js中,傳入vm,el,
將
el緩存在vm.$el上判斷有沒有
render方法,沒有則直接把createEmptyVNode作為render函數(shù)開發(fā)環(huán)境警告(沒有
Render但有el/template不能使用runtime-only版本、render和template必須要有一個(gè))掛載
beforeMount鉤子-
定義
updateComponent, 渲染相關(guān)updateComponent = () => { vm._update(vm._render(), hydrating) } -
new Watcher()實(shí)例化一個(gè)渲染watcher,簡(jiǎn)單看下定義,
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 }執(zhí)行
this.get(),則執(zhí)行了this.getter,即updateComponent,所以new Watcher()時(shí)會(huì)執(zhí)行updateComponent,也就會(huì)執(zhí)行到vm._update、vm._render方法。因?yàn)橹蟛恢钩跏蓟瘯r(shí)需要渲染頁面,數(shù)據(jù)發(fā)生變化時(shí)也是要更新到dom上的,實(shí)例watcher可以實(shí)現(xiàn)對(duì)數(shù)據(jù)進(jìn)行監(jiān)聽以及隨后的更新
dom處理,watcher會(huì)在初始化執(zhí)行回調(diào),也會(huì)在數(shù)據(jù)變化時(shí)執(zhí)行回調(diào),此處先簡(jiǎn)單介紹為什么要使用watcher,不深入分析watcher實(shí)現(xiàn)原理。 最后判斷有無根節(jié)點(diǎn),無則表示首次掛載,添加
mounted鉤子函數(shù) ,返回vm
總結(jié)
實(shí)例初始化:new Vue()->掛載方法屬性->this._init->初始化data->$mount
掛載過程:(在complier版本,生成render函數(shù))對(duì)el作處理,執(zhí)行mountComponent,mountComponent中定義了updateComponent,通過實(shí)例化watcher的回調(diào)執(zhí)行updateComponent,執(zhí)行updateComponent,即調(diào)用了vm._update、vm._render真實(shí)渲染成dom對(duì)象。