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
树的过程,也是将栈变成树的过程。