vue parseHTML函数源码解析start钩子函数

目录
  • 正文
    • platformGetTagNamespace 源码
    • isForbiddenTag 函数
    • addIfCondition是什么
    • processIfConditions 源码

正文

接上章节:parseHTML 函数源码解析 AST 预备知识

现在我们就可以愉快的进入到Vue start钩子函数源码部分了。

start: function start(tag, attrs, unary) {
	// 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);
	}
	var element = createASTElement(tag, attrs, currentParent);
	if (ns) {
		element.ns = ns;
	}
	if (isForbiddenTag(element) && !isServerRendering()) {
		element.forbidden = true;
		warn$2(
			'Templates should only be responsible for mapping the state to the ' +
			'UI. Avoid placing tags with side-effects in your templates, such as ' +
			"<" + tag + ">" + ', as they will not be parsed.'
		);
	}
	// apply pre-transforms
	for (var i = 0; i < preTransforms.length; i++) {
		element = preTransforms[i](element, options) || element;
	}
	if (!inVPre) {
		processPre(element);
		if (element.pre) {
			inVPre = true;
		}
	}
	if (platformIsPreTag(element.tag)) {
		inPre = true;
	}
	if (inVPre) {
		processRawAttrs(element);
	} else if (!element.processed) {
		// structural directives
		processFor(element);
		processIf(element);
		processOnce(element);
		// element-scope stuff
		processElement(element, options);
	}
	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
	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 {
		closeElement(element);
	}
}

如上代码start 钩子函数接受三个参数,这三个参数分别是标签名字 tag,该标签的属性数组attrs,以及代表着该标签是否是一元标签的标识 unary。

接下来别害怕看不懂,我们一点点来分析它函数体中的代码。

var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);

开头定义了 ns 变量,它的值为标签的命名空间,如何获取当前元素的命名空间呢?首先检测currentParent 变量是否存在,我们知道 currentParent 变量为当前元素的父级元素描述对象,如果当前元素存在父级并且父级元素存在命名空间,则使用父级的命名空间作为当前元素的命名空间。

如果父级元素不存在或父级元素没有命名空间那么会调用platformGetTagNamespace函数,platformGetTagNamespace 函数只会获取 svg 和 math 这两个标签的命名空间,但这两个标签的所有子标签都会继承它们两个的命名空间。

platformGetTagNamespace 源码

function getTagNamespace(tag) {
	if (isSVG(tag)) {
		return "svg"
	}
	if (tag === "math") {
		return "math"
	}
}

接下来源码:

if (isIE && ns === "svg") {
	attrs = guardIESVGBug(attrs);
}

这里通过isIE来判断宿主环境是不是IE浏览器,并且前元素的命名空间为svg, 如果是通过guardIESVGBug处理当前元素的属性数组attrs,并使用处理后的结果重新赋值给attrs变量,该问题是svg标签中渲染多余的属性,如下svg标签:

<svg xmlns:feature="http://www.openplans.org/topp"></svg>

被渲染为:

<svg xmlns:NS1="" NS1:xmlns:feature="http://www.openplans.org/topp"></svg>

标签中多了 'xmlns:NS1="" NS1:' 这段字符串,解决办法也很简单,将整个多余的字符串去掉即可。而 guardIESVGBug 函数就是用来修改NS1:xmlns:feature属性并移除xmlns:NS1="" 属性的。

接下来源码:

var element = createASTElement(tag, attrs, currentParent);
if (ns) {
	element.ns = ns;
}

在上章节聊过,createASTElement 它将生成当前标签的元素描述对象并且赋值给 element 变量。紧接着检查当前元素是否存在命名空间 ns ,如果存在则在元素对象上添加 ns 属性,其值为命名空间的值。

接下来源码:

if (isForbiddenTag(element) && !isServerRendering()) {
	element.forbidden = true;
	warn$2(
		'Templates should only be responsible for mapping the state to the ' +
		'UI. Avoid placing tags with side-effects in your templates, such as ' +
		"<" + tag + ">" + ', as they will not be parsed.'
	);
}

这里的作用就是判断在非服务端渲染情况下,当前解析的开始标签是否是禁止在模板中使用的标签。哪些是禁止的呢?

isForbiddenTag 函数

function isForbiddenTag(el) {
	return (
		el.tag === 'style' ||
		(el.tag === 'script' &amp;&amp; (
			!el.attrsMap.type ||
			el.attrsMap.type === 'text/javascript'
		))
	)
}

可以看到,style,script 都是在禁止名单中,但通过isForbiddenTag 也发现一个彩蛋。

<script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
</script>

当定义模板的方式如上,在 <script> 元素上添加 type="text/x-template" 属性。 此时的script不会被禁止。

最后还会在当前元素的描述对象上添加 element.forbidden 属性,并将其值设置为true。

接下来源码:

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

如上代码中使用 for 循环遍历了preTransforms 数组,preTransforms 是通过pluckModuleFunction 函数从options.modules 选项中筛选出名字为preTransformNode 函数所组成的数组。实际上 preTransforms 数组中只有一个 preTransformNode 函数该函数只用来处理 input 标签我们在后面章节会来讲它。

接下来源码:

if (!inVPre) {
	processPre(element);
	if (element.pre) {
		inVPre = true;
	}
}
if (platformIsPreTag(element.tag)) {
	inPre = true;
}
if (inVPre) {
	processRawAttrs(element);
} else if (!element.processed) {
	// structural directives
	processFor(element);
	processIf(element);
	processOnce(element);
	// element-scope stuff
	processElement(element, options);
}

可以看到这里会有大量的process*的函数,这些函数是做什么用的呢?实际上process* 系列函数的作用就是对元素描述对象做进一步处理,比如其中一个函数叫做 processPre,这个函数的作用就是用来检测元素是否拥有v-pre 属性,如果有v-pre 属性则会在 element 描述对象上添加一个 pre 属性,如下:

{
  type: 1,
  tag,
  attrsList: attrs,
  attrsMap: makeAttrsMap(attrs),
  parent,
  children: [],
  pre: true
}

总结:所有process* 系列函数的作用都是为了让一个元素的描述对象更加充实,使这个对象能更加详细地描述一个元素, 不过我们本节主要总结解析一个开始标签需要做的事情,所以稍后去看这些代码的实现。

接下来源码:

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.'
			);
		}
	}
}

我们知道在编写 Vue 模板的时候会受到两种约束,首先模板必须有且仅有一个被渲染的根元素,第二不能使用 slot 标签和 template 标签作为模板的根元素。

checkRootConstraints 函数内部首先通过判断 el.tag === 'slot' || el.tag === 'template' 来判断根元素是否是slot 标签或 template 标签,如果是则打印警告信息。接着又判断当前元素是否使用了 v-for 指令,因为v-for 指令会渲染多个节点所以根元素是不允许使用 v-for 指令的。

接下来源码:

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 &amp;&amp; (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 语句先检测 root 是否存在!我们知道 root 变量在一开始是不存在的,如果 root 不存在那说明当前元素应该就是根元素,所以在 if 语句块内直接把当前元素的描述对象 element 赋值给 root 变量,同时会调用 checkRootConstraints函数检查根元素是否符合要求。

再来看 else if 语句的条件,当 stack 为空的情况下会执行 else if 语句块内的代码, 那stack 什么情况下才为空呢?前面已经多次提到每当遇到一个非一元标签时就会将该标签的描述对象放进数组,并且每当遇到一个结束标签时都会将该标签的描述对象从 stack 数组中拿掉,那也就是说在只有一个根元素的情况下,正常解析完成一段 html 代码后 stack 数组应该为空,或者换个说法,即当 stack 数组被清空后则说明整个模板字符串已经解析完毕了,但此时 start 钩子函数仍然被调用了,这说明模板中存在多个根元素,这时 else if 语句块内的代码将被执行:

接下来源码:

if (root.if &amp;&amp; (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."
	);
}

想要能看懂这个代码,你需要懂一些前置知识。

[ Vue条件渲染 ] (https://cn.vuejs.org/v2/guide/conditional.html)

我们知道在编写 Vue 模板时的约束是必须有且仅有一个被渲染的根元素,但你可以定义多个根元素,只要能够保证最终只渲染其中一个元素即可,能够达到这个目的的方式只有一种,那就是在多个根元素之间使用 v-if 或 v-else-if 或 v-else 。

示例代码:

<div v-if="type === 'A'">
  A
</div>
<div v-else-if="type === 'B'">
  B
</div>
<div v-else-if="type === 'C'">
  C
</div>
<div v-else>
  Not A/B/C
</div>

在回归到代码部分。

if (root.if && (element.elseif || element.else))

root 对象中的 .if 属性、.elseif 属性以及 .else 属性都是哪里来的,它们是在通过 processIf 函数处理元素描述对象时,如果发现元素的属性中有 v-if 或 v-else-if 或 v-else ,则会在元素描述对象上添加相应的属性作为标识。

上面代码如果第一个根元素上有 .if 的属性,而非第一个根元素 element 有 .elseif 属性或者 .else 属性,这说明根元素都是由 v-if、v-else-if、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."
	);
}

checkRootConstraints 函数检查当前元素是否符合作为根元素的要求,这都能理解。

addIfCondition是什么

看下它的源代码。

function addIfCondition(el, condition) {
	if (!el.ifConditions) {
		el.ifConditions = [];
	}
	el.ifConditions.push(condition);
}

代码很简单,调用addIfCondition 传递的参数 root 对象,在函数体中扩展一个属性addIfCondition, root.addIfCondition 属性值是一个对象。 此对象中有两个属性exp、block。实际上该函数是一个通用的函数,不仅仅用在根元素中,它用在任何由 v-if、v-else-if 以及 v-else 组成的条件渲染的模板中。

通过如上分析我们可以发现,具有 v-else-if 或 v-else 属性的元素的描述对象会被添加到具有 v-if 属性的元素描述对象的 .ifConnditions 数组中。

举个例子,如下模板:

<div v-if="A"></div>
<div v-else-if="B"></div>
<div v-else-if="C"></div>
<div v-else></div>

解析后生成的 AST 如下(简化版):

{
  type: 1,
  tag: 'div',
  ifConditions: [
    {
      exp: 'A',
      block: { type: 1, tag: 'div' /* 省略其他属性 */ }
    },
    {
      exp: 'B',
      block: { type: 1, tag: 'div' /* 省略其他属性 */ }
    },
    {
      exp: 'C',
      block: { type: 1, tag: 'div' /* 省略其他属性 */ }
    },
    {
      exp: 'undefined',
      block: { type: 1, tag: 'div' /* 省略其他属性 */ }
    }
  ]
  // 省略其他属性...
}

假如当前元素不满足条件:root.if && (element.elseif || element.else) ,那么在非生产环境下会打印了警告信息。

接下来源码:

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 {
	closeElement(element);
}

我们先从下往上讲, 为什么呢?原因是在解析根元素的时候currentParent并没有赋值。

!unary 表示解析的是非一元标签,此时把该元素的描述对象添加到stack 栈中,并且将 currentParent 变量的值更新为当前元素的描述对象。如果一个元素是一元标签,那么应该调用 closeElement 函数闭合该元素。

老生常谈的总结:每当遇到一个非一元标签都会将该元素的描述对象添加到stack数组,并且currentParent 始终存储的是 stack 栈顶的元素,即当前解析元素的父级。

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;
	}
}

这里的条件要成立,则说明当前元素存在父级( currentParent ),并且当前元素不是被禁止的元素。

常见的情况如下:

if (currentParent && !element.forbidden) {
        if (element.elseif || element.else) {
         //...
	} else if (element.slotScope) { // scoped slot
	 //...
	} else {
		currentParent.children.push(element);
		element.parent = currentParent;
	}
}

在 else 语句块内,会把当前元素描述对象添加到父级元素描述对象 ( currentParent ) 的children 数组中,同时将当前元素对象的 parent 属性指向父级元素对象,这样就建立了元素描述对象间的父子级关系。

如果一个标签使用 v-else-if 或 v-else 指令,那么该元素的描述对象实际上会被添加到对应的v-if 元素描述对象的 ifConditions 数组中,而非作为一个独立的子节点,这个工作就是由如下代码完成:

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 {
	  //...
	}
}

如当前解析的元素使用了 v-else-if 或 v-else 指令,则会调用 processIfConditions 函数,同时将当前元素描述对象 element 和父级元素的描述对象 currentParent 作为参数传递:

processIfConditions 源码

function processIfConditions(el, parent) {
	var prev = findPrevElement(parent.children);
	if (prev && prev.if) {
		addIfCondition(prev, {
			exp: el.elseif,
			block: el
		});
	} else {
		warn$2(
			"v-" + (el.elseif ? ('else-if="' + el.elseif + '"') : 'else') + " " +
			"used on element <" + (el.tag) + "> without corresponding v-if."
		);
	}
}

findPrevElement 函数是去查找到当前元素的前一个元素描述对象,并将其赋值给 prev 常量,addIfCondition 不用多说如果prev 、prev.if 存在,调用 addIfCondition 函数在当前元素描述对象添加 ifConditions 属性,传入的对象存储相关信息。

如果当前元素没有使用 v-else-if 或 v-else 指令,那么还会判断当前元素是否使用了 slot-scope 特性,如下:

if (currentParent && !element.forbidden) {
	if (element.elseif || element.else) {
          //...
	} else if (element.slotScope) { // scoped slot
		currentParent.plain = false;
		var name = element.slotTarget || '"default"';
		(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
	} else {
	  //...
	}
}

如果一个元素使用了 slot-scope 特性,那么该元素的描述对象会被添加到父级元素的scopedSlots 对象下,也就是说使用了 slot-scope 特性的元素与使用了v-else-if 或 v-else 指令的元素一样,他们都不会作为父级元素的子节点,对于使用了 slot-scope 特性的元素来讲它们将被添加到父级元素描述对象的 scopedSlots 对象下。

自 2.6.0 起有所更新。已废弃的使用slot-scope 特性的语法在这里。所以此块内容就不铺开来讲了,有兴趣的同学可以去了解下,更多关于vue parseHTML start钩子函数的资料请关注我们其它相关文章!

(0)

相关推荐

  • vue parseHTML 函数源码解析

    目录 正文 函数开头定义的一些常量和变量 while 循环 textEnd ===0 parseStartTag 函数解析开始标签 总结: 正文 接上篇: Vue编译器源码分析AST 抽象语法树 function parseHTML(html, options) { var stack = []; var expectHTML = options.expectHTML; var isUnaryTag$$1 = options.isUnaryTag || no; var canBeLeftOpen

  • vue parseHTML源码解析hars end comment钩子函数

    目录 引言 chars源码: parseText end 源码: 引言 接上文  parseHTML 函数源码解析(六) start钩子函数 接下来我们主要讲解当解析器遇到一个文本节点时会如何为文本节点创建元素描述对象,又会如何对文本节点做哪些特殊的处理. parseHTML(template, { chars: function(){ //... }, //... }) chars源码: chars: function chars(text) { if (!currentParent) { {

  • vue parseHTML函数源码解析 AST预备知识

    目录 正文 createASTElement函数 解析指令所用正则 parse 函数中的变量 正文 接上章节:parseHTML 函数源码解析AST 基本形成 在正式扎进Vue parse源码之前,我们先了解下他周边的工具函数, 这能帮我们快速的去理解阅读. 还记得我们在上章节讲的element元素节点的描述对象吗? var element = { type: 1, tag: tag, parent: null, attrsList: attrs, children: [] } 在源码中定义了一

  • vue parseHTML 函数拿到返回值后的处理源码解析

    目录 引言 parseStartTag函数返回值 handleStartTag源码 tagName 及unarySlash 调用parser钩子函数 引言 继上篇文章: parseHTML 函数源码解析 var startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); if (shouldIgnoreFirstNewline(startTagMatch.tagName, html))

  • vue parseHTML 函数源码解析AST基本形成

    目录 AST(抽象语法树)? 子节点 Vue中是如何把html(template)字符串编译解析成AST 解析html 代码重新改造 接着解析 html (template)字符串 解析div AST(抽象语法树)? 在上篇文章中我们已经把整个词法分析的解析过程分析完毕了. 例如有html(template)字符串: <div id="app"> <p>{{ message }}</p> </div> 产出如下: { attrs: [&q

  • vue parseHTML函数解析器遇到结束标签

    目录 引言 match函数匹配正则endTag 关键 parseEndTag 函数代码 总结parseEndTag 函数作用 handleStartTag函数后续 最后更新 stack 栈以及 lastTag 引言 承接上篇 parseHTML 函数源码解析拿到返回值后的处理 接下来我们将会讲解当 textEnd === 0 解析器遇到结束标签,parse 结束标签的代码如下: // End tag: var endTagMatch = html.match(endTag); if (endTa

  • vue parseHTML函数源码解析start钩子函数

    目录 正文 platformGetTagNamespace 源码 isForbiddenTag 函数 addIfCondition是什么 processIfConditions 源码 正文 接上章节:parseHTML 函数源码解析 AST 预备知识 现在我们就可以愉快的进入到Vue start钩子函数源码部分了. start: function start(tag, attrs, unary) { // check namespace. // inherit parent ns if ther

  • Django contrib auth authenticate函数源码解析

    引言 django提供了一个默认的auth系统用于用户的登录和授权,并提供了一定的扩展性,允许开发者自行定义多个验证后台,每个验证后台必须实现authenticate函数,并返回None或者User对象. 默认的后台是django.contrib.auth.backends.ModelBackend,该后台通过用户名和密码进行用户的验证,以settings.AUTH_USER_MODEL作为模型.但是在实际的开发中,相信大家都不会固定的使用用户名以及同一个model进行验证,比如,不同的角色需要

  • jQuery each函数源码分析

    jQuery.each方法用于遍历一个数组或对象,并对当前遍历的元素进行处理,在jQuery使用的频率非常大,下面就这个函数做了详细讲解: 代码 /*! * jQuery源码分析-each函数 * jQuery版本:1.4.2 * * ---------------------------------------------------------- * 函数介绍 * * each函数通过jQuery.extend函数附加到jQuery对象中: * jQuery.extend({ * each:

  • Vue 2源码解析HTMLParserOptions.start函数方法

    目录 HTMLParserOptions.start() 处理后的 input ast element HTMLParserOptions.start() 用来解析标签的开始部分(匹配到标签开始部分时调用),主要区分标签类型.解析标签指令配置与动态绑定参数等等. let root let currentParent function start(tag, attrs, unary, start, end) { const ns = (currentParent && currentPare

  • FilenameUtils.getName 函数源码分析

    目录 一.背景 二.源码分析 2.1 问题1:为什么需要 NonNul 检查 ? 2.1.1 怎么检查的? 2.1.2 为什么要做这个检查呢? 2.2 问题2: 为什么不根据当前系统类型来获取分隔符? 三.Zoom Out 3.1 代码健壮性 3.2 代码严谨性 3.3 如何写注释 四.总结 一.背景 最近用到了 org.apache.commons.io.FilenameUtils#getName 这个方法,该方法可以传入文件路径,获取文件名. 简单看了下源码,虽然并不复杂,但和自己设想略有区

  • Vue之Watcher源码解析(1)

    上一节最后再次调用了mount函数,我发现竟然跳到了7000多行的那个函数,之前我还说因为声明早了被覆盖,看来我错了! 就是这个函数: // Line-7531 Vue$3.prototype.$mount = function(el, hydrating) { el = el && inBrowser ? query(el) : undefined; return mountComponent(this, el, hydrating) }; 第一步query就不用看了,el此时是一个DO

  • Vue 2.0中生命周期与钩子函数的一些理解

    前言 在使用vue一个多礼拜后,感觉现在还停留在初级阶段,虽然知道怎么和后端做数据交互,但是对于mounted这个挂载还不是很清楚的.放大之,对vue的生命周期不甚了解.只知道简单的使用,而不知道为什么,这对后面的踩坑是相当不利的. 因为我们有时候会在几个钩子函数里做一些事情,什么时候做,在哪个函数里做,我们不清楚. 于是我开始先去搜索,发现vue2.0的生命周期没啥文章.大多是1.0的版本介绍.最后还是找到一篇不错的(会放在最后) vue生命周期简介 咱们从上图可以很明显的看出现在vue2.0

随机推荐