Vue
的使用按照官方的說法支持CDN
和NPM
兩種方式,CDN
的方式是以script
的方式將打包好的vue.js
引入頁面腳本中,而NPM
的方式是和諸如 webpack
或 Browserify
模塊打包器配置使用,以npm install vue
的方式引入,這也是咱們開發應用的主要形式。而從單純分析源碼思路和實現細節的角度來說,打包後的vue.js
在分析和提煉源碼方面會更加方便,因此這個系列的源碼分析,使用的是打包後的vue
腳本,版本號是v2.6.8
html
分析的開始固然是vue
的基礎使用,咱們引入了vue.js
而且new
了一個Vue
實例,並將它掛載到#app
上,這是最基礎的用法。前端
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.8/dist/vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
message: '選項合併'
},
})
</script>
複製代碼
雖然這一節的重點是闡述Vue
的選項配置,從選項配置入手也是咱們從零開始品讀源碼最容易開始的思路,可是爲了分析的完整性,避免後續出現未知的概念,有必要先大體瞭解一下vue
在腳本引入以後分別作了什麼。vue
打包後的源碼是聽從UMD
規範的,它是commonjs
和amd
的整合。而Vue
的本質是一個構造器,而且它保證了只能經過new
實例的形式去調用,而不能直接經過函數的形式使用。html5
(function (global, factory) {
// 遵循UMD規範
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.Vue = factory());
}(this, function () { 'use strict';
···
// Vue 構造函數
function Vue (options) {
// 保證了沒法直接經過Vue()去調用,只能經過new的方式去建立實例
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
return Vue
})
複製代碼
Vue之因此能適應基礎的開發場景,除了常常提到的支持組件化開發,以及完善的響應式系統等外,還有重要的一點是它提供了豐富的api
方法,不論是靜態仍是原型方法,它們都豐富到足以知足咱們平常基礎的開發需求。因此熟練閱讀vue-api文檔並精準使用api
方法是邁向熟練開發的前提。接下來咱們看看這些方法屬性是在哪裏定義的,注意,該小節會忽略大部分屬性方法具體的實現,這些詳細的細節會貫穿在後續系列的分析中。node
首先是原型上的屬性方法,在構造函數的定義以後,有這樣五個函數,他們分別針對不一樣場景定義了Vue
原型上的屬性和方法。webpack
// 定義Vue原型上的init方法(內部方法)
initMixin(Vue);
// 定義原型上跟數據相關的屬性方法
stateMixin(Vue);
//定義原型上跟事件相關的屬性方法
eventsMixin(Vue);
// 定義原型上跟生命週期相關的方法
lifecycleMixin(Vue);
// 定義渲染相關的函數
renderMixin(Vue);
複製代碼
咱們一個個看,首先initMixin
定義了內部在實例化Vue
時會執行的初始化代碼,它是一個內部使用的方法。web
function initMixin (Vue) {
Vue.prototype._init = function (options) {}
}
複製代碼
stateMixin
方法會定義跟數據相關的屬性方法,例如代理數據的訪問,咱們能夠在實例上經過this.$data
和this.$props
訪問到data,props
的值,而且也定義了使用頻率較高的this.$set,this.$delte
等方法。算法
function stateMixin (Vue) {
var dataDef = {};
dataDef.get = function () { return this._data };
var propsDef = {};
propsDef.get = function () { return this._props };
{
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);
// $set, $del
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
// $watch
Vue.prototype.$watch = function (expOrFn,cb,options) {};
}
複製代碼
eventsMixin
會對原型上的事件相關方法作定義,文檔中提到的vm.$on,vm.$once,vm.$off,vm.$emit
也就是在這裏定義的。npm
function eventsMixin(Vue) {
// 自定義事件監聽
Vue.prototype.$on = function (event, fn) {};
// 自定義事件監聽,只觸發一次
Vue.prototype.$once = function (event, fn) {}
// 自定義事件解綁
Vue.prototype.$off = function (event, fn) {}
// 自定義事件通知
Vue.prototype.$emit = function (event, fn) {
}
複製代碼
lifecycleMixin,renderMixin
兩個均可以算是對生命週期渲染方法的定義,例如$forceUpdate
觸發實例的強制刷新,$nextTick
將回調延遲到下次 DOM
更新循環以後執行等。api
// 定義跟生命週期相關的方法
function lifecycleMixin (Vue) {
Vue.prototype._update = function (vnode, hydrating) {};
Vue.prototype.$forceUpdate = function () {};
Vue.prototype.$destroy = function () {}
}
// 定義原型上跟渲染相關的方法
function renderMixin (Vue) {
Vue.prototype.$nextTick = function (fn) {};
// _render函數,後面會着重講
Vue.prototype._render = function () {};
}
複製代碼
除了原型方法外,Vue
還提供了豐富的全局api
方法,這些都是在initGlobalAPI
中定義的。
/* 初始化構造器的api */
function initGlobalAPI (Vue) {
// config
var configDef = {};
configDef.get = function () { return config; };
{
configDef.set = function () {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
);
};
}
// 經過Vue.config拿到配置信息
Object.defineProperty(Vue, 'config', configDef);
// 工具類不做爲公共暴露的API使用
Vue.util = {
warn: warn,
extend: extend,
mergeOptions: mergeOptions,
defineReactive: defineReactive###1
};
// Vue.set = Vue.prototype.$set
Vue.set = set;
// Vue.delete = Vue.prototype.$delete
Vue.delete = del;
// Vue.nextTick = Vue.prototype.$nextTick
Vue.nextTick = nextTick;
// 2.6 explicit observable API
Vue.observable = function (obj) {
observe(obj);
return obj
};
// 構造函數的默認選項默認爲components,directive,filter, _base
Vue.options = Object.create(null);
ASSET_TYPES.forEach(function (type) {
Vue.options[type + 's'] = Object.create(null);
});
// options裏的_base屬性存儲Vue構造器
Vue.options._base = Vue;
extend(Vue.options.components, builtInComponents);
// Vue.use()
initUse(Vue);
// Vue.mixin()
initMixin$1(Vue);
// 定義extend擴展子類構造器的方法
// Vue.extend()
initExtend(Vue);
// Vue.components, Vue.directive, Vue.filter
initAssetRegisters(Vue);
}
複製代碼
看着源碼對靜態方法的定義作一個彙總。
config
配置作一層代理,能夠經過Vue.config
拿到默認的配置,而且能夠修改它的屬性值,具體哪些能夠配置修改,能夠先參照官方文檔。set,delet,nextTick
方法,本質上原型上也有這些方法的定義。Vue.components,Vue.directive,Vue.filter
的定義,這些是默認的資源選項,後續會重點分析。Vue.use()
方法Vue.mixin()
方法Vue.extend()
方法如今我相信你已經對引入Vue
的階段有了一個大體的認識,在源碼分析的初期階段,咱們不須要死磕每一個方法,思路的實現細節,只須要對大體的結構有基本的認識。有了這些基礎,咱們開始進入這個章節的主線。
咱們回到最開始的例子,在實例化Vue
時,咱們會將選項對象傳遞給構造器進行初始化,這個選項對象描述了你想要的行爲,例如以data
定義實例中的響應式數據,以computed
描述實例中的計算屬性,以components
來進行組件註冊,甚至是定義各個階段執行的生命週期鉤子等。然而Vue
內部自己會自帶一些默認的選項,這些選項和用戶自定義的選項會在後續一塊兒參與到Vue
實例的初始化中。
在initGlobalAPI
方法中有幾行默認選項的定義。Vue
內部的默認選項會保留在靜態的options
屬性上,從源碼看Vue
自身有四個默認配置選項,分別是component,directive, filter
以及返回自身構造器的_base
。
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
// 原型上建立了一個指向爲空對象的options屬性
Vue.options = Object.create(null);
ASSET_TYPES.forEach(function (type) {
Vue.options[type + 's'] = Object.create(null);
});
Vue.options._base = Vue;
複製代碼
很明顯咱們開發者對這幾個選項是很是熟悉的,components
是須要註冊的組件選項,directives
是須要註冊的指令,而filter
則表明須要註冊的過濾器。從代碼的實現細節看,Vue
爲components
提供了keepAlive,transition,transitionGroup
的內置組件,爲directives
提供了v-model,v-show
的內置指令,而過濾器則沒有默認值。
// Vue內置組件
var builtInComponents = {
KeepAlive: KeepAlive
};
var platformComponents = {
Transition: Transition,
TransitionGroup: TransitionGroup
};
// Vue 內置指令,例如: v-model, v-show
var platformDirectives = {
model: directive,
show: show
}
extend(Vue.options.components, builtInComponents);
extend(Vue.options.components, platformComponents); // 擴展內置組件
extend(Vue.options.directives, platformDirectives); // 擴展內置指令
複製代碼
其中extend
方法實現了對象的合併,若是屬性相同,則用新的屬性值覆蓋舊值。
// 將_from對象合併到to對象,屬性相同時,則覆蓋to對象的屬性
function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}
複製代碼
所以作爲構造器而言,Vue
默認的資源選項配置以下:
Vue.options = {
components: {
KeepAlive: {}
Transition: {}
TransitionGroup: {}
},
directives: {
model: {inserted: ƒ, componentUpdated: ƒ}
show: {bind: ƒ, update: ƒ, unbind: ƒ}
},
filters: {}
_base
}
複製代碼
介紹完Vue
自身擁有的選項後,咱們回過頭來看看,實例化Vue
的階段發生了什麼。從構造器的定義咱們很容易發現,實例化Vue
作的核心操做即是執行_init
方法進行初始化。初始化操做會通過選項合併配置,初始化生命週期,初始化事件中心,乃至構建數據響應式系統等。而關鍵的第一步就是對選項的合併。合併後的選項會掛載到實例的$options
屬性中。(你能夠先在實例中經過this.$options
訪問最終的選項)
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this;
// a uid
// 記錄實例化多少個vue對象
vm._uid = uid$3++;
// 選項合併,將合併後的選項賦值給實例的$options屬性
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), // 返回Vue構造函數自身的配置項
options || {},
vm
);
};
}
複製代碼
從代碼中能夠看到,選項合併的重點是將用戶自身傳遞的options
選項和Vue
構造函數自身的選項配置合併。咱們看看mergeOptions
函數的實現。
function mergeOptions (parent,child,vm) {
{
checkComponents(child);
}
if (typeof child === 'function') {
child = child.options;
}
// props,inject,directives的校驗和規範化
normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);
// 針對extends擴展的子類構造器
if (!child._base) {
// extends
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm);
}
// mixins
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
}
var options = {};
var key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
function mergeField (key) {
// 拿到各個選擇指定的選項配置,若是沒有則用默認的配置
var strat = strats[key] || defaultStrat;
// 執行各自的合併策略
options[key] = strat(parent[key], child[key], vm, key);
}
// console.log(options)
return options
}
複製代碼
**選項合併過程當中更多的不可控在於不知道用戶傳遞了哪些配置選項,這些配置是否符合規範,是否達到合併配置的要求。所以每一個選項的書寫規則須要嚴格限定,原則上不容許用戶脫離規則外來傳遞選項。**所以在合併選項以前,很大的一部分工做是對選項的校驗。其中components,prop,inject,directive
等都是檢驗的重點。
若是項目中須要使用到組件,咱們會在vue
實例化時傳入組件選項以此來註冊組件。所以,組件命名須要遵照不少規範,好比組件名不能用html
保留的標籤(如:img,p
),也不能包含非法的字符等。這些都會在validateComponentName
函數作校驗。
// components規範檢查函數
function checkComponents (options) {
// 遍歷components對象,對每一個屬性值校驗。
for (var key in options.components) {
validateComponentName(key);
}
}
function validateComponentName (name) {
if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) {
// 正則判斷檢測是否爲非法的標籤,例如數字開頭
warn(
'Invalid component name: "' + name + '". Component names ' +
'should conform to valid custom element name in html5 specification.'
);
}
// 不能使用Vue自身自定義的組件名,如slot, component,不能使用html的保留標籤,如 h1, svg等
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
);
}
}
複製代碼
Vue
的官方文檔規定了props
選項的書寫形式有兩種,分別是
{ props: ['a', 'b', 'c'] }
,{ props: { a: { type: 'String', default: 'prop校驗' } }}
從源碼上看,兩種形式最終都會轉換成對象的形式。// props規範校驗
function normalizeProps (options, vm) {
var props = options.props;
if (!props) { return }
var res = {};
var i, val, name;
// props選項數據有兩種形式,一種是['a', 'b', 'c'],一種是{ a: { type: 'String', default: 'hahah' }}
// 數組
if (Array.isArray(props)) {
i = props.length;
while (i--) {
val = props[i];
if (typeof val === 'string') {
name = camelize(val);
// 默認將數組形式的props轉換爲對象形式。
res[name] = { type: null };
} else {
// 規則:保證是字符串
warn('props must be strings when using array syntax.');
}
}
} else if (isPlainObject(props)) {
for (var key in props) {
val = props[key];
name = camelize(key);
res[name] = isPlainObject(val)
? val
: { type: val };
}
} else {
// 非數組,非對象則斷定props選項傳遞非法
warn(
"Invalid value for option \"props\": expected an Array or an Object, " +
"but got " + (toRawType(props)) + ".",
vm
);
}
options.props = res;
}
複製代碼
provide/inject
這對組合在咱們平常開發中可能使用得比較少,當咱們須要在父組件中提供數據或者方法給後代組件使用時能夠用到provide/inject
,注意關鍵是後代,而不單純指子代,這是有別於props
的使用場景。官方把它被稱爲依賴注入,依賴注入使得組件後代都能訪問到父代注入的數據/方法,且後代不須要知道數據的來源。重要的一點,依賴提供的數據是非響應式的。
基本的使用以下:
// 父組件
var Provider = {
provide: {
foo: 'bar'
},
// ...
}
// 後代組件
var Child = {
// 數組寫法
inject: ['foo'],
// 對象寫法
inject: {
foo: {
from: 'foo',
default: 'bardefault'
}
}
}
複製代碼
inject
選項有兩種寫法,數組的方式以及對象的方式,和props
的校驗規則一致,最終inject
都會轉換爲對象的形式存在。
// inject的規範化
function normalizeInject (options, vm) {
var inject = options.inject;
if (!inject) { return }
var normalized = options.inject = {};
//數組的形式
if (Array.isArray(inject)) {
for (var i = 0; i < inject.length; i++) {
// from: 屬性是在可用的注入內容中搜索用的 key (字符串或 Symbol)
normalized[inject[i]] = { from: inject[i] };
}
} else if (isPlainObject(inject)) {
// 對象的處理
for (var key in inject) {
var val = inject[key];
normalized[key] = isPlainObject(val)
? extend({ from: key }, val)
: { from: val };
}
} else {
// 非法規則
warn(
"Invalid value for option \"inject\": expected an Array or an Object, " +
"but got " + (toRawType(inject)) + ".",
vm
);
}
}
複製代碼
咱們先看看指令選項的用法,Vue
容許咱們自定義指令,而且它提供了五個鉤子函數bind, inserted, update, componentUpdated, unbind
,具體的用法能夠參考官方-自定義指令文檔,而除了能夠以對象的形式去定義鉤子函數外,官方還提供了一種函數的簡寫,例如:
{
directives: {
'color-swatch': function(el, binding) {
el.style.backgroundColor = binding.value
}
}
}
複製代碼
函數的寫法會在bind,update
鉤子中觸發相同的行爲,而且不關心其餘鉤子。這個行爲就是定義的函數。所以在對directives
進行規範化時,針對函數的寫法會將行爲賦予bind,update
鉤子。
function normalizeDirectives (options) {
var dirs = options.directives;
if (dirs) {
for (var key in dirs) {
var def###1 = dirs[key];
// 函數簡寫一樣會轉換成對象的形式
if (typeof def###1 === 'function') {
dirs[key] = { bind: def###1, update: def###1 };
}
}
}
}
複製代碼
這個內容跟選項的規範化無關,當讀到上面規範檢測的代碼時,筆者發現有一段函數優化的代碼值得咱們學習。它將每次執行函數後的值進行緩存,當再次執行的時候直接調用緩存的數據而不是重複執行函數,以此提升前端性能,這是典型的用空間換時間的優化,也是經典的偏函數應用。
function cached (fn) {
var cache = Object.create(null); // 建立空對象做爲緩存對象
return (function cachedFn (str) {
var hit = cache[str];
return hit || (cache[str] = fn(str)) // 每次執行時緩存對象有值則不須要執行函數方法,沒有則執行並緩存起來
})
}
var camelizeRE = /-(\w)/g;
// 緩存會保存每次進行駝峯轉換的結果
var camelize = cached(function (str) {
// 將諸如 'a-b'的寫法統一處理成駝峯寫法'aB'
return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
});
複製代碼
選項校驗介紹完後,在正式進入合併策略以前,還須要先了解一個東西:子類構造器。爲何須要先提到子類構造器呢?
按照前面的知識,Vue
內部提供了四個默認選項,關鍵的三個是components,directives,filter
。那麼當咱們傳遞一個選項配置到Vue
進行初始化,所須要合併的選項好像也僅僅是那關鍵的三個默認選項而已,那麼源碼中大篇幅作的選項合併策略又是針對什麼場景呢?答案就是這個子類構造器。
Vue
提供了一個Vue.extend
的靜態方法,它是基於基礎的Vue
構造器建立一個「子類」,而這個子類所傳遞的選項配置會和父類的選項配置進行合併。這是選項合併場景的由來。
所以有不要先了解子類構造器的實現。下面例子中,咱們建立了一個Child
的子類,它繼承於父類Parent
,最終將子類掛載到#app
元素上。最終獲取的data
即是選項合併後的結果。
var Parent = Vue.extend({
data() {
test: '父類',
test1: '父類1'
}
})
var Child = Parent.extend({
data() {
test: '子類',
test2: '子類1'
}
})
var vm = new Child().$mount('#app');
console.log(vm.$data);
// 結果
{
test: '子類',
test1: '父類1',
test2: '子類1'
}
複製代碼
Vue.extend
的實現思路很清晰,建立了一個Sub
的類,這個類的原型指向了父類,而且子類的options
會和父類的options
進行合併,mergeOptions
的其餘細節接下來會重點分析。
Vue.extend = function (extendOptions) {
extendOptions = extendOptions || {};
var Super = this;
var name = extendOptions.name || Super.options.name;
if (name) {
validateComponentName(name); // 校驗子類的名稱是否符合規範
}
// 建立子類構造器
var Sub = function VueComponent (options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype); // 子類繼承於父類
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
// 子類和父類構造器的配置選項進行合併
Sub.options = mergeOptions(
Super.options,
extendOptions
);
return Sub // 返回子類構造函數
};
複製代碼