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

posted @ 2017-08-17 17:13  书生小龙  阅读(620)  评论(0编辑  收藏  举报