zoukankan      html  css  js  c++  java
  • Vue源码后记-其余内置指令(3)

      其实吧,写这些后记我才真正了解到vue源码的精髓,之前的跑源码跟闹着玩一样。

      

      go!

      之前将AST转换成了render函数,跳出来后,由于仍是字符串,所以调用了makeFunction将其转换成了真正的函数:

        function compileToFunctions(template, options, vm) {
            // code...
    
            // compile
            var compiled = compile(template, options);
    
            // code...
    
            // 转换render
            res.render = makeFunction(compiled.render, fnGenErrors);
            var l = compiled.staticRenderFns.length;
            // 转换staticRenderFns
            res.staticRenderFns = new Array(l);
            for (var i = 0; i < l; i++) {
                res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i], fnGenErrors);
            }
    
            // code...
    
            return (functionCompileCache[key] = res)
        }
    
        function makeFunction(code, errors) {
            try {
                return new Function(code)
            } catch (err) {
                // error...
            }
        }

      这个没啥讲的

      将render转换成VNode其实也没什么讲的,重点看一下之前没见过的函数,

        _c('div' /*<div id='app'>*/ , {
            attrs: {
                "id": "app"
            }
        }, [(vIfIter) /*v-if条件*/ ?
            // 条件为真渲染下面的DOM
            _c('div' /*<div v-if="vIfIter" v-bind:style="styleObject">*/ , {
                style: (styleObject)
            }, [_c('input' /*<input v-show="vShowIter" v-model='vModel' />*/ , {
                    directives: [{
                        name: "show",
                        rawName: "v-show",
                        value: (vShowIter),
                        expression: "vShowIter"
                    }, {
                        name: "model",
                        rawName: "v-model",
                        value: (vModel),
                        expression: "vModel"
                    }],
                    domProps: {
                        "value": (vModel)
                    },
                    on: {
                        "input": function($event) {
                            if ($event.target.composing) return;
                            vModel = $event.target.value
                        }
                    }
                }),
                _v(" ") /*这些是回车换行符*/ ,
                _m(0) /*<span v-once>{{msg}}</span>*/ , _v(" "),
                _c('div' /*<div v-html="html"></div>*/ , {
                    domProps: {
                        "innerHTML": _s(html)
                    }
                })
            ]) :
            // 否则渲染一个空的div...(错了)
            _e() /*comment*/ ,
            _v(" "),
            _c('div' /*<div class='on'>empty Node</div>*/ , {
                staticClass: "on"
            }, [_v("empty Node")])
        ])

      该render函数包含_c、_v、_m、_e、_s5个函数,其中_c、_v、_s之前都讲过,这里看一下_m、_e是什么。

    _m

      直接看源码:

        Vue.prototype._m = renderStatic;
    
        function renderStatic(index, isInFor) {
            var tree = this._staticTrees[index];
            // 如果该静态节点已经被渲染且不在v-for中
            // 复用该节点
            if (tree && !isInFor) {
                return Array.isArray(tree) ?
                    cloneVNodes(tree) :
                    cloneVNode(tree)
            }
            // otherwise, render a fresh tree.
            tree = this._staticTrees[index] =
                this.$options.staticRenderFns[index].call(this._renderProxy);
            markStatic(tree, ("__static__" + index), false);
            return tree
        }

      可以看到,对于静态节点,vue做了一层缓存,尽量复用现成的虚拟DOM,但是目前是初次渲染,所以会创建一个新的。

      这里有两步。

    第一步:this.$options.staticRenderFns[index].call(this._renderProxy)

      即取出staticRenderFns对应索引的函数并执行,将其缓存到_staticTrees上。

      之前在生成render函数时,将v-once的节点当成静态节点处理,弹入了该数组,函数如下:

        (function() {
            with(this) {
                return _c('span', [_v(_s(msg))])
            }
        })

      这里_s将msg字符串化,_v生成一个文本VNode,_c生成一个带有tag的VNode,children为之前的VNode。

    第二步:markStatic(tree, ("__static__" + index), false)

      给VNode做标记。

        // tree => VNode
        // key => __static__0 
        // isonce => false
        function markStatic(tree, key, isOnce) {
            if (Array.isArray(tree)) {
                for (var i = 0; i < tree.length; i++) {
                    if (tree[i] && typeof tree[i] !== 'string') {
                        // key => __static__0_0...
                        markStaticNode(tree[i], (key + "_" + i), isOnce);
                    }
                }
            } else {
                markStaticNode(tree, key, isOnce);
            }
        }
    
        function markStaticNode(node, key, isOnce) {
            node.isStatic = true;
            node.key = key;
            node.isOnce = isOnce;
        }

      比较简单,直接看结果了:

      

    _e

      这个其实我在注释里写了,就是一个空的div,瞄一眼源码,发现我错了:

        Vue.prototype._e = createEmptyVNode;
    
        var createEmptyVNode = function() {
            var node = new VNode();
            node.text = '';
            node.isComment = true;
            return node
        };

      生成一个空的VNode,将其标记为注释,内容为空。

      //剩下的太简单,我不想讲啦!撤了,最近心情不好,兼容360,这客户我真是日了狗了。

      还没完,讲讲patch阶段那些directives、on、domProps是如何渲染的吧!

    input

        <input v-show="vShowIter" v-model='vModel' />

      

      VNode中的data属性细节可以看图,这里看一下domProps、on是如何渲染的。

      首先on是事件相关,刚发现chrome调试一个特别好用的东西,可以看函数流向!

      

      图中patch是渲染DOM的入口函数,createElm生成DOM节点,createChildren递归处理子节点,invokeCreateHooks则负责处理节点的属性,updateDOMListeners很显然是处理事件绑定,看一下源码:

        function updateDOMListeners(oldVnode, vnode) {
            // 新旧VNode至少有一个存在on属性
            if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
                return
            }
            // 保存属性
            var on = vnode.data.on || {};
            var oldOn = oldVnode.data.on || {};
            target$1 = vnode.elm;
            // 特殊情况处理
            normalizeEvents(on);
            updateListeners(on, oldOn, add$1, remove$2, vnode.context);
        }

      除去判断,这里会先对特殊情况下的on做特殊处理,然后再进行事件绑定,可以看下处理的代码:

        function normalizeEvents(on) {
            var event;
            /* istanbul ignore if */
            if (isDef(on[RANGE_TOKEN])) {
                // IE input[type=range] only supports `change` event
                event = isIE ? 'change' : 'input';
                on[event] = [].concat(on[RANGE_TOKEN], on[event] || []);
                delete on[RANGE_TOKEN];
            }
            if (isDef(on[CHECKBOX_RADIO_TOKEN])) {
                // Chrome fires microtasks in between click/change, leads to #4521
                event = isChrome ? 'click' : 'change';
                on[event] = [].concat(on[CHECKBOX_RADIO_TOKEN], on[event] || []);
                delete on[CHECKBOX_RADIO_TOKEN];
            }
        }

      可以看到,特殊情况有两种:

      第一种是IE下的type=range,这个H5属性只支持IE10+,并且在IE中只有change事件。

      第二种是Chrome下的radio、checkbox,事件类型会被置换为click。

      接下来是事件的绑定函数:

        // on/oldOn => 新旧VNode事件
        // add/remove$$1 => 事件的绑定与解绑函数
        function updateListeners(on, oldOn, add, remove$$1, vm) {
            var name, cur, old, event;
            for (name in on) {
                cur = on[name];
                old = oldOn[name];
                // str => obj
                event = normalizeEvent(name);
                if (isUndef(cur)) {
                    // error
                }
                // 添加事件 
                else if (isUndef(old)) {
                    if (isUndef(cur.fns)) {
                        cur = on[name] = createFnInvoker(cur);
                    }
                    add(event.name, cur, event.once, event.capture, event.passive);
                }
                // 事件替换
                else if (cur !== old) {
                    old.fns = cur;
                    on[name] = old;
                }
            }
            // 旧VNode事件解绑
            for (name in oldOn) {
                if (isUndef(on[name])) {
                    event = normalizeEvent(name);
                    remove$$1(event.name, oldOn[name], event.capture);
                }
            }
        }

      在遍历所有事件类型字符串的时候,由于可能会有特殊标记,所以会对其进行解析转换为一个对象:

        var normalizeEvent = cached(function(name) {
            var passive = name.charAt(0) === '&';
            name = passive ? name.slice(1) : name;
            var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first
            name = once$$1 ? name.slice(1) : name;
            var capture = name.charAt(0) === '!';
            name = capture ? name.slice(1) : name;
            return {
                name: name,
                once: once$$1,
                capture: capture,
                passive: passive
            }
        });

      意思简单明了,这里就不解释了。

      接下来会将事件处理函数作为属性挂载到一个invoker函数上:

        // 将函数或函数数组作为属性挂到函数上 可以调用执行
        function createFnInvoker(fns) {
            function invoker() {
                var arguments$1 = arguments;
    
                var fns = invoker.fns;
                if (Array.isArray(fns)) {
                    for (var i = 0; i < fns.length; i++) {
                        fns[i].apply(null, arguments$1);
                    }
                } else {
                    // return handler return value for single handlers
                    return fns.apply(null, arguments)
                }
            }
            invoker.fns = fns;
            return invoker
        }

      这样做的原因可能是方便执行事件,有时候一个DOM会有多个相同事件,此时事件会是一个数组,通过这样处理后,无论是单一函数还是函数数组都可以通过直接调用invoker来执行。

      下面就是最后一个步骤,事件绑定:

        // event => input
        // handler => invoker
        // 剩余三个为之前normalizeEvent的属性
        function add$1(event, handler, once$$1, capture, passive) {
            // 一次性执行事件
            if (once$$1) {
                var oldHandler = handler;
                var _target = target$1;
                handler = function(ev) {
                    // 单参数 or 多参数
                    var res = arguments.length === 1 ?
                        oldHandler(ev) :
                        oldHandler.apply(null, arguments);
                    // 执行完立马解绑事件
                    if (res !== null) {
                        remove$2(event, handler, capture, _target);
                    }
                };
            }
            target$1.addEventListener(
                event,
                handler,
                supportsPassive ? {
                    capture: capture,
                    passive: passive
                } :
                capture
            );
        }

      对于一次性执行事件,这里的处理和jQuery源码里还是蛮像的,不过要简洁多了,这个很简单,没啥讲的。

      下面处理domProps,起初我以为这个属性是专门处理组件间传值那个props的,后来发现这属性有点瞎:

        function updateDOMProps(oldVnode, vnode) {
            if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {
                return
            }
            var key, cur;
            var elm = vnode.elm;
            var oldProps = oldVnode.data.domProps || {};
            var props = vnode.data.domProps || {};
            // __ob__属性代表动态变化的值
            if (isDef(props.__ob__)) {
                props = vnode.data.domProps = extend({}, props);
            }
            // 新VNode缺失属性置为空
            for (key in oldProps) {
                if (isUndef(props[key])) {
                    elm[key] = '';
                }
            }
            for (key in props) {
                cur = props[key];
                // 这两种情况特殊处理
                if (key === 'textContent' || key === 'innerHTML') {
              if (vnode.children) { vnode.children.length = 0; }
              if (cur === oldProps[key]) { continue }
                }
    
                if (key === 'value') {
                    // 先保存值 之后所有值会被转为字符串
                    elm._value = cur;
                    // avoid resetting cursor position when value is the same
                    var strCur = isUndef(cur) ? '' : String(cur);
                    if (shouldUpdateValue(elm, vnode, strCur)) {
                        elm.value = strCur;
                    }
                } else {
                    elm[key] = cur;
                }
            }
        }
    
        function shouldUpdateValue(elm, vnode, checkVal) {
            return (!elm.composing && (
                vnode.tag === 'option' ||
                // document.activeElement !== elm && elm.value !== checkVal
                isDirty(elm, checkVal) ||
                // 处理trim,number
                isInputChanged(elm, checkVal)
            ));
        }

      其中包括两种情况,textContent、innerHTML、value以及其他,此处props的值为value,会判断当前DOM的值是否一致,然后进行修正。

      当props为textContent或innerHTML时,需要将所有子节点清空,然后将对应的属性修改为对应的值。

      至此,基本上必要的点已经完事了~啊。。。。88

  • 相关阅读:
    Docker服务启动报错:Job for docker.service failed because the control process exited with error code.
    mysql忘记密码如何重置及修改密码
    linux下的/opt目录作用
    linux防火墙查看状态firewall、iptable
    nmap基本使用方法
    HTTP响应码大全
    端口镜像
    查看占用端口
    restful规范 APIview 解析器组件 Postman
    状态码301和302的区别
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/7374683.html
Copyright © 2011-2022 走看看