vue中的ast是用来抽象描述模板(DOM结构)的对象结构,我们知道DOM也是一比较复杂的树,而ast是一个简单的树,用简单的树描述复杂的树有一种四两拨千斤的感觉,也是一种化繁为简的思路。
先看一个简单的DOM真实的节点属性:
<div id="app"></div>
<script>
const dom = document.querySelector('#app');
let count = 0;
let keys = '';
for (let key in dom) {
keys += key + ',';
count++;
}
console.log('count: ', count);
console.log('key: ', keys);
</script>
执行结果:
可以看出有大量的属性,而且
DOM节点的改变可能会引起复杂的重绘或重排,因此用对象描述DOM节点就显得顺势而生,不管是ast还是Virtual DOM都是一种通过少量属性描述复杂DOM结构的方式。本文主要介绍vue中ast产生的概况。
从vue2从template到render:模板编译的入口中找到了获取render函数的真实入口是baseCompile,其中ast = parse(template.trim(), options)获取ast是第一步。
将以上代码生成
ast如下:
1、parse
/**
* Convert HTML string to AST.
*/
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// 各种变量 ...
const stack = []
let root
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
start (tag, attrs, unary, start, end) {
// ...
},
end (tag, start, end) {
// ...
},
chars (text: string, start: number, end: number) {
// ...
},
comment (text: string, start, end) {
// ...
}
})
// 各种方法 ...
return root
}
这里定义了在处理模板时需要的变量和函数,将template作为第一个参数,start、
end、chars、comment和其他变量拼接成参数对象options在parseHTML中调用。
2、parseHTML
export function parseHTML (html, options) {
const stack = []
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
let index = 0
let last, lastTag
while (html) {
last = html
// Make sure we're not in a plaintext content element like script/style
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
if (textEnd === 0) {
// Comment:
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
}
advance(commentEnd + 3)
continue
}
}
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}
// Doctype:
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
// End tag:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
}
}
// ...
function advance (n) {
index += n
html = html.substring(n)
}
function parseStartTag () {
// ...
}
function handleStartTag (match) {
// ...
}
function parseEndTag (tagName, start, end) {
// ...
}
}
这里主要通过while循环对template的Comment、Doctype、End tag和Start tag进行处理,执行过程中会调用advance (n)将索引index进行移动。
执行
advance(4)之后的结果
在移动的过程中,会对其中的
Comment、Doctype、End tag和Start tag进行处理。
3、过程处理
(1)Comment和Doctype
当遇到Comment、conditionalComment和Doctype的时候,只会让索引index向前移动。
(2)Start tag
①通过parseStartTag进行开始标签信息汇总
function parseStartTag () {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index
}
advance(start[0].length)
let 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对象,并把标签名称tagName、开始索引start、结束索引end、属性列表attrs和一元标签标示作为属性用来描述开始标签信息。
②通过handleStartTag进行标签信息处理
function handleStartTag (match) {
const tagName = match.tagName
const unarySlash = match.unarySlash
if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
const unary = isUnaryTag(tagName) || !!unarySlash
const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
const value = args[3] || args[4] || args[5] || ''
const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
attrs[i].start = args.start + args[0].match(/^\s*/).length
attrs[i].end = args.end
}
}
if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
lastTag = tagName
}
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
这里通过循环的方式重新对match中的属性attrs进行重构,循环中定义const args = match.attrs[i],然后以args[1]为name,args[3] || args[4] || args[5] || ''为value。当unary为false(即是闭合标签)的时候进行栈的管理,当前例子管理结果如下:
③通过options.start进行ast树的管理
function start (tag, attrs, unary, start, end) {
// check namespace.
// inherit parent ns if there is one
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
if (process.env.NODE_ENV !== 'production') {
if (options.outputSourceRange) {
element.start = start
element.end = end
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
cumulated[attr.name] = attr
return cumulated
}, {})
}
//...
if (!root) {
root = element
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(root)
}
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
},
这里通过createASTElement(tag, attrs, currentParent)的方式创建astElement,再为其添加属性start、end和rawAttrsMap。如果root不存在,将element作为根节点。如果!unary(即是闭合标签),将element赋值为currentParent,并进行ast树的管理:
(3)End tag
①通过parseEndTag进行结束标签的处理
function parseEndTag (tagName, start, end) {
let 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 (let i = stack.length - 1; i >= pos; i--) {
if (process.env.NODE_ENV !== 'production' &&
(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)
}
}
}
这里的例子会执行到options.end(stack[i].tag, start, end)。
②通过options.end进行ASTElement的出栈
function end (tag, start, end) {
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
element.end = end
}
closeElement(element)
}
这里通过stack.length -= 1的方式进行出栈处理,第一次弹出SPAN-ASTElement,第二次弹出DIV-ASTElement,每一次弹出的过程中都会通过currentParent = stack[stack.length - 1]保留一个父级ASTElement。
③通过closeElement管理ast树
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)) {
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(element)
}
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (process.env.NODE_ENV !== 'production') {
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.
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
}
currentParent.children.push(element)
element.parent = currentParent
}
}
// final children cleanup
// filter out scoped slots
element.children = element.children.filter(c => !(c: any).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 (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
这里通过currentParent.children.push(element)和element.parent = currentParent的方式进行父子关系的构建,是ast树由栈变成树的核心步骤。
④通标签对象的出栈
执行完options.end(stack[i].tag, start, end)之后,会通过以下方式进行标签的出栈处理
// Remove the open elements from the stack
stack.length = pos
lastTag = pos && stack[pos - 1].tag
这里的pos是当通过lastTag确定的位置,第一次执行到时lastTag为span,栈中的位置索引为1,执行完stack.length=1后,栈中只剩描述div的对象。执行完lastTag = pos && stack[1 - 1].tag以后lastTag变成了div。第二次执行时,lastTag为div使得pos为0,进而清空栈。执行流程如下:
小结
整个
parse的过程是将HTML模板处理得到ast树的过程,也是将栈变成树的过程。