1. 程式人生 > >Vue源碼後記-vFor列表渲染(2)

Vue源碼後記-vFor列表渲染(2)

property per share turn logs eno ext 形參 dsl

這一節爭取搞完!

  

  回頭來看看那個render代碼,為了便於分析,做了更細致的註釋;

    (function() {
        // 這裏this指向vue對象 下面的所有方法默認調用Vue$3.prototype上的方法
        with(this){
            return _c/*方法調用 => has攔截器過濾*/
            (‘div‘,{attrs:{"id":"app"}},
            _l/*方法調用 => has攔截器過濾*/(
                (items/*_data屬性訪問 => 自定義proxy過濾
*/), function(item){ return _c/*方法調用 => has攔截器過濾*/ (‘a‘,{attrs:{"href":"#"}}, [_v/*方法調用 => has攔截器過濾*/ (_s/*方法調用 => has攔截器過濾*/(item))]) })) } })

  所有的has攔截器之前分析過了,跳過,但是這裏又多了一個特殊的訪問,即items,但是Vue$3上並沒有這個屬性,屬性在Vue$3._data上,如圖:技術分享

,那這是如何訪問到的呢?

  Vue在initState的時候自己又封裝了一個proxy,所有對屬性的訪問會自動跳轉到_data上,代碼如下:

    Vue.prototype._init = function(options) {
        // code...

        // 這裏處理是ES6的Proxy
        {
            initProxy(vm);
        }
        
        // beforeCreate

        initInjections(vm); // resolve injections before data/props
initState(vm); initProvide(vm); // resolve provide after data/props callHook(vm, ‘created‘); // code... }; function initState(vm) { // if... if (opts.data) { initData(vm); } else { // 沒有data參數 observe(vm._data = {}, true /* asRootData */ ); } // if... } function initData(vm) { // code... while (i--) { if (props && hasOwn(props, keys[i])) { // warning } else if (!isReserved(keys[i])) { proxy(vm, "_data", keys[i]); } } // observe data... } // target => vm // sourceKey => _data 這個還有可能是props 不過暫時不管了 // key => data參數中所有的對象、數組 function proxy(target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter() { return this[sourceKey][key] }; sharedPropertyDefinition.set = function proxySetter(val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); }

  可以看到,最後一個函數中,通過defineProperty方法,所有對vm屬性的直接訪問會被跳轉到Vue$3[sourceKey]上,這裏指就是_data屬性。

  而這個屬性的讀寫,同樣被特殊處理過,即數據劫持,跑源碼的時候也講過,直接貼核心代碼:

    function defineReactive$$1(obj, key, val, customSetter) {
        // var...

        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get: function reactiveGetter() {
                var value = getter ? getter.call(obj) : val;
                if (Dep.target) {
                    dep.depend();
                    if (childOb) {
                        childOb.dep.depend();
                    }
                    if (Array.isArray(value)) {
                        dependArray(value);
                    }
                }
                return value
            },
            set: function reactiveSetter(newVal) {
                // set...
            }
        });
    }

  簡單來講,所有對_data上的屬性的讀寫都會被攔截並調用自定義的get、set方法,這裏也不例外,數據會被添加到依賴接受監聽,詳細過程太細膩就不貼了,有興趣可以自己去跑跑。

  訪問items後,數組中的元素會被watch,有變化會通知DOM進行更新,這裏接下來會執行_l方法:

    Vue.prototype._l = renderList;

    // val => items
    // render => function(item){...}
    function renderList(val, render) {
        var ret, i, l, keys, key;
        // 數組 => 遍歷進行值渲染
        if (Array.isArray(val) || typeof val === ‘string‘) {
            ret = new Array(val.length);
            for (i = 0, l = val.length; i < l; i++) {
                ret[i] = render(val[i], i);
            }
        }
        // 純數字 => 處理類似於item in 5這種無數據源的模板渲染 
        else if (typeof val === ‘number‘) {
            ret = new Array(val);
            for (i = 0; i < val; i++) {
                ret[i] = render(i + 1, i);
            }
        }
        // 對象 => 取對應的值進行渲染
        else if (isObject(val)) {
            keys = Object.keys(val);
            ret = new Array(keys.length);
            for (i = 0, l = keys.length; i < l; i++) {
                key = keys[i];
                ret[i] = render(val[key], key, i);
            }
        }
        return ret
    }

  代碼還是清晰的,三種情況:數組、純數字、對象。

  用過應該都明白是如何處理三種情況的,這裏將對應的值取出來調用render方法,這個方法來源於第二個參數:

    // item => 1,2,3,4,5
    (function(item) {
        return _c(‘a‘, {attrs: {"href": "#"}}, [_v(_s(item))])
    })

  方法很抽象,慢慢解析。

  因為與tag相關,所以再次調用了_c函數,但是執行順序還是從內到外,因此會對_v、_s做過濾並首先調用_s函數:

    Vue.prototype._s = toString;

    // val => item => 1,2,3,4,5
    function toString(val) {
        return val == null ?
            ‘‘ :
            typeof val === ‘object‘ ?
            JSON.stringify(val, null, 2) :
            String(val)
    }

  這個方法一句話概括就是字符串化傳進來的參數。

  這裏先傳了一個數字1,返回字符串1並將其作為參數傳入_v函數:

    Vue.prototype._v = createTextVNode;

    // val => 1
    function createTextVNode(val) {
        return new VNode(undefined, undefined, undefined, String(val))
    }

  這個函數從命名也能看出來,創建一個文本的vnode,值為傳進來的參數。

  可以看一眼這個虛擬DOM的結構:技術分享,因為是文本節點,所以只有text是有值的。

  形參都處理完畢,下一步進入_c函數,看下代碼:

    vm._c = function(a, b, c, d) {
        return createElement(vm, a, b, c, d, false);
    };

    var SIMPLE_NORMALIZE = 1;
    var ALWAYS_NORMALIZE = 2;

    function createElement(context, tag, data, children, normalizationType, alwaysNormalize) {
        // 參數修正
        if (Array.isArray(data) || isPrimitive(data)) {
            normalizationType = children;
            children = data;
            data = undefined;
        }
        // 模式設定
        if (isTrue(alwaysNormalize)) {
            normalizationType = ALWAYS_NORMALIZE;
        }
        return _createElement(context, tag, data, children, normalizationType)
    }

    // context => vm
    // tag => ‘a‘
    // data => {attr:{‘href‘:‘#‘}}
    // children => [vnode...]
    // normalizationType => undefined
    // alwaysNormalize => false
    function _createElement(context, tag, data, children, normalizationType) {
        if (isDef(data) && isDef((data).__ob__)) {
            // warning...
            return createEmptyVNode()
        }
        if (!tag) {
            // in case of component :is set to falsy value
            return createEmptyVNode()
        }
        // support single function children as default scoped slot
        if (Array.isArray(children) && typeof children[0] === ‘function‘) {
            data = data || {};
            data.scopedSlots = {
                default: children[0]
            };
            children.length = 0;
        }
        // 未設置該參數
        if (normalizationType === ALWAYS_NORMALIZE) {
            children = normalizeChildren(children);
        } else if (normalizationType === SIMPLE_NORMALIZE) {
            children = simpleNormalizeChildren(children);
        }
        var vnode, ns;
        if (typeof tag === ‘string‘) {
            var Ctor;
            // 判斷標簽是否為math、SVG
            // math是HTML5新出的標簽 用來寫數學公式
            // SVG就不用解釋了吧……
            ns = config.getTagNamespace(tag);
            // 判斷標簽是否為內置標簽
            if (config.isReservedTag(tag)) {
                // 生成vnode
                // config.parsePlatformTagName返回傳入的值 是一個傻逼函數
                vnode = new VNode(
                    config.parsePlatformTagName(tag), data, children,
                    undefined, undefined, context
                );
            } else if (isDef(Ctor = resolveAsset(context.$options, ‘components‘, tag))) {
                // component
                vnode = createComponent(Ctor, data, context, children, tag);
            } else {
                // 未知標簽
                vnode = new VNode(
                    tag, data, children,
                    undefined, undefined, context
                );
            }
        } else {
            // direct component options / constructor
            vnode = createComponent(tag, data, context, children);
        }
        if (isDef(vnode)) {
            // 特殊標簽處理
            if (ns) {
                applyNS(vnode, ns);
            }
            return vnode
        } else {
            return createEmptyVNode()
        }
    }

  其實吧,這函數看起來那麽長,其實也只能根據傳進去的參數生成一個vnode,具體過程看註釋,看看結果:技術分享技術分享

  可以看出,屬性還是那樣子,沒怎麽變,children是之前生成的那個文本虛擬DOM。

  

  在renderList函數中,循環調用render,分別傳進去items數組的1、2、3、4、5,所以依次生成了5個vnode,作為數組ret的元素,最後返回一個數組:技術分享

  接下來進入外部的_c函數,這一次是對div標簽進行轉化,過程與上面類似,最後生成一個完整的虛擬DOM,如下所示:技術分享

  這裏也就將整個掛載的DOM轉化成了虛擬DOM,其實吧,一點也不難,是吧!

  要不先這樣,下一節再patch……

Vue源碼後記-vFor列表渲染(2)