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


      钩子函数比较简单,没有什么意思,这一节搞点大事情 => 源码中v-for的渲染过程。

      vue的内置指令包含了v-html、v-if、v-once、v-bind、v-on、v-show等,先从一个入手,其余的也就那么回事。

      案例模板依照之前的,但是多加了一个v-for指令,如下所示:

        <body>
            <div id='app'>
                <a href="#" v-for="item in items">{{item}}</a>
            </div>
        </body>
        <script src='./vue.js'></script>
        <script>
            var app = new Vue({
                el: '#app',
                data: {
                    items: [1, 2, 3, 4, 5]
                },
            });
        </script>

      为了保持DOM的纯净,没有添加样式和一些额外杂质。

      

      跳过无用的流程,直接进入不同的地方,首先是compile函数,此处将DOM字符串转化为一个对象,直接跳到baseCompile中:

        function baseCompile(template,options) {
            var ast = parse(template.trim(), options);
            optimize(ast, options);
            var code = generate(ast, options);
            return {
                ast: ast,
                render: code.render,
                staticRenderFns: code.staticRenderFns
            }
        }

      

    一、parse 

      参数就不解释了,进第一个参数解析内部,parse => parseHTML。

      函数会先解析掉<div id='app'>,生成一个对象:,然后遇到回车符号,省略后继续解析,就到了本文要讲的v-for。

      这个函数之前讲过,特别长,不过主要关注一个地方:

        function parseHTML(html, options) {
            // var...
    
            while (html) {
                last = html;
                if (!lastTag || !isPlainTextElement(lastTag)) {
                    var textEnd = html.indexOf('<');
                    if (textEnd === 0) {
                        // code...
    
                        // Start tag:
                        var startTagMatch = parseStartTag();
                        if (startTagMatch) {
                            handleStartTag(startTagMatch);
                            continue
                        }
                    }
    
                    // code...
                } else {
                    // code...
                }
    
                // code...
            }
    
            // Clean up any remaining tags
            parseEndTag();
    
            function advance(n) {
                index += n;
                html = html.substring(n);
            }
    
            function parseStartTag() {
                // code...
            }
    
            function handleStartTag(match) {
                // code...
            }
    
            function parseEndTag(tagName, start, end) {
                // code...
            }
        }

      就是对startTag进行处理的两个函数,第一个parseStartTag函数将字符串切割成如图的对象:,这里attrs有两个,一个href属性,一个是v-for属性,只是做正则切割,没有区分是HTML属性还是vue属性。

      第二个handleStartTag负责将对象进行二次处理,因为可能包含某些特殊的属性,这里只需要关注一个start函数:

        function handleStartTag(match) {
            var tagName = match.tagName;
            var unarySlash = match.unarySlash;
    
            // code...
    
            if (options.start) {
                options.start(tagName, attrs, unary, match.start, match.end);
            }
        }

      函数接受5个参数,分别为标签名、属性、是否一元、字符串开始索引、字符串结束索引,这个函数也是长得要死,直接看重点:

        function start(tag, attrs, unary) {
            // code...
    
            if (inVPre) {
                processRawAttrs(element);
            } else {
                processFor(element);
                processIf(element);
                processOnce(element);
                processKey(element);
    
                // determine whether this is a plain element after
                // removing structural attributes
                element.plain = !element.key && !attrs.length;
    
                processRef(element);
                processSlot(element);
                processComponent(element);
                for (var i$1 = 0; i$1 < transforms.length; i$1++) {
                    transforms[i$1](element, options);
                }
                processAttrs(element);
            }
    
            function checkRootConstraints(el) {
                // code...
            }
    
            // code...
        }

      重点看中间那部分,会对内置指令作处理,跑源码的时候全部跳过了,这里就需要进来看看:

        function processFor(el) {
            var exp;
            // getAndRemoveAttr函数将v-for的值从attrsMap中取出,并将attrsList中对应的删除
            // exp => item in items
            if ((exp = getAndRemoveAttr(el, 'v-for'))) {
                // forAliasRE正则切割in或of
                // item in items => ['item in items','item','items']
                var inMatch = exp.match(forAliasRE);
                if (!inMatch) {
                    "development" !== 'production' && warn$2(
                        ("Invalid v-for expression: " + exp)
                    );
                    return
                }
                // for的数据源 => items
                el.for = inMatch[2].trim();
                // 列表数据别名 => item
                var alias = inMatch[1].trim();
                // 这个iterator暂时不清楚干嘛的 我的v-for表达式改成'item in 5'这里也是null
                var iteratorMatch = alias.match(forIteratorRE);
                if (iteratorMatch) {
                    el.alias = iteratorMatch[1].trim();
                    el.iterator1 = iteratorMatch[2].trim();
                    if (iteratorMatch[3]) {
                        el.iterator2 = iteratorMatch[3].trim();
                    }
                } else {
                    el.alias = alias;
                }
            }
        }

      函数执行完后,在el对象上添加了2个属性:for、alias。如图所示:

    二、optimize

      这个没什么好讲,因为DOM节点有v-for属性,所以被认定为非静态节点,staic属性标记为false。

    三、generate

      这一步将ast打包成一个函数,有一个地方也会专门处理v-for属性: 

        function generate(ast,options) {
            // var code...
    
            var code = ast ? genElement(ast) : '_c("div")';
            staticRenderFns = prevStaticRenderFns;
            onceCount = prevOnceCount;
            return {
                render: ("with(this){return " + code + "}"),
                staticRenderFns: currentStaticRenderFns
            }
        }

      跳过前面声明变量的代码,这里的genElement会对ast对象做转化处理,如下:

        function genElement(el) {
            if (el.staticRoot && !el.staticProcessed) {
                return genStatic(el)
            } else if (el.once && !el.onceProcessed) {
                return genOnce(el)
            } else if (el.for && !el.forProcessed) {
                return genFor(el)
            } else if (el.if && !el.ifProcessed) {
                return genIf(el)
            } else if (el.tag === 'template' && !el.slotTarget) {
                return genChildren(el) || 'void 0'
            } else if (el.tag === 'slot') {
                return genSlot(el)
            } else {
                // component or element...
            }
        }

      可以看到,针对各个特殊属性,有专门的gen函数处理,这里只看genFor就行了:

        function genFor(el) {
            // items
            var exp = el.for;
            // item
            var alias = el.alias;
            // ''
            var iterator1 = el.iterator1 ? ("," + (el.iterator1)) : '';
            var iterator2 = el.iterator2 ? ("," + (el.iterator2)) : '';
    
            // key warning...
    
            // 表示for属性处理完了 避免递归
            el.forProcessed = true;
            return "_l((" + exp + ")," +
                "function(" + alias + iterator1 + iterator2 + "){" +
                "return " + (genElement(el)) +
                '})'
        }

      v-for的处理比一般的要特殊一些,可以看到,这里再次调用了genElement处理其余属性,由于节点标记了forProcessed,所以不会再次进入这个函数。

      第二次调用genElement时,会跳到最后,并生成一个去除v-for属性的gen字符串:

      这个字符串是在处理v-for函数中返回的一部分,所有的字符串加起来变成了这样:

      前面的属于最外层div,后面_l属于有v-for属性的a标签,而最后的_v是a标签的文本内容。

      这样,ast的generate就处理完了。

      有了render函数,下面就是vnode的生成和patch过程了。

      往下跑,会调用一连串的函数,包含watcher、update等等,截取一下关键的代码片段:

        function mountComponent(vm, el, hydrating) {
    
            // 'beforeMount'
    
            var updateComponent;
            /* istanbul ignore if */
            if ("development" !== 'production' && config.performance && mark) {
                // 开发者模式下的update
            } else {
                updateComponent = function() {
                    vm._update(vm._render(), hydrating);
                };
            }
    
            vm._watcher = new Watcher(vm, updateComponent, noop);
            hydrating = false;
    
            // 'mounted'
    
            return vm
        }
    
        var Watcher = function Watcher(vm, expOrFn, cb, options) {
            // this.a...
            // this.b...
    
            // 此处expOrFn为上面的updateComponent
            if (typeof expOrFn === 'function') {
                this.getter = expOrFn;
            } else {
                this.getter = parsePath(expOrFn);
                if (!this.getter) {
                    // warning...
                }
            }
            this.value = this.lazy ?
                undefined :
                this.get();
        };
    
        Watcher.prototype.get = function get() {
            // var...
    
            if (this.user) {
                // try:value = this.getter.call(vm, vm);
            } else {
                // 调用上面的expOrFn
                // 即vm._update(vm._render(), hydrating);
                value = this.getter.call(vm, vm);
            }
    
            // code...
            return value
        };
    
        Vue.prototype._render = function() {
            // var...
    
            try {
                // render => return _c('div',{attrs:{"id":"app"}},_l((items),function(item){return _c('a',{attrs:{"href":"#"}},[_v(_s(item))])}))
                vnode = render.call(vm._renderProxy, vm.$createElement);
            } catch (e) {
                // warning...
            }
    
            // warning
    
            return vnode
        };

      最后面那个vode会返回到第二个函数,作为vm._watcher对象的value属性保存起来。

      之前跑源码,跳过了vnode的生成过程,这次硬刚一波!

      

    不要怂,干!

      分析一下这个字符串函数,首先忽略那个with(this),没啥解释的,然后是return的主体函数,函数名为_c,这是一个缩写,后面再说,传入了5个参数,分别为:

      1、'div' => 根标签的tagName

      2、{attrs:{"id":"app"}} => 根标签的相关属性

      3、_l((items) => v-for相关函数

      4、function(item){return _c('a',{attrs:{"href":"#"}} => v-for相关函数,包含了a标签的tagName与属性

      5、[_v(_s(item))] => a标签文本

      注意到该函数是用call调用,并且第一个参数传了vm._renderProxy作为执行上下文。而这个vm._renderProxy是什么呢?是一个代理,代码如下:

        initProxy = function initProxy(vm) {
            if (hasProxy) {
                var options = vm.$options;
                var handlers = options.render && options.render._withStripped ?
                    getHandler :
                    hasHandler;
                // 此处handlers => hasHandler
                vm._renderProxy = new Proxy(vm, handlers);
            } else {
                vm._renderProxy = vm;
            }
        };
    
        var hasHandler = {
            has: function has(target, key) {
                var has = key in target;
                var isAllowed = allowedGlobals(key) || key.charAt(0) === '_';
                if (!has && !isAllowed) {
                    warnNonPresent(target, key);
                }
                return has || !isAllowed
            }
        };
    
        var allowedGlobals = makeMap(
            'Infinity,undefined,NaN,isFinite,isNaN,' +
            'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
            'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
            'require' // for Webpack/Browserify
        );

      关于这个Proxy构造函数,是ES6的新特性,专门去阮老师的开源书里摘抄一下介绍:

      

      重点看这句:外界对该对象的访问,都必须通过这层拦截。在这里,vm被设置了一个has拦截器,该拦截器的说明如下:

      

      简单来说,当访问对象属性时,会被has拦截,并调用对应的方法来执行一些过滤。

      

      回到render.cal那里,把函数美化一下如下:

        (function() {
            with(this){
                return _c('div',{attrs:{"id":"app"}},_l((items),function(item){return _c('a',{attrs:{"href":"#"}},[_v(_s(item))])}))
            }
        })

      with中的this指的是vm,即当前vue实例,所以_c调用的实际是vm._c,因为访问了属性,所以会调用拦截器对_c进行过滤,跑一个看看过程:

        // target => vm
        // key => _c
        has: function has(target, key) {
            // 判断属性是否在vue实例上
            var has = key in target;
            // allowedGlobals是所有内置的全局方法
            // 缩写方法都是_开头
            var isAllowed = allowedGlobals(key) || key.charAt(0) === '_';
            if (!has && !isAllowed) {
                warnNonPresent(target, key);
            }
            return has || !isAllowed
        }

      简单来讲,has拦截器做了两重判断:

    一、判断vue实例上是否有此方法

      这个_c方法早在beforeCreated的时候就添加上了,如下:

        Vue.prototype._init = function(options) {
            // code...
    
            initLifecycle(vm);
            initEvents(vm);
            initRender(vm);
            callHook(vm, 'beforeCreate');
            initInjections(vm); // resolve injections before data/props
            initState(vm);
            initProvide(vm); // resolve provide after data/props
            callHook(vm, 'created');
    
            // code...
        };
    
        function initRender(vm) {
            // code...
    
            vm._c = function(a, b, c, d) {
                return createElement(vm, a, b, c, d, false);
            };
    
            // code...
        }

      至于这个方法是干啥的之后再说,反正vue实例上有这个方法,因此has变量值为true。

    二、判断该方法是否是合法的内置全局方法或是否以_开头

      vue对象在初始化时,原型上就添加了一系列以_开头的方法,如果一个方法不在原型上又以_ 开头会造成混淆,所以会做此判断,如下:

        renderMixin(Vue$3);
    
        function renderMixin(Vue) {
            Vue.prototype.$nextTick = function(fn) {
                // code...
            };
    
            Vue.prototype._render = function() {
                // code...
            };
    
            // internal render helpers.
            // these are exposed on the instance prototype to reduce generated render
            // code size.
            Vue.prototype._o = markOnce;
            Vue.prototype._n = toNumber;
            Vue.prototype._s = toString;
            Vue.prototype._l = renderList;
            Vue.prototype._t = renderSlot;
            Vue.prototype._q = looseEqual;
            Vue.prototype._i = looseIndexOf;
            Vue.prototype._m = renderStatic;
            Vue.prototype._f = resolveFilter;
            Vue.prototype._k = checkKeyCodes;
            Vue.prototype._b = bindObjectProps;
            Vue.prototype._v = createTextVNode;
            Vue.prototype._e = createEmptyVNode;
            Vue.prototype._u = resolveScopedSlots;
        }

      这些方法全是用来生成虚拟DOM的工具方法。

      当检测出问题时,会调用warnNonPresent报错返回false,正常情况会返回true。

      

      这个玩意比我想象中的还要复杂,分开写吧!

  • 相关阅读:
    爱情十二课,失恋后遗症
    爱情十三课,爱人的五功能
    爱情第八课,爱也是投资
    爱情第二课,择爱两大误区
    爱情十七课,吵架的原则
    MFC DC的获取
    MFC关于使用CArchive流输入产生的问题
    MFCCFileException类学习笔记
    MFC中指针的获取
    文字编辑和文字处理
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/7274874.html
Copyright © 2020-2023  润新知