對於大部分的前端開發人員來說,熟練使用vue
作項目是第一步,但當進階後遇到一些特殊場景,解決棘手問題時,瞭解vue
框架的設計思想和實現思路即是基礎須要。本專題將深刻vue
框架源碼,一步步挖掘框架設計理念和思想,並儘量利用語言將實現思路講清楚。但願您是在熟練使用vue
的前提下閱讀此係列文章,也但願您閱讀後能留下寶貴建議,以便後續文章改進。
<div id="app"></div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.8/dist/vue.js"></script> var vm = new Vue({ el: '#app', data: { message: '選項合併' }, components: { 'components': {} } })
從最簡單的使用入手,new
一個Vue
實例對象是使用vue
的第一步,在這一步中,咱們須要傳遞一些基礎的選項配置,Vue
會根據系統的默認選項和用戶自定選項進行合併選項配置的過程。本系列將從這一過程展開,在這一節中咱們研究的核心在於各類數據選項在vue
系統中是如何進行合併的(忽略過程當中的響應式系統構建,後面專題講解)。html
// Vue 構造函數 function Vue (options) { if (!(this instanceof Vue) ) { // 規定vue只能經過new實例化建立,不然拋出異常 warn('Vue is a constructor and should be called with the `new` keyword'); } this._init(options); } // 在引進Vue時,會執行initMixin方法,該方法會在Vue的原型上定義數據初始化init方法,方法只在實例化Vue時執行。 initMixin(Vue); // 暫時忽略其餘初始化過程。。。 ···
接下來,咱們將圍繞vue數據的初始化展開解析。前端
var ASSET_TYPES = [ 'component', 'directive', 'filter' ]; Vue.options = Object.create(null); // 原型上建立了一個指向爲空對象的options屬性 ASSET_TYPES.forEach(function (type) { Vue.options[type + 's'] = Object.create(null); }); Vue.options._base = Vue;
Vue構造函數自身有四個默認配置選項,分別是component,directive, filter
以及返回自身構造器的_base
(這裏先不展開對每一個屬性內容的介紹)。這四個屬性掛載在構造函數的options
屬性上。vue
咱們抓取_init
方法合併選項的核心部分代碼以下:html5
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
構造函數自身的選項配置合併,並將合併結果掛載到實例對象的$options
屬性上。算法
選項合併過程咱們更多的不可控在於不知道用戶傳了哪些配置選項,這些配置是否符合規範,因此每一個選項的規範須要嚴格定義好,不容許用戶按照規範外的標準來傳遞選項。所以在合併選項以前,很大的一部分工做是對選項的校驗。其中components,prop,inject,directive
等都是檢驗的重點。下面只會列舉components
和props
的校驗講解,其餘的如inject, directive
校驗相似,請自行對着源碼解析。npm
function mergeOptions ( parent, child, vm ) { { checkComponents(child); // 合併前對選項components進行規範檢測 } if (typeof child === 'function') { child = child.options; } normalizeProps(child, vm); // 校驗props選項 normalizeInject(child, vm); // 校驗inject選項 normalizeDirectives(child); // 校驗directive選項 if (!child._base) { if (child.extends) { parent = mergeOptions(parent, child.extends, vm); } 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); } return options }
咱們能夠在vue
實例化時傳入組件選項以此來註冊組件。所以,組件命名須要遵照不少規範,好比組件名不能用html
保留的標籤(如:img,p
),只能以字母開頭等。所以在選項合併以前,須要對規範進行檢查。api
// components規範檢查函數 function checkComponents (options) { 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
選項的形式有兩種,一種是['a', 'b', 'c']
的數組形式,一種是{ a: { type: 'String', default: 'hahah' }}
帶有校驗規則的形式。從源碼上看,兩種形式最終都會轉換成對象的形式。數組
// 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); res[name] = { type: null }; // 默認將數組形式的props轉換爲對象形式。 } 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; }
在讀到props
規範檢驗時,我發現了一段函數優化的代碼,他將每次執行函數後的值緩存起來,下次重複執行的時候調用緩存的數據,以此提升前端性能,這是典型的偏函數應用,能夠參考我另外一篇文章打造屬於本身的underscore系列(五)- 偏函數和函數柯里化緩存
function cached (fn) { var cache = Object.create(null); // 建立空對象做爲緩存對象 return (function cachedFn (str) { var hit = cache[str]; return hit || (cache[str] = fn(str)) // 每次執行時緩存對象有值則不須要執行函數方法,沒有則執行並緩存起來 }) } var camelize = cached(function (str) { // 將諸如 'a-b'的寫法統一處理成駝峯寫法'aB' return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) });
選項校驗介紹完後,在正式進入合併策略以前,還須要先了解一個東西,子類構造器。在vue
的應用實例中,咱們經過Vue.extend({ template: '<div></div>', data: function() {} })
建立一個子類,這個子類和Vue
實例建立的父類同樣,能夠經過建立實例並掛載到具體的一個元素上。具體用法詳見Vue官方文檔,而具體實現以下所示(只簡單抽取部分代碼):app
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 // 返回子類構造函數 };
爲何要先介紹子類構造器的概念呢,緣由是在選項合併的代碼中,除了須要合併Vue實例和Vue構造器自身的配置,還須要合併子類構造器和父類構造器選項的場景。
合併策略之因此是難點,其中一個是合併選項類型繁多,大致能夠分爲如下三類:Vue自定義策略, 父類自身配置, 子類自身策略(用戶配置)。如何理解?
Vue
自定義策略,vue
在選項合併的時候對一些特殊的選項有自身定義好的合併策略,例如data
的合併,el
的合併,而每個的合併規則都不同,所以須要對每個規定選項進行特殊的合併處理Vue
構造函數自身的options
屬於父類自身配置,咱們須要將實例傳遞的配置和Vue.options
進行合併。再者前面提到的var P = Vue.extends(); var C = P.extends()
,P做爲C的父類,在合併選項時一樣須要考慮進去。new
實例傳遞的options
選項在Vue
源碼中,如何處理好這三個選項的合併,思路是這樣的:
function mergeOptions ( parent, child, 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); } return options }
喜歡本系列的朋友歡迎關注公衆號 假前端,有源碼解析和算法精選哦