<p style="font-size: 2rem;color: red;margin-bottom: 1.5rem">前方高能?。。?lt;/p>
這只是一篇個人學(xué)習(xí)Vue.js源碼的筆記,并非教程,鑒于個人水平有限,可能存在錯誤,還望各路大神指點(diǎn)
文章內(nèi)容極度粗俗,各種無腦分析,各種瘋狂輸出,各位看官斟酌而行,切勿走火入魔?。?!
Vue.js版本 --2.1.7
之所以選擇這個是因?yàn)榭戳诉@位大神的分析,決定采用同一個版本,目前Vue已經(jīng)發(fā)布了2.5.x了
這里極力推薦大家去看看,據(jù)說這位大神的兩篇源碼分析都是經(jīng)過尤大佬推薦的哦,本文作為第一篇
也是參考大神的文章作為開頭,參考了極大部分再加上自己的理解而寫的
JavaScript實(shí)現(xiàn)MVVM之我就是想監(jiān)測一個普通對象的變化
src/core/instance/index.js
這是整個vue的入口文件,首先引入vue后我們只是引入了一個構(gòu)造函數(shù),所以
一般我們初始化的時候都是用new Vue的方式啟動,即下面的Vue函數(shù),里面執(zhí)行了一句
this._init(options),這個options即是我們傳入的各種參數(shù),即
new Vue({
el: '#app',
data: {
name: 'zhang san',
age: 18
}
})
下面是src/core/instance/index.js的源碼,其中引入vue的時候馬上就初始化了5個mixin
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
引入依賴,定義 Vue 構(gòu)造函數(shù),然后以Vue構(gòu)造函數(shù)為參數(shù),調(diào)用了五個方法,最后導(dǎo)出 Vue。這五個方法分別來自五個文件:init.js state.js render.js events.js 以及 lifecycle.js。
打開這五個文件,找到相應(yīng)的方法,你會發(fā)現(xiàn),這些方法的作用,就是在 Vue 的原型 prototype 上掛載方法或?qū)傩?,?jīng)歷了這五個方法后的Vue會變成這樣:
// initMixin(Vue) src/core/instance/init.js **************************************************
Vue.prototype._init = function (options?: Object) {}
// stateMixin(Vue) src/core/instance/state.js **************************************************
Vue.prototype.$data
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function(){}
// renderMixin(Vue) src/core/instance/render.js **************************************************
Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}
Vue.prototype._s = _toString
Vue.prototype._v = createTextVNode
Vue.prototype._n = toNumber
Vue.prototype._e = createEmptyVNode
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = function(){}
Vue.prototype._o = function(){}
Vue.prototype._f = function resolveFilter (id) {}
Vue.prototype._l = function(){}
Vue.prototype._t = function(){}
Vue.prototype._b = function(){}
Vue.prototype._k = function(){}
// eventsMixin(Vue) src/core/instance/events.js **************************************************
Vue.prototype.$on = function (event: string, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
// lifecycleMixin(Vue) src/core/instance/lifecycle.js **************************************************
Vue.prototype._mount = function(){}
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype._updateFromParent = function(){}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
這樣就結(jié)束了嗎?并沒有,根據(jù)我們之前尋找 Vue 的路線,這只是剛剛開始,我們追溯路線往
回走,那么下一個處理 Vue 構(gòu)造函數(shù)的應(yīng)該是 src/core/index.js 文件,我們打開它:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Vue.version = '__VERSION__'
export default Vue
我們則從這里作為第一出發(fā)點(diǎn)開始
首先這里說下這個isServerRendering,找到這個env文件,里面有個方法
export const inBrowser = typeof window !== 'undefined'
let _isServer
export const isServerRendering = () => {
if (_isServer === undefined) {
/* istanbul ignore if */
if (!inBrowser && typeof global !== 'undefined') {
// detect presence of vue-server-renderer and avoid
// Webpack shimming the process
_isServer = global['process'].env.VUE_ENV === 'server'
} else {
_isServer = false
}
}
return _isServer
}
這段代碼判斷是否為服務(wù)端,const inBrowser = typeof window !== 'undefined'這段代碼
大概就明白了
然后綁定在構(gòu)造函數(shù)的原型的一個屬性$isServer上面
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
然后再說initGlobalAPI方法的執(zhí)行
src\core\global-api\index.js
這個文件導(dǎo)出一個函數(shù),函數(shù)接受Vue構(gòu)造函數(shù)作為參數(shù)
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
util.warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
Vue.util = util
Vue.set = set
Vue.delete = del
Vue.nextTick = util.nextTick
Vue.options = Object.create(null)
config._assetTypes.forEach(type => {
Vue.options[type + 's'] = 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
util.extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
首先定義一個對象configDef,configDef.get關(guān)聯(lián)到了config,打開src\core\config.js
里面其實(shí)就是導(dǎo)出一個對象,對象里面有很多的屬性,其中有
_assetTypes: [
'component',
'directive',
'filter'
],
_lifecycleHooks: [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated'
],
這兩個便是常見的屬性和生命周期,然后判斷在開發(fā)環(huán)境下則有個set方法用來發(fā)出提示的
最后Object.defineProperty(Vue, 'config', configDef),定義構(gòu)造函數(shù)的一個靜態(tài)
屬性config,分別有g(shù)et和set的方法的,然后綁定util,set,delete,nextTick在Vue
構(gòu)造函數(shù)上面,然后創(chuàng)建一個options空對象,然后遍歷config的_assetTypes,也就是
上面說的三個屬性,把它們的名字加上s復(fù)數(shù),即components,directives,filters綁定
再options下面的屬性上,三個都是空對象,<font color=#FF0000>再有一個_base = Vue
是自身的賦值,這里暫時不知道為什么要這么做,可能是為了緩存吧</font>,然后執(zhí)行
util.extend(Vue.options.components, builtInComponents),這里是把自身的默認(rèn)
組件KeepAlive綁定到了options里面,即
Vue.config
Vue.util = util
Vue.set = set
Vue.delete = del
Vue.nextTick = util.nextTick
Vue.options = {
components: {
KeepAlive
},
directives: {},
filters: {},
_base: Vue
}
最后執(zhí)行4個方法
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
這4個方法對應(yīng)當(dāng)前目錄的4個js文件,首先是use.js,這里給Vue掛載了靜態(tài)方法,并非原型上的
這個我們用的比較多,用來安裝插件的,比如Vue.use(VueRouter)路由插件就是這樣子,這里首先
判斷installed是否安裝過了,然后執(zhí)行toArray方法,返回一個數(shù)組,toArray方法解釋在
methods realizes目錄下找到對應(yīng)名字文件夾,然后args.unshift(this)在這個返回的數(shù)組頭部
添加Vue構(gòu)造函數(shù),然后執(zhí)行這個install方法,一般插件的install方法形式如官方文檔所寫
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或?qū)傩? Vue.myGlobalMethod = function () {
// 邏輯...
}
// 2. 添加全局資源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 邏輯...
}
})
// 3. 注入組件
Vue.mixin({
created: function () {
// 邏輯...
}
})
// 4. 添加實(shí)例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 邏輯...
}
}
Vue.use(MyPlugin)
這里就是執(zhí)行這個install方法了,即調(diào)用Vue的全局方法,注冊指令,在prototype上掛載方法
或者使用全局mixin等等,這里還有個else語句,也就是install不是函數(shù)的情況,那么這種情況
應(yīng)該是直接傳入一個function,形如下面這樣,但最后都是啟動這個方法,在Vue構(gòu)造函數(shù)上面做
一些操作
Vue.use(function (Vue, options) {
// 1. 添加全局方法或?qū)傩? Vue.myGlobalMethod = function () {
// 邏輯...
}
// 2. 添加全局資源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 邏輯...
}
})
// 3. 注入組件
Vue.mixin({
created: function () {
// 邏輯...
}
})
// 4. 添加實(shí)例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 邏輯...
}
})
最后則plugin.installed = true,設(shè)置為已經(jīng)安裝了,這樣就不會重復(fù)安裝插件了,這樣
initUse(Vue)方法大致明白了思路
到了mixin.js文件,也就是第二個方法initMixin(Vue)
到了extend.js文件,也就是第二個方法initExtend(Vue)
這兩個文件涉及到比較多的問題,加之個人水平有限,暫且留著,這里簡單說下
mixin文件主要綁定了Vue.mixin方法,就是常用的全局混合,里面調(diào)用了mergeOptions合并策略
這個非常重要,后面細(xì)說,extend文件綁定了Vue.extend方法,這個就是常見的vue構(gòu)造器,可以
理解為創(chuàng)建一個子組件吧,還添加了一個屬性Vue.cid = 0。
這里直接看第4個方法initAssetRegisters(Vue),找到assets.js文件
這里又是遍歷這個數(shù)組_assetTypes,這里和上面說的有點(diǎn)類似,當(dāng)初看的時候有點(diǎn)懵逼了
其實(shí)是這樣的
// 上面綁定是這這樣的,在Vue.options上面綁定的
Vue.options = {
components: {
KeepAlive
},
directives: {},
filters: {}
}
// 這里綁定的是這樣的
Vue.component = function(id, definition){
}
Vue.directive = function(id, definition){
}
Vue.filter = function(id, definition){
}
// 一個在Vue的options屬性上,一個直接掛載在Vue上,一個后面帶有復(fù)數(shù)s,一個沒有
這個三個方法都是同一個函數(shù)操作,接受兩個參數(shù),第一個是str即指令名稱,第二個是fun或
者obj,如果沒有第二個參數(shù)則返回對應(yīng)的指令,比如
var directiveData = Vue.directive('show')
console.log(directiveData)
// 這樣可以看到v-show指令的情況,這個情況一般較少
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
var filterData = Vue.filter('capitalize')
console.log(filterData)
// 這樣可以看到自定義過濾器capitalize的方法,如果之前沒有注冊這個過濾器,那么則返回undefined
// 這里我們還是按照平常使用的方法例子來說,首先是指令
// 文檔提供了兩種方式注冊指令
1.
Vue.directive('demo', {
bind: function (el, binding, vnode) {
var s = JSON.stringify
el.innerHTML =
'name: ' + s(binding.name) + '<br>' +
'value: ' + s(binding.value) + '<br>' +
'expression: ' + s(binding.expression) + '<br>' +
'argument: ' + s(binding.arg) + '<br>' +
'modifiers: ' + s(binding.modifiers) + '<br>' +
'vnode keys: ' + Object.keys(vnode).join(', ')
}
})
2.
Vue.directive('color-swatch', function (el, binding) {
el.style.backgroundColor = binding.value
})
// 但是會發(fā)現(xiàn),第2種最后會經(jīng)過
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// 這個if,最后還是變成了第1種的方式變成了
{
bind: function (el, binding) {
el.style.backgroundColor = binding.value
},
update: function (el, binding) {
el.style.backgroundColor = binding.value
}
}
// 最后掛載在Vue構(gòu)造函數(shù)上面this.options[type + 's'][id] = definition
// return這個方法,這個倒是比較少用到
// 還記得Vue.options這個值是這樣的
Vue.options = {
components: {
KeepAlive
},
directives: {
// 這兩個是Vue自帶的,在另一個地方初始化的,這里暫時這樣理解就好,存在這兩個方法的
model: {
},
show: {
},
// 如果經(jīng)過了第一種方法注冊指令,那么就會添加下面一個了
demo: {
bind: function () {
// some methods
}
}
},
filters: {}
}
當(dāng)然,注冊自定義指令還有其他的生命周期鉤子和參數(shù),可以參考文檔自定義指令
// 然后是注冊過濾器,方法和指令一樣,只不過三個if都不走
// 直接是this.options[type + 's'][id] = definition
// 例如文檔例子
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
var filterData = Vue.filter('capitalize')
console.log(filterData)
// 這樣就可以注冊一個過濾器了
// 最后就剩下component方法了,這個是注冊組件用的,按照文檔例子說起
Vue.component('my-component', {
name: 'my-name',
template: '<span>{{ message }}</span>',
data () {
return {
message: 'hello'
}
}
})
// 首先走第1個if,這里有個config.isReservedTag(id)方法,方法解釋看methods realizes目錄
// 這里構(gòu)建前的源碼的config.isReservedTag方法找到的是一個no方法,但其實(shí)這個方法在
// platforms\web\util\element.js文件里面,這里直接看構(gòu)建后的源碼即可
// 這個方法就是不允許用戶使用html原有的標(biāo)簽作為組件的標(biāo)簽名字,然后到了第2個if語句,這里有個
// 方法isPlainObject,方法解釋看methods realizes目錄,這里這個是一個obj,如果沒有傳入name
// 則會使用id也就是注冊的組件名字作為name,上面name是my-name,如果沒有則name就是my-component
// 然后調(diào)用definition = this.options._base.extend(definition),這個其實(shí)就是上面跳過的方法
// initExtend(Vue)同樣的,其實(shí)這里的this.options._base就是Vue構(gòu)造函數(shù),因?yàn)?// console.log(this.options._base === Vue) // => true
// 最后掛載在this.options上面,就變成了這樣
// 還記得Vue.options這個值是這樣的
Vue.options = {
components: {
KeepAlive: {
},
// 這兩個是Vue自帶的,在另一個地方初始化的,這里暫時這樣理解就好,存在這兩個方法的
Transition: {
},
TransitionGroup: {
},
my-component: {
}
},
directives: {
// 這兩個是Vue自帶的,在另一個地方初始化的,這里暫時這樣理解就好,存在這兩個方法的
model: {
},
show: {
},
// 如果經(jīng)過了第一種方法注冊指令,那么就會添加下面一個了
demo: {
bind: function () {
// some methods
}
}
},
filters: {}
}
<p style="font-weight: bold;margin-bottom: 10px;color: #FF0000">剩下的幾個問題</p>
Vue.set = set // 涉及到Vue的數(shù)據(jù)響應(yīng)式系統(tǒng),先保留
Vue.delete = del // 涉及到Vue的數(shù)據(jù)響應(yīng)式系統(tǒng),先保留
Vue.nextTick = util.nextTick // 水平有限,看不懂 - -#
initMixin(Vue) // 這個后面再講
initExtend(Vue) // 水平有限,看不懂 - -#
綜上所述:
initGlobalAPI 的作用是在 Vue 構(gòu)造函數(shù)上掛載靜態(tài)屬性和方法,Vue 在經(jīng)過 initGlobalAPI
之后,會變成這樣:
Vue.config
Vue.util = util
Vue.set = set
Vue.delete = del
Vue.nextTick = util.nextTick
Vue.options = {
components: {
KeepAlive
},
directives: {},
filters: {},
_base: Vue
}
Vue.use
Vue.mixin
Vue.cid = 0
Vue.extend
Vue.component = function(){}
Vue.directive = function(){}
Vue.filter = function(){}
Vue.prototype.$isServer
Vue.version = '__VERSION__'