Vue源碼后記-更多options參數(1)


  我是這樣計划的,寫完這個還寫一篇數據變動時,VNode是如何更新的,順便初探一下diff算法。

  至於vue-router、vuex等插件源碼,容我緩一波好吧,vue看的有點傷。

 

  其實在之前講其余內置指令有涉及到on事件綁定,這里再詳細從頭看一下事件綁定的正統流程吧!

  上html代碼:

    <body>
        <div id='app'>
            <div @click.self.number='click'>
                {{computedValue | filter}}
            </div>
        </div>
    </body>
    <script src='./vue.js'></script>
    <script>
        new Vue({
            el: '#app',
            data: {
                value: 1
            },
            computed: {
                computedValue: function() {
                    return this.value * 2
                }
            },
            methods: {
                click: function() {
                    console.log('click!');
                }
            },
            filters: {
                filter: function(value) {
                    if (!value) {
                        return;
                    }
                    return value * 4;
                }
            }
        })
    </script>

  包含最常見的click事件,計算屬性以及過濾器,鈎子函數講過就不寫了,所有的函數都是最簡單模式。

 

1、mergeOptions

  源碼看的有點惡心,流程都能背下來了。

  第一步是進行參數合並,傳入的對象作為options與默認的options進行合並,並通過strat對象對應的方法處理:

    // parent => baseOptions
    // child => options
    function mergeOptions(parent, child, vm) {
        // code...

        function mergeField(key) {
            var strat = strats[key] || defaultStrat;
            options[key] = strat(parent[key], child[key], vm, key);
        }
        return options
    }

  這里主要是調用strat對象方法對每一個options的鍵進行處理,目前傳進來有data、computed、methods、filters四個。

  data前面講過,其余三個依次看一下。

 

  computed、methods

  兩個參數對應同一個方法:

    strats.props =
        strats.methods =
        strats.computed = function(parentVal, childVal) {
            if (!childVal) {
                return Object.create(parentVal || null)
            }
            if (!parentVal) {
                return childVal
            }
            var ret = Object.create(null);
            extend(ret, parentVal);
            extend(ret, childVal);
            return ret
        };

  比較簡單,將兩個合並,直接返回該對象並掛載到vm上:

  

  filters

    function mergeAssets(parentVal, childVal) {
        var res = Object.create(parentVal || null);
        return childVal ?
            extend(res, childVal) :
            res
    }

  非常無趣的函數:

 

2、initState

  vue上面屬性添加完后,接下來是數據初始化階段:

    function initState(vm) {
        vm._watchers = [];
        var opts = vm.$options;
        if (opts.props) { initProps(vm, opts.props); }
        if (opts.methods) { initMethods(vm, opts.methods); }
        if (opts.data) {
            initData(vm);
        } else {
            observe(vm._data = {}, true /* asRootData */ );
        }
        if (opts.computed) { initComputed(vm, opts.computed); }
        if (opts.watch) { initWatch(vm, opts.watch); }
    }

  可以看到這里對props、methods、data、computed、watch幾個屬性都做了初始化。

methods

    function initMethods(vm, methods) {
        var props = vm.$options.props;
        for (var key in methods) {
            // 重新綁定上下文
            vm[key] = methods[key] == null ? noop : bind(methods[key], vm); {
                if (methods[key] == null) {
                    // 函數對象值為空時
                }
                // 判斷重名
                if (props && hasOwn(props, key)) {
                    // warning
                }
            }
        }
    }

  這個Init函數做了2件事,第一是對所有函數進行上下文綁定並掛載到vm上,第二是與props進行排重,不允許出現相同鍵名。

 initComputed

    function initComputed(vm, computed) {
        var watchers = vm._computedWatchers = Object.create(null);

        for (var key in computed) {
            var userDef = computed[key];
            var getter = typeof userDef === 'function' ? userDef : userDef.get; {
                if (getter === undefined) {
                    // warn No getter function
                    getter = noop;
                }
            }
            // create internal watcher for the computed property.
            watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions);

            if (!(key in vm)) {
                defineComputed(vm, key, userDef);
            } else {
                // key查重
            }
        }
    }

    function defineComputed(target, key, userDef) {
        if (typeof userDef === 'function') {
            sharedPropertyDefinition.get = createComputedGetter(key);
            sharedPropertyDefinition.set = noop;
        } else {
            // setter、getter
        }
        Object.defineProperty(target, key, sharedPropertyDefinition);
    }

    function createComputedGetter(key) {
        return function computedGetter() {
            var watcher = this._computedWatchers && this._computedWatchers[key];
            if (watcher) {
                if (watcher.dirty) {
                    watcher.evaluate();
                }
                if (Dep.target) {
                    watcher.depend();
                }
                return watcher.value
            }
        }
    }

  處理computed時,內部會new一個watcher來專門監聽相關數據變動,然后用defineProperty在vm上聲明一個對象。

 

2、parseHTML

  合並完參數並做init初始化后,第二步應該是解析DOM字符串了。

  直接看關鍵點,外層的div跳過,只看里層的,首先是:

    <div @click.self.number='click'>

  切割函數為:

    var startTagMatch = parseStartTag();
    if (startTagMatch) {
        handleStartTag(startTagMatch);
        continue
    }

  第一次暴力切割后,直接以=分割屬性:

  接下來進入handleStartTag => start => processAttrs

    function processAttrs(el) {
        var list = el.attrsList;
        var i, l, name, rawName, value, modifiers, isProp;
        for (i = 0, l = list.length; i < l; i++) {
            name = rawName = list[i].name;
            value = list[i].value;
            // name => @click.self.number
            if (dirRE.test(name)) {
                el.hasBindings = true;
                // 修飾符
                // modifiers => {self:true,number:true}
                modifiers = parseModifiers(name);
                if (modifiers) {
                    // name => @click
                    name = name.replace(modifierRE, '');
                }
                if (bindRE.test(name)) { // v-bind
                    // code...
                } else if (onRE.test(name)) { // v-on
                    name = name.replace(onRE, '');
                    addHandler(el, name, value, modifiers, false, warn$2);
                } else { // normal directives
                    // code...
                }
            } else {
                // code...
            }
        }
    }

    function parseModifiers(name) {
        // modifierRE => /\.[^.]+/g;
        var match = name.match(modifierRE);
        if (match) {
            var ret = {};
            match.forEach(function(m) {
                ret[m.slice(1)] = true;
            });
            return ret
        }
    }

  這里會首先對事件name進行修飾符判斷,截取出self與number兩個字段,保存到modifiers對象中。

  修飾符處理完后,進入v-on指令分支,首先正則替換掉第一個@字符,進入addHandler函數。

    function addHandler(el, name, value, modifiers, important, warn) {
        // handle capture,passive,once
        var events;
        if (modifiers && modifiers.native) {
            delete modifiers.native;
            events = el.nativeEvents || (el.nativeEvents = {});
        } else {
            events = el.events || (el.events = {});
        }
        var newHandler = {
            value: value,
            modifiers: modifiers
        };
        var handlers = events[name];
        /* istanbul ignore if */
        if (Array.isArray(handlers)) {
            important ? handlers.unshift(newHandler) : handlers.push(newHandler);
        } else if (handlers) {
            events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
        } else {
            // {value:click,modifiers:{self:true,number:true}}
            events[name] = newHandler;
        }
    }

  該函數處理特殊事件類型,包括capture、once、passive,並會給事件字符串添加特殊的前綴。

  完事后,該AST會被添加一個events屬性,如圖:

 

  下面轉換節點內部的表達式:

    {{computedValue | filter}}

  關鍵函數是parseFilters,用來分割過濾器:

    function parseFilters(exp) {
        // var...

        for (i = 0; i < exp.length; i++) {
            prev = c;
            c = exp.charCodeAt(i);
            if (inSingle) {
                // code...
            } else if (
                c === 0x7C && // pipe
                exp.charCodeAt(i + 1) !== 0x7C &&
                exp.charCodeAt(i - 1) !== 0x7C &&
                !curly && !square && !paren
            ) {
                // 第一次匹配到 | 時
                if (expression === undefined) {
                    // first filter, end of expression
                    lastFilterIndex = i + 1;
                    expression = exp.slice(0, i).trim();
                }
                // 多個filter串聯 
                else {
                    pushFilter();
                }
            } else {
                // code...
            }
        }

        if (expression === undefined) {
            expression = exp.slice(0, i).trim();
        } else if (lastFilterIndex !== 0) {
            pushFilter();
        }

        function pushFilter() {
            (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim());
            lastFilterIndex = i + 1;
        }

        if (filters) {
            for (i = 0; i < filters.length; i++) {
                expression = wrapFilter(expression, filters[i]);
            }
        }

        return expression
    }

  其中切割大括號的部分跳過,主要看是如何處理分割后的filter:

    function wrapFilter(exp, filter) {
        // filter有可能帶有參數 => {{value | filter(args)}}
        var i = filter.indexOf('(');
        if (i < 0) {
            // _f: resolveFilter
            return ("_f(\"" + filter + "\")(" + exp + ")")
        } else {
            var name = filter.slice(0, i);
            var args = filter.slice(i + 1);
            return ("_f(\"" + name + "\")(" + exp + "," + args)
        }
    }

  這里本來只有兩種情況,一種只有函數名,一種是帶參數的函數。

  1、單純函數

    {{computedValue | filter}}

  如果只有函數名,此時i為-1,會進入第一個分支,直接返回對應的拼接字符串,如圖:

   2、帶參數的函數

    {{computedValue | filter(1)}}

  此時會進入分支2,並且通過正則進行切割,name為函數名,args為參數,最后返回拼接字符串:

  3、???

  后來,我又發現了第三種情況,就是如果函數名被括號給包起來,解析會變得有點奇怪。

    {{computedValue | (filter)}}

  此時,由於檢測到小括號的存在,后面的被認為是形參,一個空白字符串被當成函數名進行拼接,返回如圖:

  當然,這個會報錯,filter被認為是形參,又不存在對應的函數,既然有warning提示,也不算啥問題,盡量不作死就行。

 

  至此,AST的轉化完成,下一節講render函數的生成。


免責聲明!

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



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