vue parseHTML函数源码解析start钩子函数
作者:李李
正文
接上章节: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' && ( !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 && (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 && (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钩子函数的资料请关注脚本之家其它相关文章!