.1-Vue源碼起步


搞事!搞事!

 

  截止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屬性:

 

  最后,用一張圖結束這個亂糟糟的源碼小跑第一節吧。

 

 

  撒花!撒花!

 

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM