.7-Vue源码之AST(3)

  上一节到了parseHTML函数,该函数接受一个字符串与一个对象,字符串即对应的DOM,对象包含几个字符串匹配集及3个长函数。

  简略梳理部分函数代码如下:

    // Line-7672
    function parseHTML(html, options) {
        var stack = [];
        var expectHTML = options.expectHTML;
        var isUnaryTag$$1 = options.isUnaryTag || no;
        var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
        var index = 0;
        var last, lastTag;
        while (html) {
            last = html;
            // 排除script,style,textarea三个标签
            if (!lastTag || !isPlainTextElement(lastTag)) {
                var textEnd = html.indexOf('<');
                if (textEnd === 0) {
                    // 截取注释
                    if (comment.test(html)) {
                        var commentEnd = html.indexOf('-->');

                        if (commentEnd >= 0) {
                            advance(commentEnd + 3);
                            continue
                        }
                    }
                    // 处理向下兼容的注释 比如说<!--[if lt IE 9]>
                    if (conditionalComment.test(html)) {
                        var conditionalEnd = html.indexOf(']>');

                        if (conditionalEnd >= 0) {
                            advance(conditionalEnd + 2);
                            continue
                        }
                    }
                    // Doctype:
                    var doctypeMatch = html.match(doctype);
                    if (doctypeMatch) {
                        advance(doctypeMatch[0].length);
                        continue
                    }
                    // End tag:
                    var endTagMatch = html.match(endTag);
                    if (endTagMatch) {
                        var curIndex = index;
                        advance(endTagMatch[0].length);
                        parseEndTag(endTagMatch[1], curIndex, index);
                        continue
                    }
                    // Start tag:
                    // 匹配起始标签
                    var startTagMatch = parseStartTag();
                    if (startTagMatch) {
                        handleStartTag(startTagMatch);
                        continue
                    }
                }
                // 初始化为undefined 这样安全且字符数少一点
                var text = (void 0),
                    rest$1 = (void 0),
                    next = (void 0);
                if (textEnd >= 0) {
                    rest$1 = html.slice(textEnd);
                    while (!endTag.test(rest$1) &&
                        !startTagOpen.test(rest$1) &&
                        !comment.test(rest$1) &&
                        !conditionalComment.test(rest$1)
                    ) {
                        // 处理文本中的<字符
                        next = rest$1.indexOf('<', 1);
                        if (next < 0) {
                            break
                        }
                        textEnd += next;
                        rest$1 = html.slice(textEnd);
                    }
                    text = html.substring(0, textEnd);
                    advance(textEnd);
                }

                if (textEnd < 0) {
                    text = html;
                    html = '';
                }

                if (options.chars && text) {
                    options.chars(text);
                }
            } else {
                /* code... */
            }

            if (html === last) {
                /* code... */
            }
        }
        // Clean up any remaining tags
        parseEndTag();

        function advance(n) {
            /* code... */
        }

        function parseStartTag() {
            /* code... */
        }

        function handleStartTag(match) {
            /* code... */
        }

        function parseEndTag(tagName, start, end) {
            /* code... */
        }
    }

  函数比较长,除去开头的参数获取,后面直接用while循环开始对字符串进行切割。

  在判断标签不是script,style,textarea三个特殊标签后,当字符串以<开头时,以注释、向下兼容注释、Doctype、结束标签、起始标签的顺序依次切割。

  由于案例中字符串是以<div开头,所以直接跳到起始标签的匹配:

    // Line-7722
    var startTagMatch = parseStartTag();
    if (startTagMatch) {
        handleStartTag(startTagMatch);
        continue
    }

    // Line-7795
    function parseStartTag() {
        // 正则匹配
        var start = html.match(startTagOpen);
        if (start) {
            var match = {
                // 标签名(div)
                tagName: start[1],
                // 属性
                attrs: [],
                // 游标索引(初始为0)
                start: index
            };
            advance(start[0].length);
            var end, attr;
            // 进行属性的正则匹配
            // startTagClose匹配/>或>
            // attribute匹配属性 正则太长 没法讲
            // 本例中attr匹配后 => ['id=app','id','=','app']
            while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
                advance(attr[0].length);
                // 属性加入
                match.attrs.push(attr);
            }
            // 在第二次while循环后 end匹配到结束标签 => ['>','']
            if (end) {
                match.unarySlash = end[1];
                advance(end[0].length);
                // 标记结束位置
                match.end = index;
                // 返回匹配对象
                return match
            }
        }
    }

    // Line-7790
    // 该函数将函数局部变量index往前推 并切割字符串
    function advance(n) {
        index += n;
        html = html.substring(n);
    }

 

  可以看到,通过起始标签的匹配,字符串的<div id='app'>已经被切割出来,保存在一个对象中返回:

  接下来,会调用handleStartTag方法再次处理返回的对象,看一下这个方法:

    // Line-7818
    function handleStartTag(match) {
        var tagName = match.tagName;
        var unarySlash = match.unarySlash;

        if (expectHTML) {
            // PhrasingTag(段落元素)涉及到标签元素类型 具体可见http://www.5icool.org/a/201308/a2081.html
            if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
                parseEndTag(lastTag);
            }
            // 可以省略闭合标签
            if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {
                parseEndTag(tagName);
            }
        }

        // 自闭合标签
        var unary = isUnaryTag$$1(tagName) || tagName === 'html' && lastTag === 'head' || !!unarySlash;

        // 记录属性个数 目前只有一个id属性
        var l = match.attrs.length;
        var attrs = new Array(l);
        for (var i = 0; i < l; i++) {
            var args = match.attrs[i];
            // 一个bug 在对(.)?匹配时会出现
            if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
                if (args[3] === '') {
                    delete args[3];
                }
                if (args[4] === '') {
                    delete args[4];
                }
                if (args[5] === '') {
                    delete args[5];
                }
            }
            // 匹配属性名app
            var value = args[3] || args[4] || args[5] || '';
            attrs[i] = {
                name: args[1],
                // 处理转义字符
                value: decodeAttr(
                    value,
                    options.shouldDecodeNewlines
                )
            };
        }
        // 将切割出来的字符串转换为AST
        if (!unary) {
            stack.push({
                tag: tagName,
                lowerCasedTag: tagName.toLowerCase(),
                attrs: attrs
            });
            // 标记结束标签
            lastTag = tagName;
        }

        // 这是参数中第一个函数
        if (options.start) {
            options.start(tagName, attrs, unary, match.start, match.end);
        }
    }

    // Line-7667
    function decodeAttr(value, shouldDecodeNewlines) {
        // lg,gt等字符的正则
        var re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr;
        return value.replace(re, function(match) {
            return decodingMap[match];
        })
    }

  在该函数中,对之前的对象进行了二次处理,根据标签名、属性生成一个新对象,push到最开始的stack数组中,结果如图所示:

  由于匹配的是起始标签,所以也会以这个标签名结束,因此被标记为最后的结束标签,即前面一直是undefined的lastTag。

 

  最后,调用了一个start函数,蒙了半天没找到,后来才发现是最开始传进来的参数中有3个函数:start、end、chars,现在可以看一下这个方法干啥用的了。

  start方法只接受3个参数,这里传了5个,后面2个被忽略,参数情况是这样的:

  这个方法比较长:

    // Line-8026
    function start(tag, attrs, unary) {
        // 检查命名空间是否是svg或者math
        var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);

        // handle IE svg bug
        if (isIE && ns === 'svg') {
            attrs = guardIESVGBug(attrs);
        }

        var element = {
            type: 1,
            tag: tag,
            // {name:'id',value:'app'}
            attrsList: attrs,
            // {id:'app'}
            attrsMap: makeAttrsMap(attrs),
            parent: currentParent,
            children: []
        };
        if (ns) {
            element.ns = ns;
        }
        // 检查tag属性是否是style、script
        if (isForbiddenTag(element) && !isServerRendering()) {
            element.forbidden = true;
            /* warning */
        }

        // apply pre-transforms 本例中没有
        // Line-7990:preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
        for (var i = 0; i < preTransforms.length; i++) {
            preTransforms[i](element, options);
        }

        if (!inVPre) {
            // 判断是否有v-pre属性
            processPre(element);
            if (element.pre) {
                inVPre = true;
            }
        }
        // 判断tag是不是pre
        if (platformIsPreTag(element.tag)) {
            inPre = true;
        }
        // 分支跳转到else
        if (inVPre) {
            processRawAttrs(element);
        } else {
            // 处理v-for
            processFor(element);
            // 处理v-if,v-else,v-else-if
            processIf(element);
            // 处理v-once
            processOnce(element);
            // 处理:
            processKey(element);

            // 检测是否是空属性节点
            element.plain = !element.key && !attrs.length;

            // 处理:ref或v-bind:ref属性
            processRef(element);
            // 当tag为slot时
            processSlot(element);
            // 处理:is或v-bind:is属性
            processComponent(element);
            // Line-7991:transforms = pluckModuleFunction(options.modules, 'transformNode');
            // 处理class与style属性 包括原始的和通过:动态绑定
            for (var i$1 = 0; i$1 < transforms.length; i$1++) {
                transforms[i$1](element, options);
            }
            // 处理属性
            processAttrs(element);
        }

        // 根元素不允许为slot或template 且不能有v-for属性
        // 总之必须为单一不可变的节点
        function checkRootConstraints(el) {
            {
                if (el.tag === 'slot' || el.tag === 'template') {
                    warnOnce(
                        "Cannot use <" + (el.tag) + "> as component root element because it may " +
                        'contain multiple nodes.'
                    );
                }
                if (el.attrsMap.hasOwnProperty('v-for')) {
                    warnOnce(
                        'Cannot use v-for on stateful component root element because ' +
                        'it renders multiple elements.'
                    );
                }
            }
        }

        // tree management
        // 这个root是在parse函数开始的时候定义的
        if (!root) {
            root = element;
            checkRootConstraints(root);
        } else if (!stack.length) {
            // allow root elements with v-if, v-else-if and v-else
            if (root.if && (element.elseif || element.else)) {
                checkRootConstraints(element);
                addIfCondition(root, {
                    exp: element.elseif,
                    block: element
                });
            } else {
                warnOnce(
                    "Component template should contain exactly one root element. " +
                    "If you are using v-if on multiple elements, " +
                    "use v-else-if to chain them instead."
                );
            }
        }
        // 没有父元素 跳过
        if (currentParent && !element.forbidden) {
            if (element.elseif || element.else) {
                processIfConditions(element, currentParent);
            } else if (element.slotScope) { // scoped slot
                currentParent.plain = false;
                var name = element.slotTarget || '"default"';
                (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
            } else {
                currentParent.children.push(element);
                element.parent = currentParent;
            }
        }
        if (!unary) {
            currentParent = element;
            stack.push(element);
        } else {
            endPre(element);
        }
        // 没有这个 跳过
        // Line-7992:postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');
        for (var i$2 = 0; i$2 < postTransforms.length; i$2++) {
            postTransforms[i$2](element, options);
        }
    }

    // Line-8470
    // 函数作用:attrs={name:'id',value:'app'} => map = {id : app}
    function makeAttrsMap(attrs) {
        var map = {};
        for (var i = 0, l = attrs.length; i < l; i++) {
            // 检测重复属性名
            if (
                "development" !== 'production' &&
                map[attrs[i].name] && !isIE && !isEdge
            ) {
                warn$2('duplicate attribute: ' + attrs[i].name);
            }
            map[attrs[i].name] = attrs[i].value;
        }
        return map
    }

 

  这个方法首先对标签名进行校验,然后再对属性进行更细致的处理,比如说v-pre,v-for,v-if等等,案例里没有,所以暂时不分析如何处理,下次再搞。

  值得注意的是transforms这个数组,包含两个函数:transformNode与transformNode$1,其实只是对静态或动态绑定的class与style进行处理,代码如下:

    // Line-9416
    function transformNode(el, options) {
        var warn = options.warn || baseWarn;
        // 获取原始class属性
        var staticClass = getAndRemoveAttr(el, 'class');
        if ("development" !== 'production' && staticClass) {
            var expression = parseText(staticClass, options.delimiters);
            if (expression) {
                /*<div class="{{ val }}"> => <div :class="val"> */
            }
        }
        // 将原始class属性保存为属性
        if (staticClass) {
            el.staticClass = JSON.stringify(staticClass);
        }
        // 获取:class 
        var classBinding = getBindingAttr(el, 'class', false /* getStatic */ );
        if (classBinding) {
            el.classBinding = classBinding;
        }
    }

    // Line-9458
    function transformNode$1(el, options) {
        var warn = options.warn || baseWarn;
        var staticStyle = getAndRemoveAttr(el, 'style');
        if (staticStyle) {
            /* istanbul ignore if */
            {
                var expression = parseText(staticStyle, options.delimiters);
                if (expression) {
                    /*<div style="{{ val }}"> => <div :style="val"> */
                }
            }
            el.staticStyle = JSON.stringify(parseStyleText(staticStyle));
        }

        var styleBinding = getBindingAttr(el, 'style', false /* getStatic */ );
        if (styleBinding) {
            el.styleBinding = styleBinding;
        }
    }

  在最后,调用processAttrs对动态绑定的属性(v-,@,:)进行处理,代码如下:

    // Line-8376
    function processAttrs(el) {
        // {name:'id',value:'app'}
        var list = el.attrsList;
        var i, l, name, rawName, value, modifiers, isProp;
        for (i = 0, l = list.length; i < l; i++) {
            // id
            name = rawName = list[i].name;
            // app
            value = list[i].value;
            // dirRE => 以v-、@、:开头
            if (dirRE.test(name)) {
                // 标记为拥有动态绑定属性 本例中没有 跳过……
                el.hasBindings = true;
                // modifiers
                modifiers = parseModifiers(name);
                if (modifiers) {
                    name = name.replace(modifierRE, '');
                }
                if (bindRE.test(name)) { // v-bind
                    name = name.replace(bindRE, '');
                    value = parseFilters(value);
                    isProp = false;
                    if (modifiers) {
                        if (modifiers.prop) {
                            isProp = true;
                            name = camelize(name);
                            if (name === 'innerHtml') {
                                name = 'innerHTML';
                            }
                        }
                        if (modifiers.camel) {
                            name = camelize(name);
                        }
                        if (modifiers.sync) {
                            addHandler(
                                el,
                                ("update:" + (camelize(name))),
                                genAssignmentCode(value, "$event")
                            );
                        }
                    }
                    if (isProp || platformMustUseProp(el.tag, el.attrsMap.type, name)) {
                        addProp(el, name, value);
                    } else {
                        addAttr(el, name, value);
                    }
                } else if (onRE.test(name)) { // v-on
                    name = name.replace(onRE, '');
                    addHandler(el, name, value, modifiers, false, warn$2);
                } else { // normal directives
                    name = name.replace(dirRE, '');
                    // parse arg
                    var argMatch = name.match(argRE);
                    var arg = argMatch && argMatch[1];
                    if (arg) {
                        name = name.slice(0, -(arg.length + 1));
                    }
                    addDirective(el, name, rawName, value, arg, modifiers);
                    if ("development" !== 'production' && name === 'model') {
                        checkForAliasModel(el, value);
                    }
                }
            } else {
                {
                    var expression = parseText(value, delimiters);
                    if (expression) {
                        /* warn */
                    }
                }
                // 添加了个attrs属性
                addAttr(el, name, JSON.stringify(value));
            }
        }
    }

  到此,element的属性如图所示:,attrs和attrsList是一样的,都是一个数组,包含一个对象,值为{name:id,value:app}。

  这函数有点长。

  

 

posted @ 2017-06-07 15:06  书生小龙  阅读(1290)  评论(0编辑  收藏  举报