搞事!搞事!
截止2017.5.16,終於把vue的源碼全部抄完,總共有9624行,花時大概一個月時間,中間迭代了一個版本(2.2-2.3),部分代碼可能不一致,不過沒關系!
上一個鏈接https://github.com/pflhm2005/vue
進入階段2:嘗試一下,從小案例看一看代碼在vue源碼中的走向,Go!(語文不好,將就看看)
從最簡單的案例開始,摘抄官網的起步:
<body>
<div id='app'> {{message}} </div>
</body>
<script src='./vue.js'></script>
<script>
var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } }); </script>
打斷點,開始執行!
初始化函數
html代碼中,包含2大部分,掛載DOM節點,以及初始化vue的js代碼。
有幾個小地方,雖然按照官網的案例不會出現問題,但是還是說明一下:
(1)、el不能掛載到html或者body標簽上
// Line-9547
if (el === document.body || el === document.documentElement) { "development" !== 'production' && warn( "Do not mount Vue to <html> or <body> - mount to normal elements instead." ); return this }
(2)、關於代碼各處可見的"development" !== 'production'
這個是dev模式才有,vue.js文件中對所有警告的代碼判斷條件進行了替換,報錯方便調試,在發布模式中會自動清除,方便開發。
與jQuery不一樣,這里需要手動new才會創建一個vue實例。
直接上源碼。
jQuery:
// Line-94 jQuery 3.2.1
// 順便吐槽一下 這個版本終於把初始化提前了 代碼結構應該棒棒的
var jQuery = function(selector, context) { // The jQuery object is actually just the init constructor 'enhanced'
// Need init if jQuery is called (just allow error to be thrown if not included)
return new jQuery.fn.init(selector, context); }
Vue:
// Line-9622
return Vue$3;
但是我們看到源碼最后其實返回的是Vue$3,至於為什么new的是Vue也行呢?看一下源碼開頭的整個IIFE表達式也就明白了。
(function(global, factory) { // 兼容各種宿主環境 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : // 瀏覽器環境 (global.Vue = factory()); }(this, /*vue*/ ));
基本上框架都是這個套路,引入一個宿主環境的對象以及框架本身。
上述代碼形參中,global在瀏覽器環境中相當於window,由於有時會在node、webpack等環境中運行,所以需要進行兼容處理,於是有很長的typeof。
對於瀏覽器來講,上述代碼其實就是window.Vue = Vue$3({options}),所以這就很明了了。
起步流程兩個框架都是一樣的,首先通過一個init函數進行全局初始化。
// Line-4055
function Vue$3(options) { if ("development" !== 'production' &&
!(this instanceof Vue$3)) { warn('Vue is a constructor and should be called with the `new` keyword'); } this._init(options); }
這里的options參數,很明顯就是我們在new對象的時候傳進去的對象,目前只有el和data兩個。
入口函數只是簡單的判斷了一下有沒有new,然后自動調用了原型函數_init。
_init函數的定義地點有點意思,是在一個函數內部定義,然后在后面調用了這個函數。
// Line-3924 function initMixin(Vue) { Vue.prototype._init = function(options) { //.... }; } // Line-4063 initMixin(Vue$3);
整個函數只定義了_init這個初始化原型函數,原因在某個注釋中寫,直接定義原型會出現問題,所以采用這種方法進行規避。
至於具體什么問題,我找不到那行注釋了。。。
接下來看看初始化函數里面都干了啥事。
// Line-3926 // 生成的實例保存為別名vm var vm = this; // 全局記數 表示有幾個vue實例 vm._uid = uid$1++; var startTag, endTag; // 這里的config.performance開發版默認是false if ("development" !== 'production' && config.performance && mark) { startTag = "vue-perf-init:" + (vm._uid); endTag = "vue-perf-end:" + (vm._uid); mark(startTag); } // 代表這是一個vue實例 vm._isVue = true; // 非組件 跳過 if (options && options._isComponent) { initInternalComponent(vm, options); } // 正常實例初始化 // 在這里對參數進行二次加工 else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ); } // ...more
前面基本上沒做什么事,對於config對象,在開發版中的默認參數如下:
// 開發版的默認配置 var config = ({ optionMergeStrategies: Object.create(null), silent: false, productionTip: "development" !== 'production', devtools: "development" !== 'production', performance: false, errorHandler: null, ignoredElements: [], keyCodes: Object.create(null), isReservedTag: no, isReservedAttr: no, isUnknownElement: no, getTagNamespace: noop, parsePlatformTagName: identity, mustUseProp: no, // 歷史遺留 _lifecycleHooks: LIFECYCLE_HOOKS });
由於提示信息不是重點,所以第一步直接可以走到mergeOptions這里,從名字就可以看出這是一個參數合並的函數,接受3個參數:
1、resolveConstructorOptions(vm.constructor)
這個函數屬於內部初始化,接受的參數就是Vue函數自身,如下:
// Line-4136 Sub.prototype.constructor = Sub;
跳進去看一眼這個函數做了什么:
// Line-3998 function resolveConstructorOptions(Ctor) { // Ctor=Constructor // options為所有vue實例基礎參數 // 包含components,directives,filters,_base var options = Ctor.options; // 這個屬性比較麻煩 暫時沒有 跳過 if (Ctor.super) { //... } // 返回修正后的options return options }
如果忽略那個super屬性的話,返回的其實就是Vue$3.constructor.options,該對象包含4個屬性,如圖所示。
// Line-4368 // Vue函數自身的引用 Vue.options._base = Vue; // Line-7523 extend(Vue$3.options.directives, platformDirectives); extend(Vue$3.options.components, platformComponents); // Line-7161 // 指令相關方法 var platformDirectives = { model: model$1, show: show };
// Line-7509 // 組件相關 var platformComponents = { Transition: Transition, TransitionGroup: TransitionGroup };
其中filters屬性暫時是空的,其余3個屬性在2個地方有定義,一個是組件、指令方法集,一個是vue函數自身引用。
2、options || {} => 傳進來的參數
3、vm => 當前vue實例
最后,總覽3個參數如下:
帶着3個小東西,跳進了mergeOptions函數進行參數合並。
// Line-1298 // 父子組件合並參數 本案例父組件為默認對象 function mergeOptions(parent, child, vm) { // 檢測components參數中鍵是否合法 checkComponents(child); if (typeof child === 'function') { child = child.options; } // 格式化props,directives參數 normalizeProps(child); normalizeDirectives(child); // 格式化extends參數 var extendsFrom = child.extends; if (extendsFrom) { parent = mergeOptions(parent, extendsFrom, 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); } return options }
這個函數中前半部分可以跳過,因為只有簡單的el、data參數,所以直接從mergeField開始執行。
上面已經列舉出父組件的鍵,有components、directives、_filters、_base四個。
這里又多出一個新的東西,叫strats,英文翻譯成戰略,所以應該怎么叫我也是很懵逼的。這個對象內容十分豐富,從生命周期到data、computed、methods都有,如下所示:
方法太多,就不一個一個講了,說說本案例相關的幾個方法。
看起來非常嚇人,其實定義簡單粗暴,上代碼看看就明白了。
// Line-281 var ASSET_TYPES = [ 'component', 'directive', 'filter' ]; // Line-1182 ASSET_TYPES.forEach(function(type) { strats[type + 's'] = mergeAssets; }); // Line-1175 function mergeAssets(parentVal, childVal) { var res = Object.create(parentVal || null); return childVal ? extend(res, childVal) : res }
簡單講就是,3個鍵對應的是同一個方法,接受2個參數,方法還賊簡單。
所以,對上面的mergeOptions函數進行簡化,可以轉換成如下代碼:
// parent鍵:components、directives、_filters、_base // child鍵:data、el function mergeOptions(parent, child, vm) { var options = {}; var key; // 父子對象鍵沒有重復 參數直接可以寫undefined 一步一步簡化 for (key in parent) { //options[key] = mergeAssets(parent[key], child[key], vm, key); //options[key] = mergeAssets(parent[key], undefined); options[key] = Object.create(parent[key]); } // 子鍵data和el需要額外分析 第一個參數同樣可以寫成undefined for (key in child) { if (!hasOwn(parent, key)) { //options[key] = strats[key](parent[key], child[key], vm, key); options[key] = strats[key](undefined, child[key], vm, key); } } return options } function mergeAssets(parentVal, childVal) { var res = Object.create(parentVal || null); return childVal ? extend(res, childVal) : res }
遍歷父對象其實啥也沒做,直接把幾個方法加到了options上面,然后開始遍歷子對象,子對象包含我們傳進去的el、data。
el比較簡單,只是做個判斷然后丟回來。
// Line-1064 // 簡單判斷是否是vue實例掛載的el strats.el = strats.propsData = function(parent, child, vm, key) { if (!vm) { warn( "option \"" + key + "\" can only be used during instance " + 'creation with the `new` keyword.' ); } return defaultStrat(parent, child) };
data則分兩種情況,一種是未掛載的組件,一種是實例化的vue。
不管未掛載,直接看實例化vue是如何處理data參數。
// Line-1098 strats.data = function(parentVal, childVal, vm) { // 未掛載 if (!vm) { //... } // new出來的 // 傳進來的parentVal、childVal分別為undefined、{message:'Hello Vue!} else if (parentVal || childVal) { return function mergedInstanceDataFn() { var instanceData = typeof childVal === 'function' ? childVal.call(vm) : childVal; var defaultData = typeof parentVal === 'function' ? parentVal.call(vm) : undefined; if (instanceData) { return mergeData(instanceData, defaultData) } else { return defaultData } } } };
這里直接返回了一個函數,暫時不做分析,后面執行時候再來看。
到此,整個mergeOptions函數執行完畢,返回一個處理過的options,將這個結果給了實例的$options屬性:
最后,用一張圖結束這個亂糟糟的源碼小跑第一節吧。
撒花!撒花!