vue源码学习(四)模板编译之解析器

vue源码版本为2.6.11(cdn地址为: https://lib.baomitu.com/vue/2.6.11/vue.js

模板编译核心由三部分组成:

  • 解析,将类 html 模版转换为 AST 对象(Abstract Syntax Tree,抽象语法树)
  • 优化,也叫静态标记,遍历 AST 对象,标记每个节点是否为静态节点,以及标记出静态根节点
  • 代码生成器,将 AST 对象生成渲染函数字符串

AST对象描述 

AST 的全称是 Abstract Syntax Tree(抽象语法树),是源代码的抽象语法结构的树状表现形式,计算机学科中编译原理的概念。Vue 源码中借鉴 jQuery 作者 John Resig 的 HTML Parser 对模板进行解析,得到的就是 AST 代码。 

AST只是用JS中的对象来描述一个节点,一个对象代表一个节点,对象中的属性用来保存节点所需的各种数据。

以下是Vue中对AST数据的定义(代码在 https://github.com/vuejs/vue/blob/dev/flow/compiler.js中)

declare type ASTNode = ASTElement | ASTText | ASTExpression;

declare type ASTElement = {
  type: 1;
  tag: string;
  attrsList: Array<ASTAttr>;
  attrsMap: { [key: string]: any };
  rawAttrsMap: { [key: string]: ASTAttr };
  parent: ASTElement | void;
  children: Array<ASTNode>;

  start?: number;
  end?: number;

  processed?: true;

  static?: boolean;
  staticRoot?: boolean;
  staticInFor?: boolean;
  staticProcessed?: boolean;
  hasBindings?: boolean;

  text?: string;
  attrs?: Array<ASTAttr>;
  dynamicAttrs?: Array<ASTAttr>;
  props?: Array<ASTAttr>;
  plain?: boolean;
  pre?: true;
  ns?: string;

  component?: string;
  inlineTemplate?: true;
  transitionMode?: string | null;
  slotName?: ?string;
  slotTarget?: ?string;
  slotTargetDynamic?: boolean;
  slotScope?: ?string;
  scopedSlots?: { [name: string]: ASTElement };

  ref?: string;
  refInFor?: boolean;

  if?: string;
  ifProcessed?: boolean;
  elseif?: string;
  else?: true;
  ifConditions?: ASTIfConditions;

  for?: string;
  forProcessed?: boolean;
  key?: string;
  alias?: string;
  iterator1?: string;
  iterator2?: string;

  staticClass?: string;
  classBinding?: string;
  staticStyle?: string;
  styleBinding?: string;
  events?: ASTElementHandlers;
  nativeEvents?: ASTElementHandlers;

  transition?: string | true;
  transitionOnAppear?: boolean;

  model?: {
    value: string;
    callback: string;
    expression: string;
  };

  directives?: Array<ASTDirective>;

  forbidden?: true;
  once?: true;
  onceProcessed?: boolean;
  wrapData?: (code: string) => string;
  wrapListeners?: (code: string) => string;

  // 2.4 ssr optimization
  ssrOptimizability?: number;

  // weex specific
  appendAsTree?: boolean;
};

declare type ASTExpression = {
  type: 2;
  expression: string;
  text: string;
  tokens: Array<string | Object>;
  static?: boolean;
  // 2.4 ssr optimization
  ssrOptimizability?: number;
  start?: number;
  end?: number;
};

declare type ASTText = {
  type: 3;
  text: string;
  static?: boolean;
  isComment?: boolean;
  // 2.4 ssr optimization
  ssrOptimizability?: number;
  start?: number;
  end?: number;
};

 可以看到 ASTNode 有三种形式:ASTElementASTExpressionASTText

举例看下:

<div id="app">
  <h1>{{message}}</h1>
</div>

该模板转换成AST后的结构如下:

{
	tag: "div",
	type: 1,
	plain: false,
	start: 0,
	end: 44,
	parent: undefined,
	attrs: [{
		dynamic: undefined,
		end: 13,
		name: "id",
		start: 5,
		value: "app",
	}],
	attrsList: [{
		end: 13,
		name: "id",
		start: 5,
		value: "app"
	}],
	attrsMap: {
		id: "app"
	},
	rawAttrsMap: {
		id: {
			name: "id",
			value: "app",
			start: 5,
			end: 13
		}
	},
	children: [{
		tag: "h1",
		type: 1,
		type: 1,
		plain: true,
		start: 17,
		end: 37,
		parent: {...},
		attrsList: [],
		attrsMap: {},
		rawAttrsMap: {},
		children: [{
			end: 32,
			expression: "_s(message)",
			start: 21,
			text: "{{message}}",
            type: 2,
			tokens: [{
				"@binding": "message"
			}]
		}]
	}]
}

parse函数伪代码:

function parse (template, options) {
    ...
    var stack = [];
    var root;    // 最终返回出去的AST树根节点
    var currentParent; // 当前父节点
    ...
    parseHTML(template, {
        start: function start (tag, attrs, unary, start$1, end) {
            // 每当解析到标签的开始位置时,触发该函数
            ...
        },
        end: function end (tag, start, end$1) {
            // 每当解析到标签的结束位置时,触发该函数
            ...
        },
        chars: function chars (text, start, end) {
            // 每当解析到文本时,触发该函数
            ...
        },
        comment: function comment (text, start, end) {
            // 每当解析到注释时,触发该函数
            ...
        }
    });
    return root
}

以下面模板为例

<div id="app">
  <h1>{{message}}</h1>
</div>

解析上面的模版,从前向后解析,依次触发 start、start、chars、end、end 钩子函数

解析到<div>触发 start
解析到<h1>触发 start
解析到{{message}}触发chars
解析到</h1>触发end
解析到</div>触发end

各个钩子函数如何构建 AST 节点?

start 钩子函数

function createASTElement(tag, attrs, parent) {
  return {
    type: 1,
    tag: tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent: parent,
    children: [],
  };
}
parseHTML(template, {
  start: function start(tag, attrs, unary, start$1, end) {
    // ...

    var element = createASTElement(tag, attrs, currentParent);

    // ...
    
    if (!unary) {
      currentParent = element;
      stack.push(element);
    } else {
      closeElement(element);
    }
  },
});

end钩子函数

function closeElement(element) {
  // ...

  currentParent.children.push(element);
  element.parent = currentParent;
}

parseHTML(template, {
  end: function end(tag, start, end$1) {
    var element = stack[stack.length - 1];
    // pop stack
    stack.length -= 1;
    currentParent = stack[stack.length - 1];
    if (options.outputSourceRange) {
      element.end = end$1;
    }
    closeElement(element);
  },
});

chars钩子函数

parseHTML(template, {
  chars: function chars(text, start, end) {
    // ...
    if (!inVPre && text !== " " && (res = parseText(text, delimiters))) {
      child = {
        type: 2,
        expression: res.expression,
        tokens: res.tokens,
        text: text,
      };
    } else if (
      text !== " " ||
      !children.length ||
      children[children.length - 1].text !== " "
    ) {
      child = {
        type: 3,
        text: text,
      };
    }
    // ...
  },
});

 comment钩子函数

parseHTML(template, {
  comment: function comment(text, start, end) {
    // ...
    var child = {
      type: 3,
      text: text,
      isComment: true,
    };
    // ...
  },
});

上面构建出来的节点是独立的,我们需要一套逻辑把这些节点连起来,构成一个真正的 AST。

如何构建 AST 层级关系

解析 html 的时候,我们需要维护一个栈 (stack),用 stack 来记录层级关系,也可以理解为 DOM 的深度。

每当遇到开始标签,触发 start 钩子函数;每当遇到结束标签,触发 end 钩子函数。

基于以上情况,我们在触发 start 钩子函数时,将当前构建的节点推入 stack 中;触发 end 钩子函数时,从 stack 中弹出一个节点。

这样就可以保证每当触发 start 钩子函数时,stack 的最后一个节点就是当前正在构建的节点的父节点。

以下面模板为例

<div id="app">
  <h1>{{message}}</h1>
</div>

解析时具体细节

解析时的模板解析到解析后的stack解析后的AST解析后
<div id="app">\n  <h1>{{message}}</h1>\n</div><div id="app"> ["div"]{tag: "div", attrs: [ {name: "id", value: "app", start: 5, end: 13}
]}
模版中 <div id="app"> 被截取掉
\n <h1>{{message}}</h1>\n</div> 空格["div"]{tag: "div", attrs: [ {name: "id", value: "app", start: 5, end: 13}
]}
空格被截取掉
<h1>{{message}}</h1>\n</div><h1>["div", "h1"]

{tag: "div", attrs: [ {name: "id", value: "app", start: 5, end: 13}
], children: [{

        tag: 'h1', ...

}]}

<h1>标签被截取掉
{{message}}</h1>\n</div>{{message}}["div", "h1"]

{tag: "div", attrs: [ {name: "id", value: "app", start: 5, end: 13}
], children: [{

        tag: 'h1', ...,

        children: [{
            expression: "_s(message)",
            text: "{{message}}",
            type: 2,
            tokens: [{
                "@binding": "message"
            }]
        }]

}]}

{{message}}被截取掉
</h1>\n</div></h1>["div"]

同上

</h1>被截取掉
\n</div>空格["div"]同上空格被截取掉
</div></div>[]同上</div>被截取掉
-html 模版为空,解析完成[]同上-

在看parseHTML方法之前先熟悉几个正则表达式:

获取属性的正则

// 获取属性的正则
var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

解读如下:

  •  ([^\s"'<>\/=]+) 可以匹配属性的key部分,如匹配 id="app"中的id
  •  (?:\s*(=)\s*) 是一个非捕获性分组,用来匹配属性中的 = 号 当然匹配前后可能存在的空格号
  •  (?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+))  是一个非捕获性分组,用来匹配属性的value部分, 属性分为三种情况,有 id="app",  id='app' , id=app 这三种情况

举例说明:

var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

var str = 'id="app"',  str1 = "id='app'", str2 = "id=app";

console.log(str.match(attribute));
// ["id=\"app\"", "id", "=", "app", undefined, undefined, index: 0, input: "id=\"app\"", groups: undefined]
console.log(str1.match(attribute));
// ["id='app'", "id", "=", undefined, "app", undefined, index: 0, input: "id='app'", groups: undefined]
console.log(str2.match(attribute));
// ["id=app", "id", "=", undefined, undefined, "app", index: 0, input: "id=app", groups: undefined]

获取动态属性的正则

var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

获取动态属性的正则表达式只比获取普通属性的正则表达式多了 这一部分 

  •   匹配包含v-、@、:、# 等 Vue 专有的动态属性 

其他跟上面一样

举例说明:

var attribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

var str = 'v-bind:[attributename]="url"',  str1 = "v-bind:[attributename]='url'", str2 = "v-bind:[attributename]=url";

console.log(str.match(attribute));
// ["v-bind:[attributename]=\"url\"", "v-bind:[attributename]", "=", "url", undefined, undefined, index: 0, input: "v-bind:[attributename]=\"url\"", groups: undefined]
console.log(str1.match(attribute));
// ["v-bind:[attributename]='url'", "v-bind:[attributename]", "=", undefined, "url", undefined, index: 0, input: "v-bind:[attributename]='url'", groups: undefined]
console.log(str2.match(attribute));
// ["v-bind:[attributename]=url", "v-bind:[attributename]", "=", undefined, undefined, "url", index: 0, input: "v-bind:[attributename]=url", groups: undefined]

处理开始/结束标签的正则

// 用于解析html标记、组件名称和属性的unicode合法字符
var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
// 合法字符的集合
var ncname = "[a-zA-Z_][\\-\\.0-9_a-zA-Z" + (unicodeRegExp.source) + "]*";
// 表示匹配 xxx:xxx 或 xxx 模式的字符
var qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
// 匹配标签开始部分,即:<xxx:xxx 或 <xxx 的模式, 如匹配<div或<xml:xxx或<my-component
var startTagOpen = new RegExp(("^<" + qnameCapture));
// 对开始标签的结束进行匹配,可判断是否是自闭合标签
var startTagClose = /^\s*(\/?)>/;
// 匹配标签结束部分, 如</div>或</xml:xxx>或</my-component>
var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));

 startTagOpen正则可视化:

处理页面开始的 doctype 声明标签正则 

var doctype = /^<!DOCTYPE [^>]+>/i;

 处理注释部分正则 

var comment = /^<!\--/;

 处理条件注释部分正则 (出现在要对 ie 浏览器进行版本渲染的情况)

// 匹配条件注释 如<!--[if !IE]>-->我是注释<!--<![endif]-->
var conditionalComment = /^<!\[/;

Vue 通过上面几个正则表达式去匹配开始结束标签、标签名、属性等等。

下面是parseHTML函数的内部实现:

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) {
    // 保留 html 副本
    last = html;
    // 如果没有lastTag,并确保我们不是在一个纯文本内容元素中:script、style、textarea
    if (!lastTag || !isPlainTextElement(lastTag)) {
      var textEnd = html.indexOf("<");
      if (textEnd === 0) {
        // Comment:
        if (comment.test(html)) {
          // ...
        }
        // conditionalComment
        if (conditionalComment.test(html)) {
          // ...
        }

        // Doctype:
        var doctypeMatch = html.match(doctype);
        if (doctypeMatch) {
          // ...
        }

        // End tag:
        var endTagMatch = html.match(endTag);
        if (endTagMatch) {
          // ...
        }

        // Start tag:
        var startTagMatch = parseStartTag();
        if (startTagMatch) {
          // ...
        }
      }

      var text = void 0,
        rest = void 0,
        next = void 0;
      if (textEnd >= 0) {
        // ...
      }

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

      if (text) {
        advance(text.length);
      }

      if (options.chars && text) {
        options.chars(text, index - text.length, index);
      }
    } else {
      // ...
      parseEndTag(stackedTag, index - endTagLength, index);
    }

    if (html === last) {
      options.chars && options.chars(html);
      // ...
      break;
    }
  }
  // ...
}

可以看出上面代码大概意思:

1. 循环html模板

2. 正则判断html字符串是开始标签、结束标签或文本标签,并分别进行处理

还是以下面模板来解析

<div id="app">
  <h1>{{message}}</h1>
</div>

解析开始标签

/**
 * 匹配到元素的名字和属性,保存到match对象中并返回
 * @return {tagName: string, attrs: Array, start: number}
 */
function parseStartTag() {
  // 判断html中是否存在开始标签
  var start = html.match(startTagOpen);
  if (start) {
    var match = {
      tagName: start[1], // 标签名
      attrs: [], // 属性集合
      start: index, // 标签的开始位置
    };
    // 截取掉匹配到的字符串
    advance(start[0].length);
    var end, attr;
    // 判断不是开始标签结尾并且存在属性
    while (
      !(end = html.match(startTagClose)) &&
      (attr = html.match(dynamicArgAttribute) || html.match(attribute))
    ) {
      attr.start = index;
      advance(attr[0].length);
      attr.end = index;
      match.attrs.push(attr);
    }
    // 如果解析到结尾,要判断该标签是否是自闭合标签
    if (end) {
      match.unarySlash = end[1];
      advance(end[0].length);
      match.end = index;
      return match;
    }
  }
}

解析完开始标签后,我们得到了一个起始标签match的数据结构

/**
 * 处理解析后的属性,重新分割并保存到attrs数组中
 * @param match
 */
function handleStartTag(match) {
  var tagName = match.tagName;
  var unarySlash = match.unarySlash;

  if (expectHTML) {
    if (lastTag === "p" && isNonPhrasingTag(tagName)) {
      parseEndTag(lastTag);
    }
    if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {
      parseEndTag(tagName);
    }
  }

  var unary = isUnaryTag$$1(tagName) || !!unarySlash;

  var l = match.attrs.length;
  var attrs = new Array(l);
  for (var i = 0; i < l; i++) {
    var args = match.attrs[i];
    var value = args[3] || args[4] || args[5] || ""; // 属性值
    var shouldDecodeNewlines =
      tagName === "a" && args[1] === "href"
        ? options.shouldDecodeNewlinesForHref
        : options.shouldDecodeNewlines;
    // 改变attr的格式为 [{name: 'id', value: 'app'}]
    attrs[i] = {
      name: args[1],
      value: decodeAttr(value, shouldDecodeNewlines),
    };
    if (options.outputSourceRange) {
      attrs[i].start = args.start + args[0].match(/^\s*/).length;
      attrs[i].end = args.end;
    }
  }
  // 如果不是自闭合标签
  // stack为了存放标签名 为了和结束标签进行匹配的作用
  if (!unary) {
    stack.push({
      tag: tagName,
      lowerCasedTag: tagName.toLowerCase(),
      attrs: attrs,
      start: match.start,
      end: match.end,
    });
    lastTag = tagName;
  }

  if (options.start) {
    // 触发start钩子函数
    options.start(tagName, attrs, unary, match.start, match.end);
  }
}

start钩子函数

/**
 * 这个和end相对应,主要处理开始标签和标签的属性(内置和普通属性),
 * @param tag 标签名
 * @param attrs 元素属性
 * @param unary 该元素是否自闭合标签, 如img
 */
start(tag, attrs, unary, start$1, end) {
    // check namespace.
    // inherit parent ns if there is one
    var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);

    // handle IE svg bug
    /* istanbul ignore if */
    if (isIE && ns === 'svg') {
        attrs = guardIESVGBug(attrs);
    }
    // 创建基础的 ASTElement
    var element = createASTElement(tag, attrs, currentParent);
    if (ns) {
        element.ns = ns;
    }

    // ...

    // apply pre-transforms
    for (var i = 0; i < preTransforms.length; i++) {
        element = preTransforms[i](element, options) || element;
    }

    if (!inVPre) {
        // 判断有没有 v-pre 指令的元素。如果有的话 element.pre = true
        // 官网有介绍:<span v-pre>{{ this will not be compiled }}</span>
        // 跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
        processPre(element);
        if (element.pre) {
            inVPre = true;
        }
    }
    if (platformIsPreTag(element.tag)) {
        inPre = true;
    }
    if (inVPre) {
        // 处理原始属性
        processRawAttrs(element);
    } else if (!element.processed) {
        // structural directives
        // v-for v-if v-once
        processFor(element);
        processIf(element);
        processOnce(element);
    }

    if (!root) {
        root = element;
        {
            //  检查根节点约束
            checkRootConstraints(root);
        }
    }

    if (!unary) {
        currentParent = element;
        stack.push(element);
    } else {
        closeElement(element);
    }
}


function closeElement(element) {
    trimEndingWhitespace(element);
    if (!inVPre && !element.processed) {
        element = processElement(element, options);
    }
    // tree management
    if (!stack.length && element !== root) {
        // 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.",
                { start: element.start }
            );
        }
    }
    if (currentParent && !element.forbidden) {
        if (element.elseif || element.else) {
            processIfConditions(element, currentParent);
        } else {
            if (element.slotScope) {
                // scoped slot
                // keep it in the children list so that v-else(-if) conditions can
                // find it as the prev node.
                var name = element.slotTarget || '"default"'
                    ; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
            }
            // 将元素插入 children 数组中
            currentParent.children.push(element);
            element.parent = currentParent;
        }
    }

    // final children cleanup
    // filter out scoped slots
    element.children = element.children.filter(function (c) { return !(c).slotScope; });
    // remove trailing whitespace node again
    trimEndingWhitespace(element);

    // check pre state
    if (element.pre) {
        inVPre = false;
    }
    if (platformIsPreTag(element.tag)) {
        inPre = false;
    }
    // apply post-transforms
    for (var i = 0; i < postTransforms.length; i++) {
        postTransforms[i](element, options);
    }
}

其实start方法就是处理 element 元素的过程。确定命名空间;创建AST元素 element;执行预处理;定义root;处理各类 v- 标签的逻辑;最后更新 root、currentParent、stack 的结果。

解析结束标签

// End tag:
var endTagMatch = html.match(endTag);
if (endTagMatch) {
    var curIndex = index;
    advance(endTagMatch[0].length);
    parseEndTag(endTagMatch[1], curIndex, index);
    continue
}


/**
 * 解析关闭标签,
 * 查找我们之前保存到stack栈中的元素,
 * 如果找到了,也就代表这个标签的开始和结束都已经找到了,此时stack中保存的也就需要删除(pop)了
 * 并且缓存最近的标签lastTag
 * @param tagName
 * @param start
 * @param end
 */
function parseEndTag(tagName, start, end) {
    var pos, lowerCasedTagName;
    if (start == null) { start = index; }
    if (end == null) { end = index; }

    // Find the closest opened tag of the same type
    if (tagName) {
        lowerCasedTagName = tagName.toLowerCase();
        for (pos = stack.length - 1; pos >= 0; pos--) {
            if (stack[pos].lowerCasedTag === lowerCasedTagName) {
                break
            }
        }
    } else {
        // If no tag name is provided, clean shop
        pos = 0;
    }

    if (pos >= 0) {
        // Close all the open elements, up the stack
        for (var i = stack.length - 1; i >= pos; i--) {
            if (i > pos || !tagName &&
                options.warn
            ) {
                options.warn(
                    ("tag <" + (stack[i].tag) + "> has no matching end tag."),
                    { start: stack[i].start, end: stack[i].end }
                );
            }
            if (options.end) {
                options.end(stack[i].tag, start, end);
            }
        }

        // Remove the open elements from the stack
        stack.length = pos;
        lastTag = pos && stack[pos - 1].tag;
    } else if (lowerCasedTagName === 'br') {
        if (options.start) {
            options.start(tagName, [], true, start, end);
        }
    } else if (lowerCasedTagName === 'p') {
        if (options.start) {
            options.start(tagName, [], false, start, end);
        }
        if (options.end) {
            options.end(tagName, start, end);
        }
    }
}

end钩子函数

end(tag, start, end$1) {
    var element = stack[stack.length - 1];
    // pop stack
    stack.length -= 1;
    currentParent = stack[stack.length - 1];
    if (options.outputSourceRange) {
        element.end = end$1;
    }
    closeElement(element);
}

解析注释

// Comment:
if (comment.test(html)) {
    var commentEnd = html.indexOf('-->');

    if (commentEnd >= 0) {
        if (options.shouldKeepComment) {
            options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);
        }
        advance(commentEnd + 3);
        continue
    }
}

// comment钩子函数
comment(text, start, end) {
    // adding anyting as a sibling to the root node is forbidden
    // comments should still be allowed, but ignored
    if (currentParent) {
        var child = {
            type: 3,
            text: text,
            isComment: true
        };
        if (options.outputSourceRange) {
            child.start = start;
            child.end = end;
        }
        currentParent.children.push(child);
    }
}

解析条件注释

// 条件注释会被直接截取掉
if (conditionalComment.test(html)) {
    var conditionalEnd = html.indexOf(']>');

    if (conditionalEnd >= 0) {
        advance(conditionalEnd + 2);
        continue
    }
}

 解析DOCTYPE

// Doctype:
var doctypeMatch = html.match(doctype);
if (doctypeMatch) {
  advance(doctypeMatch[0].length);
  continue
}

解析文本 

解析文本分为两种情况:

        1. 父元素为正常元素的处理逻辑

        2. 父元素为script、style、textarea的处理逻辑

先来看第一种:

处理父元素为正常元素的文本

var textEnd = html.indexOf('<');
// ...
var text = (void 0), rest = (void 0), next = (void 0);
if (textEnd >= 0) {
    rest = html.slice(textEnd);
    // 1 < 2</div>, 包含了 < 符号的处理 
    while (
        !endTag.test(rest) &&
        !startTagOpen.test(rest) &&
        !comment.test(rest) &&
        !conditionalComment.test(rest)
    ) {
        // < in plain text, be forgiving and treat it as text
        next = rest.indexOf('<', 1);
        if (next < 0) { break }
        textEnd += next;
        rest = html.slice(textEnd);
    }
    text = html.substring(0, textEnd);
}
// 没有,则整个都是文本
if (textEnd < 0) {
    text = html;
}

if (text) {
    advance(text.length);
}
// 调用 chars 钩子
if (options.chars && text) {
    options.chars(text, index - text.length, index);
}

char钩子函数 

chars(text, start, end) {
    // 如果是文本,没有父节点,直接返回
    if (!currentParent) {
        return
    }
    // IE textarea placeholder bug
    /* istanbul ignore if */
    if (isIE &&
        currentParent.tag === 'textarea' &&
        currentParent.attrsMap.placeholder === text
    ) {
        return
    }
    var children = currentParent.children;
    if (inPre || text.trim()) {
        text = isTextTag(currentParent) ? text : decodeHTMLCached(text);
    } else if (!children.length) {
        // remove the whitespace-only node right after an opening tag
        text = '';
    } else if (whitespaceOption) {
        if (whitespaceOption === 'condense') {
            // in condense mode, remove the whitespace node if it contains
            // line break, otherwise condense to a single space
            text = lineBreakRE.test(text) ? '' : ' ';
        } else {
            text = ' ';
        }
    } else {
        text = preserveWhitespace ? ' ' : '';
    }
    if (text) {
        if (!inPre && whitespaceOption === 'condense') {
            // condense consecutive whitespaces into single space
            text = text.replace(whitespaceRE$1, ' ');
        }
        var res;
        var child;
        // 解析文本,处理{{xxx}} 这种形式的文本
        if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
            child = {
                type: 2,
                expression: res.expression,
                tokens: res.tokens,
                text: text
            };
        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
            child = {
                type: 3,
                text: text
            };
        }
        if (child) {
            if (options.outputSourceRange) {
                child.start = start;
                child.end = end;
            }
            children.push(child);
        }
    }
}

parseText方法 

function parseText(text, delimiters) {
    var tagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
    if (!tagRE.test(text)) {
        return
    }
    var tokens = [];
    var rawTokens = [];
    var lastIndex = tagRE.lastIndex = 0;
    var match, index, tokenValue;
    // exec中不管是不是全局的匹配,只要没有子表达式,
    // 其返回的都只有一个元素,如果是全局匹配,可以利用lastIndex进行下一个匹配,
    // 匹配成功后lastIndex的值将会变为上次匹配的字符的最后一个位置的索引。
    // 在设置g属性后,虽然匹配结果不受g的影响,
    // 返回结果仍然是一个数组(第一个值是第一个匹配到的字符串,以后的为分组匹配内容),
    // 但是会改变index和 lastIndex等的值,将该对象的匹配的开始位置设置到紧接这匹配子串的字符位置,
    // 当第二次调用exec时,将从lastIndex所指示的字符位置 开始检索。
    while ((match = tagRE.exec(text))) {
        index = match.index;
        // 当文本标签中既有{{}} 在其左边又有普通文本时,
        // 如:<span>我是普通文本{{value}}</span>, 就会执行下面的方法,添加到tokens数组中。
        if (index > lastIndex) {
            rawTokens.push(tokenValue = text.slice(lastIndex, index));
            tokens.push(JSON.stringify(tokenValue));
        }
        // 把匹配到{{}}中的tag 添加到tokens数组中
        var exp = parseFilters(match[1].trim());
        tokens.push(("_s(" + exp + ")"));
        rawTokens.push({ '@binding': exp });
        lastIndex = index + match[0].length;
    }
    // 当文本标签中既有{{}} 在其右边又有普通文本时,
    // 如:<span>{{value}} 我是普通文本</span>, 就会执行下面的方法,添加到tokens数组中。
    if (lastIndex < text.length) {
        rawTokens.push(tokenValue = text.slice(lastIndex));
        tokens.push(JSON.stringify(tokenValue));
    }
    return {
        expression: tokens.join('+'),
        tokens: rawTokens
    }
}

 处理纯文本内容元素

while (html) {
    if (!lastTag || !isPlainTextElement(lastTag)) {
        // 父元素为正常元素的处理逻辑
    } else {
        // 父元素为script、style、textarea的处理逻辑
        var endTagLength = 0;
        var stackedTag = lastTag.toLowerCase();
        // 正则表达式可以匹配结束标签前包括结束标签自身在内的所有文本
        var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
        var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
            endTagLength = endTag.length;
            if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
                text = text
                    .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
                    .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
            }
            if (shouldIgnoreFirstNewline(stackedTag, text)) {
                text = text.slice(1);
            }
            if (options.chars) {
                options.chars(text);
            }
            // 最后,返回了一个空字符串最后,返回了一个空字符串
            // 将匹配到的内容都截掉了。注意,这里的截掉会将内容和结束标签一起截取掉
            return ''
        });
        index += html.length - rest$1.length;
        html = rest$1;
        parseEndTag(stackedTag, index - endTagLength, index);
    }
}

如下面模板

<div id="app">
    <script>console.log(1)</script>
</div>

 当解析到script中的内容时,模板是下面的样子:

console.log(1)</script>
</div>

此时父元素为script,所以会进入到else中的逻辑进行处理。在其处理过程中,会触发钩子函数chars和end。

钩子函数chars的参数为script中的所有内容:

chars('console.log(1)')

总结

解析器的作用是通过模板得到AST(抽象语法树)。

生成AST的过程需要借助HTML解析器,当HTML解析器触发不同的钩子函数时,我们可以构建出不同的节点。

随后,我们可以通过栈来得到当前正在构建的节点的父节点,然后将构建出的节点添加到父节点的下面。

最终,当HTML解析器运行完毕后,我们就可以得到一个完整的带DOM层级关系的AST。

HTML解析器的内部原理是一小段一小段地截取模板字符串,每截取一小段字符串,就会根据截取出来的字符串类型触发不同的钩子函数,直到模板字符串截空停止运行。

文本分两种类型,不带变量的纯文本和带变量的文本,后者需要使用文本解析器进行二次加工。

参考资料:

深入浅出Vue.js

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值