• Vue源码后记-vFor列表渲染(2)


    这一节争取搞完!

      

      回头来看看那个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……

  • 相关阅读:
    requests模块
    unitest模块
    doctest模块
    SessionStorage
    jquery选择器
    jquery操作dom
    jquery事件
    jquery筛选
    页面跳转传值接收
    HTML5 Web SQL 数据库操作
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/7280489.html
Copyright © 2020-2023  润新知