大家都知道烤黍,閱讀源碼可以幫助自己成長。源碼解析的文章也看了不少傻盟,但是好記性不如爛筆頭速蕊,看過的東西過段時間就忘的差不多了,所以還是決定自己動手記一記娘赴。
首先看下項目目錄规哲,大致知道每個文件夾下面都是干什么的
當我們閱讀一個項目源碼的時候败去,首先看它的package.json文件纠修,這里包含了項目的依賴脖岛、執(zhí)行腳本等褥伴,可以幫助我們快速找到項目的入口乓序。
我們來看幾個重要字段:
// main和module指定了加載的入口文件裂逐,它們都指向運行時版的Vue钩骇,
"main": "dist/vue.runtime.common.js",
"module": "dist/vue.runtime.esm.js",
當打包工具遇到該模塊時:
- 如果已經(jīng)支持pkg.module字段蜀铲,會優(yōu)先使用es6模塊規(guī)范的版本泛啸,這樣可以啟用tree shaking機制
- 否則根據(jù)main字段的配置加載绿语,使用已經(jīng)編譯成CommonJS規(guī)范的版本。
webpack2+和rollup都已經(jīng)支持pkg.module, 會根據(jù)module字段的配置進行加載
接下來看一下scripts里面部分腳本配置:
"scripts": {
// 構(gòu)建完整版umd模塊的Vue
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
// 構(gòu)建運行時cjs模塊的Vue
"dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
// 構(gòu)建運行時es模塊的Vue
"dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
// 構(gòu)建web-server-renderer包
"dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
"dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer"
},
umd讓我們可以直接用script標簽來引用Vue
cjs形式的模塊是為browserify 和 webpack 1 提供的候址,他們在加載模塊的時候不能直接加載ES Module
webpack2+ 以及 Rollup可以直接加載ES Module吕粹,es形式的模塊是為它們服務(wù)的
接下來,我們將基于dev腳本進行分析
當我們執(zhí)行npm run dev命令時岗仑,
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
可以看到配置文件是scripts/config.js匹耕,傳給配置文件的TARGET變量的值是‘web-full-dev’。
在配置文件的最后荠雕,是這樣一段代碼:
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
因為process.env.TARGET有值稳其,所以執(zhí)行的是if里面的代碼。根據(jù)process.env.TARGET === 'web-full-dev', 我們看到這樣一段配置:
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'), // 入口文件
dest: resolve('dist/vue.js'), // 最終輸出文件
format: 'umd', // umd模塊
env: 'development',
alias: { he: './entity-decoder' },
banner
},
現(xiàn)在我們知道了入口文件是'web/entry-runtime-with-compiler.js'舞虱,但是web是指的哪一個目錄呢欢际?在scripts下面有一個alias.js文件,里面定義了一些別名:
module.exports = {
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
compiler: resolve('src/compiler'),
core: resolve('src/core'),
shared: resolve('src/shared'),
web: resolve('src/platforms/web'),
weex: resolve('src/platforms/weex'),
server: resolve('src/server'),
entries: resolve('src/entries'),
sfc: resolve('src/sfc')
}
可以看到web是指的'src/platforms/web'矾兜,所以入口文件的全路徑就是src/platforms/web/entry-runtime-with-compiler.js
我們使用Vue的時候损趋,是用new
關(guān)鍵字進行調(diào)用的,這說明Vue是一個構(gòu)造函數(shù),接下來我們就從入口文件開始扒一扒Vue構(gòu)造函數(shù)是咋個情況浑槽。
尋找Vue構(gòu)造函數(shù)的位置
打開入口文件src/platforms/web/entry-runtime-with-compiler.js
蒋失,我們看到這樣一句代碼
import Vue from './runtime/index'
這說明Vue是從別的文件引進來的,接著打開./runtime/index
文件桐玻,看到
import Vue from 'core/index'
說明這里也不是Vue的出生地篙挽,接著尋找。打開core/index
镊靴,根據(jù)別名配置可以知道铣卡,core是指的'src/core'目錄。Vue依然是引入的
import Vue from './instance/index'
沒辦法偏竟,接著找煮落。在./instance/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)
}
長吁一口氣,Vue構(gòu)造函數(shù)終于找到源頭了踊谋。最后我們再理一下這個路徑
src/platforms/web/entry-runtime-with-compiler.js.
——> src/platforms/web/runtime/index.js
——> src/core/index.js
——> src/core/instance/index.js
接下來我們從出生地開始一一來看
Vue構(gòu)造函數(shù)——實例屬性和方法
來看一下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'
// 定義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)
// 在Vue的原型上添加了_init方法蝉仇。在執(zhí)行new Vue()的時候,this._init(options)被執(zhí)行
stateMixin(Vue)
// 在vue的原型上定義了屬性: $data殖蚕、$props轿衔,方法:$set、$delete睦疫、$watch
eventsMixin(Vue)
// 在原型上添加了四個方法: $on $once $off $emit
lifecycleMixin(Vue)
// 在Vue.prototye上添加了三個方法:_update $forceUpdate $destory
renderMixin(Vue)
// 在原型上添加了方法:$nextTick _render _o _n _s _l _t _q _i _m _f _k _b _v _e _u _g _d _p
export default Vue
該文件主要是定義了Vue構(gòu)造函數(shù)害驹,然后又以Vue為參數(shù),執(zhí)行了initMixin蛤育、stateMixin裙秋、eventsMixin、lifecycleMixin缨伊、renderMixin這五個方法摘刑。
Vue構(gòu)造函數(shù)首先檢查了是不是用new
關(guān)鍵字調(diào)用的,然后調(diào)用了_init方法刻坊。
接下來五個方法分別在Vue的原型上添加了各種屬性和方法枷恕。首先來看initMixin
initMixin
打開'./init'文件,找到initMixin方法谭胚,發(fā)現(xiàn)它其實只做了一件事:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
...
}
}
就是在Vue.prototype上掛載了_init方法徐块,在執(zhí)行new Vue()的時候,該方法會執(zhí)行灾而。
stateMixin
export function stateMixin (Vue: Class<Component>) {
// flow somehow has problems with directly declared definition object
// when using Object.defineProperty, so we have to procedurally build up
// the object here.
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
if (process.env.NODE_ENV !== 'production') { // 不是生產(chǎn)環(huán)境胡控,設(shè)置set
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
// $data 和 $props是只讀屬性
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
...
}
這個方法首先在Vue.prototype上定義了兩個只讀屬性$data
和$props
。為什么是只讀屬性呢旁趟?因為為屬性設(shè)置set的時候有一個判斷昼激,不能是生產(chǎn)環(huán)境。
然后在原型上定義了三個方法:$set
, $delete
, $watch
eventsMixin
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
}
這里面是在原型上掛載了四個方法,這幾個方法平時也都經(jīng)常用到橙困,肯定很熟悉
lifecycleMixin
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
}
添加了三個生命周期相關(guān)的實例方法:
- _update:
- $forceUpdate: 迫使Vue實例重新渲染瞧掺,包括其下的子組件
- $destory: 完全銷毀一個實例, 觸發(fā)生命周期beforeDestroy和destroyed
renderMixin
export function renderMixin (Vue: Class<Component>) {
// install runtime convenience helpers
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
Vue.prototype._render = function (): VNode {}
}
首先是以Vue.prototype為參數(shù)調(diào)用了installRenderHelpers方法,來看一下這個方法干了啥:
export function installRenderHelpers (target: any) {
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
}
也是在原型上掛載了各種方法, 用于構(gòu)造render函數(shù)凡傅。
之后又在原型上掛載了兩個實例方法$nextTick
和_render
至此我們大致了解了instance/index.js
里面的內(nèi)容辟狈,就是包裝了Vue.prototyp,在其上掛載了各種屬性和方法夏跷。
Vue構(gòu)造函數(shù)——掛載全局API
接下來來看/src/core/index
文件
/**
* 添加全局API哼转,在原型上添加了兩個屬性$isServer和$ssrContext,加了version版本屬性
*/
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
// 在 Vue 構(gòu)造函數(shù)上添加全局的API
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
// 存儲了當前Vue的版本號
Vue.version = '__VERSION__'
export default Vue
首先是導(dǎo)入了構(gòu)造函數(shù)Vue和其他三個變量槽华,接下來就是以Vue構(gòu)造函數(shù)為參數(shù)調(diào)用了initGlobalAPI方法释簿,該方法來自./global-api/index
。我們先把下面的內(nèi)容看完再回過頭來分析該方法硼莽。
接下來是在Vue.prototype上面掛載了兩個只讀屬性$isServer
和$ssrContext
。之后又在Vue構(gòu)造函數(shù)上添加了FunctionalRenderContext屬性煮纵,根據(jù)注釋知道該屬性是在ssr中用到的懂鸵。
最后在Vue構(gòu)造函數(shù)上添加了靜態(tài)屬性version,其值是__VERSION__
行疏,這是個什么鬼匆光?打開/scripts/config.js
,可以看到這么一句代碼:
__VERSION__: version
而version的值在文件最上面可以看到:
process.env.VERSION || require('../package.json').version
所以最終的值就是Vue的版本酿联。
我們再回過頭來看一下initGlobalAPI函數(shù)终息,從函數(shù)名可以猜出它應(yīng)該是定義全局API的,其實也就是這樣贞让。
先看前部分代碼
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef) // 只讀屬性
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
// 上面意思就是輕易不要用周崭,有風險
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
}
先是定義了只讀屬性config。接著定義了util屬性喳张,并且在util上掛載了四個方法续镇。只不過util以及它下面的方法不被視為公共API的一部分,要避免使用销部,除非你可以控制風險摸航。
接著就是在Vue上添加了四個屬性:set、delete舅桩、nextTick酱虎、observable.
然后定義了一個空對象options
Vue.options = Object.create(null)
之后通過循環(huán)填充屬性:
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
ASSET_TYPES的值通過查找對應(yīng)文件后知道為['component', 'directive', 'filter'],所以循環(huán)之后options對象變?yōu)椋?/p>
Vue.options = {
components: Object.create(null),
directives: Object.create(null),
filters: Object.create(null)
}
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
這是在options上添加了_base屬性
接下來是這句代碼
// 將builtInComponents的屬性混合到Vue.options.components中
extend(Vue.options.components, builtInComponents)
extend 來自于 shared/util.js 文件,代碼也很簡單
/**
* Mix properties into target object.
*/
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
builtInComponents 來自于 core/components/index.js 文件
import KeepAlive from './keep-alive'
export default {
KeepAlive
}
現(xiàn)在為止擂涛,Vue.options變成
Vue.options = {
components: {
KeepAlive
},
directives: Object.create(null),
filters: Object.create(null),
_base: Vue
}
在函數(shù)的最后读串,調(diào)用了四個方法:
// 在Vue構(gòu)造函數(shù)上添加use方法,Vue.use()用來安裝Vue插件
initUse(Vue)
// 添加全局API:Vue.mixin()
initMixin(Vue)
// 添加Vue.cid靜態(tài)屬性 和 Vue.extend 靜態(tài)方法
initExtend(Vue)
// 添加靜態(tài)方法:Vue.component Vue.directive Vue.filter
// 全局注冊組件、指令爹土、過濾器
initAssetRegisters(Vue)
我們先大致了解這幾個方法的作用甥雕,至于具體實現(xiàn)以后再詳細分析。
第二個階段大體就了解完了胀茵,就是掛載靜態(tài)屬性和方法社露。
Vue平臺化包裝
接下來來看platforms/web/runtime/index.js
文件,我們之前看的兩個文件是在core目錄下的琼娘,是Vue的核心文件峭弟,與平臺無關(guān)的。platforms下面的就是針對特定平臺對Vue進行包裝脱拼。主要分兩個平臺:web和weex, 我們看的是web平臺下的內(nèi)容瞒瘸。
首先是安裝特定平臺的工具函數(shù)
// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
Vue.config我們之前見過,它代理的是/src/core/config.js
文件拋出的內(nèi)容熄浓,現(xiàn)在是重寫了其中部分屬性情臭。
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
這是安裝平臺運行時的指令和組件。extend的作用我們都已經(jīng)知道了赌蔑。來看一下platformDirectives和platformComponents的內(nèi)容俯在。
platformDirectives:
import model from './model'
import show from './show'
export default {
model,
show
}
platformComponents:
import Transition from './transition'
import TransitionGroup from './transition-group'
export default {
Transition,
TransitionGroup
}
Vue.options之前已經(jīng)有過包裝,經(jīng)過這兩句代碼之后變成:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: Object.create(null),
_base: Vue
}
繼續(xù)看下面的代碼
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
這是添加了兩個實例方法:__patch__
和 $mount
看完之后我們就知道了該文件的作用
- 設(shè)置平臺化的Vue.config
- 在Vue.options上混合了兩個指令:
model
和show
- 在Vue.options上混合了兩個組件:
Transition
和TransitionGroup
- 在Vue.prototye上添加了兩個方法:
__patch__
和$mount
compiler
到目前為止娃惯,運行時版本的Vue已經(jīng)構(gòu)造完了跷乐。但是我們的入口是entry-runtime-with-compiler.js
文件,從文件名可以看出來這里是多了一個compiler趾浅。我們來看看這個文件吧
// 獲取擁有指定ID屬性的元素的innerHTML
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function ( // 重寫了$mount方法
el?: string | Element,
hydrating?: boolean
): Component {}
/**
* Get outerHTML of elements, taking care
* of SVG elements in IE as well.
*/
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
}
}
// 添加compile全局API
Vue.compile = compileToFunctions
export default Vue
這個文件主要是重寫了Vue.prototype.$mount
方法愕提,添加了Vue.compile
全局API
以上,我們從Vue構(gòu)造函數(shù)入手皿哨,大致梳理了項目的脈絡(luò)浅侨。理清楚了大體流程,之后再慢慢探索細節(jié)证膨。