項(xiàng)目文件結(jié)構(gòu)
在Vue
項(xiàng)目中岔激,所有核心的代碼都是在src
目錄下完成,為了更好的了解Vue
的底層實(shí)現(xiàn)是掰,我們首先來了解一下src
目錄下代碼的組織情況虑鼎,從全局入手,在腦海里留下簡單的印象,方便后續(xù)的學(xué)習(xí)炫彩。(注意:當(dāng)前使用Vue
的版本為2.6.12
匾七,不同版本的內(nèi)容可能會有所差異)
.
├── compiler // 編譯模塊:將 template 編譯成為可以生成 vnode 的 render 函數(shù)
│ ├── codeframe.js
│ ├── codegen // 代碼生成文件:根據(jù) ast 樹可生成 vnode 的 render代碼
│ ├── create-compiler.js // 創(chuàng)建編譯器的工廠函數(shù)
│ ├── directives // 指令解析:v-on, v-bind, v-model
│ ├── error-detector.js
│ ├── helpers.js // 編譯相關(guān)方法,如屬性獲取等方法
│ ├── index.js // 入口文件
│ ├── optimizer.js // 編譯優(yōu)化:將 ast 樹進(jìn)行優(yōu)化
│ ├── parser // html 解析文件:將 template 解析成 ast 樹??
│ └── to-function.js // 創(chuàng)建編譯器的工廠函數(shù)
├── core // 構(gòu)造函數(shù)核心模塊:構(gòu)建Vue構(gòu)造函數(shù)江兢,添加原型方法昨忆,實(shí)現(xiàn)完成渲染流程的_init方法
│ ├── components // 自帶的全局組件,如 keep-alive
│ ├── config.js // 配置相關(guān)
│ ├── global-api // 全局api杉允,如 Vue.use, extend, mixin, component等方法
│ ├── index.js // 入口文件邑贴,在 Vue 上掛載全局方法并導(dǎo)出 Vue
│ ├── instance // 構(gòu)造函數(shù)起始位置
│ ├── observer // 響應(yīng)式原理
│ ├── util // 一些工具方法,包含 mergeOptions, nextTick 等方法的實(shí)現(xiàn)
│ └── vdom // 虛擬 dom
├── platforms // 平臺相關(guān)夺颤,包含不同平臺的不同構(gòu)建入口痢缎,這里主要研究web端
│ ├── weex
│ └── web
│ ├── compiler // 與平臺相關(guān)的編譯
│ ├── entry-compiler.js // vue-template-compiler 包的入口文件
│ ├── entry-runtime-with-compiler.js // 構(gòu)建入口,包含編譯器
│ ├── entry-runtime.js // 構(gòu)建入口世澜,不包含編譯器独旷,不支持 template 轉(zhuǎn)換 render
│ ├── entry-server-basic-renderer.js
│ ├── entry-server-renderer.js
│ ├── runtime // 與平臺相關(guān)的構(gòu)建
│ ├── server
│ └── util
│
├── server // 服務(wù)端渲染相關(guān)
├── sfc // 包含單文件組件(.vue文件)的解析邏輯,用于vue-template-compiler包
└── shared // 代碼庫通用代碼
├── constants.js
└── util.js
以上是Vue
項(xiàng)目中主要文件目錄寥裂,里面附帶一些注釋嵌洼,講解了比較主要模塊的功能及作用。剛開始學(xué)習(xí)時(shí)只做簡單了解即可封恰,后面我們會逐步詳細(xì)學(xué)習(xí)其中的一些模塊麻养,從而從原理級別理解整個(gè)Vue
項(xiàng)目的設(shè)計(jì)與實(shí)現(xiàn)。
Vue
的真面目
要想真正的了解Vue
是怎樣的诺舔,首先我們需要找到Vue
是咋哪里被定義的鳖昌。我們先找到package.json
文件下的scripts
配置。scripts
里存放的都是運(yùn)行命令的別名形式低飒,通過命令可以輕松找到對應(yīng)命令執(zhí)行文件的路徑许昨。
"scripts": {
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
}
這里當(dāng)運(yùn)行dev
命令實(shí)際上是運(yùn)行scripts/config.js
文件,讓我們找到scripts/config.js
文件褥赊。
通過運(yùn)行命令參數(shù)我們可以知道process.env.TARGET
的值為web-full-dev
糕档,因此可以在builds
里找到對應(yīng)的配置文件,如下
const builds = {
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
}
...
}
module.exports = genConfig(process.env.TARGET)
通過entry
拌喉,我們找到web/entry-runtime-with-compiler.js
文件:
import Vue from './runtime/index'
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
...
}
Vue.compile = compileToFunctions
export default Vue
在這里速那,我們終于找到了Vue
相關(guān)的文件,這也是Vue
的起始入口尿背。接著根據(jù)Vue
的引入路徑端仰,找到./runtime/index
文件:
import Vue from 'core/index'
...
Vue.prototype.__patch__ = inBrowser ? patch : noop
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
...
}
export default Vue
這里還不是Vue
真正的起始點(diǎn),繼續(xù)查找core/index
文件:
import Vue from './instance/index'
initGlobalAPI(Vue)
...
Vue.version = '__VERSION__'
export default Vue
發(fā)現(xiàn)仍然不是Vue
的起始點(diǎn)田藐,繼續(xù)查找'./instance/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'
// Vue 構(gòu)造函數(shù)
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
好了榆俺,大功告成!費(fèi)勁千辛萬苦終于找到了Vue
的真正定義的位置!可以看出Vue
其實(shí)就是一個(gè)構(gòu)造函數(shù)茴晋,而構(gòu)造函數(shù)內(nèi)部僅僅只是調(diào)用了_init
方法陪捷,看上去非常簡單。但是Vue
是如何通過這么簡單的定義實(shí)現(xiàn)那么復(fù)雜的功能呢诺擅?這里就要涉及到構(gòu)造函數(shù)
市袖、原型
、實(shí)例
的概念了烁涌,不了解這些概念的建議參考《javascript高級設(shè)計(jì)程序》中原型章節(jié)來進(jìn)行學(xué)習(xí)苍碟。下面我們通過下方三個(gè)方面來介紹Vue
的實(shí)現(xiàn)。
- 原型方法屬性:通過 5 個(gè)
init
方法撮执,向Vue
的原型上添加方法微峰, - 靜態(tài)方法屬性:在導(dǎo)入
Vue
構(gòu)造函數(shù)的過程中,向Vue
構(gòu)造函數(shù)上添加靜態(tài)方法抒钱,也有向原型上添加方法 - 實(shí)例化:在實(shí)例化的過程中蜓肆,執(zhí)行
_init
方法,完成整個(gè)Vue
初始化到渲染的邏輯谋币。
Vue的原型方法(通過5個(gè)init
方法添加)
initMixin
initMixin
方法主要實(shí)現(xiàn)了_init
方法仗扬。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// init 實(shí)現(xiàn)內(nèi)容,由于這里僅做概覽蕾额,所以具體實(shí)現(xiàn)均已省略
...
}
}
從上面Vue
構(gòu)造函數(shù)我們可以知道早芭,這個(gè)方法在實(shí)例化時(shí)有被調(diào)用,它主要的作用是實(shí)現(xiàn):選項(xiàng)的合并诅蝶,數(shù)據(jù)初始化(如響應(yīng)式處理)退个,以及觸發(fā)編譯和渲染的流程,所以十分重要调炬。這里也只是先做一個(gè)了解帜乞,后續(xù)的實(shí)例化章節(jié)將都會從這個(gè)方法開始分析。
stateMixin
stateMixin
主要實(shí)現(xiàn)了data,props
的代理功能筐眷,即當(dāng)我們訪問$data
時(shí),實(shí)際訪問的是_data
习柠。另外在非生產(chǎn)環(huán)境下匀谣,會對$data,$props
進(jìn)行 set
處理,每次設(shè)置新的值時(shí)都會打印提示资溃,所以實(shí)際上$data,$props
都是只讀屬性武翎。
export function stateMixin (Vue: Class<Component>) {
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
// 只讀屬性
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function () { ... }
}
除此之外,這里還在Vue
原型上掛載了比較常見的三個(gè)方法:$set
溶锭,$delete
宝恶,$watch
。
eventsMixin
和node
里EventEmitter
類似,eventsMixin
實(shí)現(xiàn)了四個(gè)方法:$on,$off,$once,$emit
垫毙,用于監(jiān)聽霹疫,觸發(fā),銷毀事件综芥。
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
Vue.prototype.$on = function () { ... }
Vue.prototype.$once = function () { ... }
Vue.prototype.$off = function () { ... }
Vue.prototype.$emit = function () { ... }
}
lifecycleMixin
lifecycleMixin
實(shí)現(xiàn)了三個(gè)方法:_update
方法非常重要丽蝎,它主要負(fù)責(zé)將vnode
生成真實(shí)節(jié)點(diǎn)。
export function lifecycleMixin (Vue: Class<Component>) {
// 更新膀藐,將 vnode 生成 真實(shí)節(jié)點(diǎn)
Vue.prototype._update = function () { ... }
// 強(qiáng)制刷新
Vue.prototype.$forceUpdate = function () { ... }
// 銷毀
Vue.prototype.$destroy = function () { ... }
}
renderMixin
renderMixin
主要做了三項(xiàng)工作
export function renderMixin (Vue: Class<Component>) {
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
Vue.prototype._render = function (): VNode {
return vnode
}
}
-
installRenderHelpers
函數(shù)用于添加render
相關(guān)方法屠阻,在編譯環(huán)節(jié)最后生成的代碼,都是由這些方法拼接而成的代碼额各,所以也是非常的重要国觉,在這里先混個(gè)眼熟。
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
-
$nextTick
方法虾啦,在下一次事件循環(huán)觸發(fā)麻诀,涉及到事件循環(huán)機(jī)制。 -
_render
方法缸逃,用于生成vnode
针饥。
Vue的靜態(tài)方法屬性
通過上面5個(gè)init
方法我們已經(jīng)了解了許多原型方法的添加過程,但是在Vue
中還有很多全局方法需频,比如Vue.component,Vue.use
等方法丁眼,它們都是構(gòu)造函數(shù)的靜態(tài)屬性,下面我們看看這些靜態(tài)屬性是如何添加的昭殉。與尋找Vue
的起始位置過程恰恰相反苞七,這次我們從Vue
的起始文件出發(fā),看看最后導(dǎo)出的Vue
是怎樣的挪丢。
/src/core/index.js
文件
這是第一層引入Vue
構(gòu)造函數(shù)的文件
import { initGlobalAPI } from './global-api/index'
initGlobalAPI(Vue)
// ... 中間省略
Vue.version = '__VERSION__'
這里我們看一下initGlobalAPI
方法保屯,打開core/global-api/index.js
文件
export function initGlobalAPI (Vue: GlobalAPI) {
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
這里掛載了很多靜態(tài)方法,Vue
中大多數(shù)的全局方法都在這個(gè)位置添加的嗡呼,這里我們著重分析一下options
:
import builtInComponents from '../components/index'
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
可以看出壁榕,在Vue
構(gòu)造函數(shù)上添加了一個(gè)options
屬性(注意!這里是靜態(tài)屬性任内,為構(gòu)造函數(shù)所有撵渡,區(qū)別于在實(shí)例化傳入的options
)。隨后又通過遍歷ASSET_TYPES
死嗦,在options
上添加了components,directives,filters
方法趋距。另外還添加了_base
,指向當(dāng)前構(gòu)造函數(shù)越除。最后通過extend
方法將builtInComponents
合并到options.components
當(dāng)中节腐。這里的builtInComponents
實(shí)際上就是Vue
自帶的組件外盯,即keep-alive
組件。所以最終Vue.options
的內(nèi)容如下:
// Vue.options 內(nèi)容
{
components: {
KeepAlive
},
filters: {},
directives: {},
_base: Vue
}
這里之所以額外提起翼雀,是因?yàn)樵诤罄m(xù)選項(xiàng)合并時(shí)饱苟,會使用此處的options
進(jìn)行合并。
/src/platforms/web/runtime/index.js
文件
這里是第二層引入Vue
的文件锅纺,主要給Vue
處理平臺相關(guān)的一些方法
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 } 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)
Vue.prototype.__patch__ = inBrowser ? patch : noop
Vue.prototype.$mount = function () { ... }
export default Vue
這里首先給Vue.config
添加了一系列方法掷空,注意,這些方法之所以在這里添加而不是在core/index.js
文件里添加囤锉,是因?yàn)檫@里的方法都與平臺相關(guān)坦弟,不同的平臺的方法實(shí)現(xiàn)也會不一樣。
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
這兩個(gè)extend
實(shí)際上進(jìn)一步擴(kuò)充了Vue.options
方法官地,擴(kuò)充后的內(nèi)容如下
// Vue.options 內(nèi)容
{
components: {
KeepAlive,
// 新增 platformComponents
Transition,
// 新增 platformComponents
TransitionGroup
},
filters: {},
directives: {
// 新增 platformDirectives
model,
// 新增 platformDirectives
show
},
_base: Vue
}
這也是為什么我們可以不用注冊也能全局使用v-model,v-show
的原因了酿傍,因?yàn)?code>Vue已經(jīng)幫我們?nèi)肿粤恕?/p>
/src/platforms/web/entry-runtime-with-compiler.js
文件
這是最后一層引入Vue
了
import Vue from './runtime/index'
...
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
...
return mount.call(this, el, hydrating)
}
Vue.compile = compileToFunctions
export default Vue
這里主要是重新實(shí)現(xiàn)了$mount
方法,但是為什么原先在runtime/index.js
文件里實(shí)現(xiàn)了$mount
方法驱入,這里又要重新實(shí)現(xiàn)一遍呢赤炒?因?yàn)?code>runtime/index.js里的$mount
與編譯是無關(guān)的,無法處理template
模板代碼亏较,而這里重寫的$mount
實(shí)際上還是調(diào)用了runtime/index.js
里的$mount
莺褒,但是在此之前,增加了從template
到render
的編譯過程雪情。
實(shí)例化過程
前面已經(jīng)將Vue
的各種方法屬性掛載完畢遵岩,現(xiàn)在則是需要進(jìn)行實(shí)例化了,也就是調(diào)用之前提到的_init
方法巡通。打開/src/core/instance/init.js
文件尘执,代碼如下:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
...
// 1. 合并options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
...
// 2. 初始化數(shù)據(jù)
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')
// 3. 掛載
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
在初始化的過程中,主要分為三個(gè)階段:
階段一:合并選項(xiàng)宴凉,將 Vue.options
和傳入的options
進(jìn)行合并
階段二:初始化數(shù)據(jù)誊锭,并對數(shù)據(jù)進(jìn)行響應(yīng)式處理
階段三:編譯代碼,得到render
函數(shù)弥锄,將vnode
生成真實(shí)節(jié)點(diǎn)丧靡,并掛載到界面
由于這部分比較核心,且難以理解籽暇,這里僅做了解温治,后續(xù)會逐一進(jìn)行分析。
Vue的整體設(shè)計(jì)
通過上面的分析图仓,我們已經(jīng)對Vue原型方法
,Vue靜態(tài)方法屬性
但绕,Vue實(shí)例化過程
有了大致的了解救崔,下面我們用張圖總結(jié)下整體的內(nèi)容惶看,也就是Vue整體的設(shè)計(jì)思路。
總結(jié)下來就是:
- 構(gòu)建一個(gè)具有完備功能的構(gòu)造函數(shù)六孵,因此在上面添加各個(gè)模塊需要的方法屬性纬黎。包括原型方法屬性和靜態(tài)方法屬性。
- 進(jìn)行實(shí)例化劫窒,在實(shí)例化過程中進(jìn)行各種處理本今,其中包括:選項(xiàng)合并,數(shù)據(jù)響應(yīng)式處理主巍,編譯冠息,虛擬
DOM
更新等等。
這里的描述比較籠統(tǒng)孕索,旨在從整體上來對Vue進(jìn)行一個(gè)了解逛艰。在接下來的章節(jié)我們會詳細(xì)分析實(shí)例化的整個(gè)過程,從而由點(diǎn)及面的了解Vue搞旭。下一章節(jié)我們將開始Vue
核心代碼的正式學(xué)習(xí)散怖。