compile——生成ast
生成ast
的函数是src/compiler/parser/index.js
文件中的parse
函数,
从这里入手,我们一起来看看一段html
字符串,是如何一步步转换成抽象语法树的。
这一部分会涉及到许许多多的正则匹配,知道每个正则有什么用途,会更加方便之后的分析。
( 这些正则真是高, 以前想写, 写不出来 )
正则表达式
src/compiler/parser/index.js
export const onRE = /^@|^v-on:/
export const dirRE = /^v-|^@|^:/
export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
export const forIteratorRE = /\((\{[^}]*\}|[^,]*),([^,]*)(?:,([^,]*))?\)/ const argRE = /:(.*)$/
const bindRE = /^:|^v-bind:/
const modifierRE = /\.[^.]+/g
这一部分的几个正则很简单,我们简单过一下。
onRE
是匹配@
或v-on
开头的属性,也就是我们添加事件的语法。
dirRE
是匹配v-
或@
或:
开头的属性,即vue
中的绑定数据或事件的语法。
forAliasRE
匹配v-for
中的属性值,比如item in items
、(item, index) of items
。
forIteratorRE
是对forAliasRE
中捕获的第一部分内容,进行拆解,我们都知道v-for
中in|of
前最后可以有三个逗号分隔的参数。
argRE
匹配并捕获:
开头的属性。
bindRE
匹配:
或v-bind
开头的属性,即绑定数据的语法。
modifierRE
是匹配@click.stop
、@keyup.enter
等属性中的修饰符 .stop .enter。
src/compiler/parser/html-parser.js
这部分内容的正则表达式比较复杂,相信很多人看到这些都一个头两个大。
由于我的水平也比较有限,马马虎虎带大家过一下这些正则。
前面几行是定义了匹配属性的正则。
// Regular Expressions for parsing tags and attributes
const singleAttrIdentifier = /([^\s"'<>/=]+)/
const singleAttrAssign = /(?:=)/
const singleAttrValues = [
// attr value double quotes
/"([^"]*)"+/.source,
// attr value, single quotes
/'([^']*)'+/.source,
// attr value, no quotes
/([^\s"'=<>`]+)/.source
]
const attribute = new RegExp(
'^\\s*' + singleAttrIdentifier.source +
'(?:\\s*(' + singleAttrAssign.source + ')' +
'\\s*(?:' + singleAttrValues.join('|') + '))?'
)
第一个singleAttrIdentifier
/([^\s"'<>/=]+)/
比较简单,它是匹配一个或多个非空白字符,非
"'<>/=
字符,
并捕获匹配到的内容,主要用于匹配属性名。
例如:
"/abc'de<".match(singleAttrIdentifier)
// ["abc", "abc", index: 1, input: "/abc'de<"]
第二个singleAttrAssign
,更简单,就是匹配一个=
,但不捕获。
const singleAttrValues = [
// attr value double quotes
/"([^"]*)"+/.source,
// attr value, single quotes
/'([^']*)'+/.source,
// attr value, no quotes
/([^\s"'=<>`]+)/.source
]
第三个singleAttrValues
主要用于匹配属性值,因为我们的属性值可以包含在单引号或双引号内,也可以不用引号。所以这里分为三种情况:
1、双引号括起来/"([^"]*)"+/.source
。主要是捕获双引号括起来的非"
内容。
2、单引号括起来/'([^']*)'+/.source
。主要是捕获双引号括起来的非'
内容。
3、没有引号/([^\s"'=<>
]+)/.source。捕获多个非空白字符或非
"'=<>``字符的内容。
const attribute = new RegExp(
'^\\s*' + singleAttrIdentifier.source +
'(?:\\s*(' + singleAttrAssign.source + ')' +
'\\s*(?:' + singleAttrValues.join('|') + '))?'
)
最后一个是把前三个整合起来,用于匹配一个完整的属性,并且允许属性名、等号、属性值之前可以有多个空白字符。
列举几个符合的例子:
"href='https://www.imliutao.com'".match(attribute)
// ["href='https://www.imliutao.com'", "href", "=", undefined, "https://www.imliutao.com", undefined, index: 0, input: "href='https://www.imliutao.com'"]
// 上例中把单引号和双引号互换,结果一样 // 我们在属性名、等号、属性值之前加了空格,我们依然可以正确匹配捕获键和值。
" href = 'https://www.imliutao.com'".match(attribute)
// [" href = "https://www.imliutao.com"", "href", "=", "https://www.imliutao.com", undefined, undefined, index: 0, input: " href = "https://www.imliutao.com""] // 去掉属性值的引号
' href =https://www.imliutao.com'.match(attribute)
// [" href =https://www.imliutao.com", "href", "=", undefined, undefined, "https://www.imliutao.com", index: 0, input: " href =https://www.imliutao.com"]
上面列出来的三个例子,最终匹配的结果数组中https://www.imliutao.com
的位置变换了,
这是因为我们匹配属性值时有三种情况,3-5依次为双引号、单引号、没有引号的捕获结果。
接着一组,是用来匹配起始标签的标签名。
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = '((?:' + ncname + '\\:)?' + ncname + ')'
const startTagOpen = new RegExp('^<' + qnameCapture)
ncname
匹配的是以a-zA-Z_
开头,然后是0或多个a-zA-Z_
、-
或.
。
qnameCapture
匹配ncname
开头,紧跟着一个冒号,然后又跟着一个ncname
,捕获整体匹配的内容。
startTagOpen
也很明了,其实就是匹配起始标签,我们的标签有字母、下划线、中划线或点组成,因为可能有命名空间,所以有了qnameCapture
。
例子如下:
'<a href="http://www.imliutao.com">刘涛的个人小站</a>'.match(startTagOpen)
// ["<a", "a", index: 0, input: "<a href="http://www.imliutao.com">刘涛的个人小站</a>"] '<svg:path />'.match(startTagOpen)
// ["<svg:path", "svg:path", index: 0, input: "<svg:path />"] '<svg:path.test />'.match(startTagOpen)
// ["<svg:path.test", "svg:path.test", index: 0, input: "<svg:path.test />"] '<-svg:path />'.match(startTagOpen)
// null
接着startTagClose
比较简单,用于匹配起始标签的结束部分,这里做了单标签的区分,单标签匹配的第二个元素是/
,如下:
const startTagClose = /^\s*(\/?)>/
' />'.match(startTagClose)
// [" />", "/", index: 0, input: " />"] ' >'.match(startTagClose)
// [" >", "", index: 0, input: " >"]
endTag
是匹配双标签的结束标签。以<
开始,然后是/
,然后是标签名qnameCapture
,接着是0或多个非>
,最后是>
。其中捕获是qnameCapture
进行的。
const endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>') '</div>'.match(endTag)
// ["</div>", "div", index: 0, input: "</div>"] '</div >'.match(endTag)
// ["</div >", "div", index: 0, input: "</div >"] ' </div >'.match(endTag)
// null
最后三个比较简单,doctype
是匹配文档类型,comment
匹配html
注释的起始部分,conditionalComment
匹配<![CDATA
等内容。
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!--/
const conditionalComment = /^<!\[/
-->
src/compiler/parser/text-parser.js
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g
defaultTagRE
是默认的模板分隔符匹配。
regexEscapeRE
用于匹配需要转义的字符。
src/compiler/parser/filter-parser.js
validDivisionCharRE = /[\w).+\-_$\]]/
这个也比较简单,就是匹配字母数字及列出的字符,具体用途是判断表达式是不是正则时用到,具体我们讲解filter
时深入分析。
编译模板
光看代码,毕竟无聊又生硬,我们还是从一个简单的例子出发,来过一下最基本的流程。
例子如下:
var vm = new Vue({
el: '#app',
template: '<div id="app">\
这里是文本<箭头之后的文本\
<a :href="url" target="_blank" >前面的文本{{title}}后面的文本</a>\
<img :src="img" />\
</div>',
data: {
url: 'https://www.imliutao.com',
title: '刘涛的个人小站',
img: 'https://pic1.zhimg.com/092406f3919e915fffc7ef2f2410e560_is.jpg'
}
})
</script>
我们先看一眼parse
方法的实现。
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
warn = options.warn || baseWarn platformGetTagNamespace = options.getTagNamespace || no // 获取tag的命名空间,svg或math
platformMustUseProp = options.mustUseProp || no // 判断是否需要通过绑定prop来绑定属性
platformIsPreTag = options.isPreTag || no // 是不是pre标签
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
transforms = pluckModuleFunction(options.modules, 'transformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
delimiters = options.delimiters // 分隔符 const stack = []
const preserveWhitespace = options.preserveWhitespace !== false
let root
let currentParent
let inVPre = false
let inPre = false
let warned = false function warnOnce (msg) {
if (!warned) {
warned = true
warn(msg)
}
} function endPre (element) {
// check pre state
if (element.pre) {
inVPre = false
}
if (platformIsPreTag(element.tag)) {
inPre = false
}
} parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag, // 是否是单标签
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
start (tag, attrs, unary) {
...
}, end () {
...
}, chars (text: string) {
...
}
})
return root
}
最开始保存了一系列options
传入的方法等,这些内容我们在上一篇文章中提到过,
这里就不多说了。接着定义了一些变量和内部方法。
然后是一个很长很长的函数调用parseHTML
,对整个模板的解析,都是在这里进行的。
我们来看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) {
...
} // Clean up any remaining tags
parseEndTag() function advance (n) {
...
} function parseStartTag () {
...
} function handleStartTag (match) {
...
} function parseEndTag (tagName, start, end) {
...
}
}
刚开始也是先定义一堆变量,然后开始了一个大循环。Let us go!
round one
第一次循环html
就是我们的模板,last
用于保存还没有解析的模板部分。
lastTag
为undefined
,所以!lastTag
为true
。textEnd = html.indexOf('<')
为0。
里面大量用到了advance
方法,先说一下它是干啥的。
function advance (n) {
index += n
html = html.substring(n)
}
它只是把html
从n
位置开始截取,并且记录索引index
的位置。
// 过滤掉注释,doctype等
// Comment:
if (comment.test(html)) {
const commentEnd = html.indexOf('-->') if (commentEnd >= 0) {
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
}
接着过滤文档类型标示的字符串。以上内容我们模板都没有,所以直接跳过。
const endTagMatch = html.match(endTag) if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
endTag
匹配结束的标签,这里匹配结果为null
。
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
continue
}
这里执行了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(attribute))) {
advance(attr[0].length)
match.attrs.push(attr)
}
// 是不是单标签
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
html.match(startTagOpen)
返回内容如下:
["<div", "div", index: 0, input: "<div id="app">↵ 这里是文本<箭头之后的文本↵ <a :href="url" t…t="_blank">{{title}}</a>↵ <img :src="img">↵</div>"]
此时start
是一个数组,match
就是:
match = {
tagName: "div",
attrs: [],
start: 0 // index是parseHTML内部全局变量
}
advance(start[0].length)
后html
截去前四个字符。
while
循环中主要是匹配属性知道起始标签结束,我们这里只有一个id="app"
。
end
匹配结果如下:
[">", "", index: 0, input: ">↵ 这里是文本<箭头之后的文本↵ <a :href="url" target="_blank">{{title}}</a>↵ <img :src="img">↵</div>"]
所以最终match
如下:
match = {
tagName: "div",
attrs: [[" id="app"", "id", "=", "app", undefined, undefined, index: 0, input: " id="app"> 这里是文本<箭头之后的文本 <a :href="url" target="_blank">{{title}}</a> <img :src="img"> </div>"]],
start: 0,
end: 14,
unarySlash: ""
}
之后会把match
传给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) || tagName === 'html' && lastTag === 'head' || !!unarySlash const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
// 上面讲正则的时候,提到过3、4、5分别为双引号、单引号、没有引号的捕获结果。
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
if (args[3] === '') { delete args[3] }
if (args[4] === '') { delete args[4] }
if (args[5] === '') { delete args[5] }
}
const value = args[3] || args[4] || args[5] || ''
attrs[i] = {
name: args[1],
value: decodeAttr(
value,
options.shouldDecodeNewlines
)
}
} if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
lastTag = tagName
} if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
细节大家自己看,经过循环处理:
attrs = [{name: "id", value: "app"}]
stack = [{
attrs,
lowerCasedTag: "div",
tag: "div"
}]
lastTag = "div"
我们在parse
中传入了start
函数,接着会执行start
函数,该函数比较长且涉及情况太多,我就不直接粘贴代码了,大家最好对照源码一起看,在每次解析的时候,我会附上本次解析相关代码。
整体分为以下几步:
1、 定义基本的ast结构
2、 对ast进行预处理(preTransforms
)
3、 解析v-pre
、v-if
、v-for
、v-once
、slot
、key
、ref
等指令。
4、 对ast处理(transforms
)
5、 解析v-bind
、v-on
以及普通属性
6、 根节点或v-else
块等处理
7、 模板元素父子关系的建立
8、 对ast后处理(postTransforms
)
第一步定义基本的ast结构:
const element1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: []
}
第二步对ast的预处理在weex
中才会有,我们直接跳过。
第三步对不同指令的解析,我们之后再分别讲解。
第四步中只有对class
和style
属性操作。
第五步主要是processAttrs
函数。
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
// dirRE.test('id') = false
if (dirRE.test(name)) {
...
} else {
if (process.env.NODE_ENV !== 'production') {
const expression = parseText(value, delimiters)
if (expression) {
warn(
`${name}="${value}": ` +
'Interpolation inside attributes has been removed. ' +
'Use v-bind or the colon shorthand instead. For example, ' +
'instead of <div id="{{ val }}">, use <div :id="val">.'
)
}
}
addAttr(el, name, JSON.stringify(value))
}
}
}
parseText
函数主要用于解析文本,因为不是Vue
指令才会走到else
中,这里主要是用于提示用户,不要用id="{{ val }}"
这种方式,函数的具体实现,之后解析文本的时候再说。
export function addAttr (el: ASTElement, name: string, value: string) {
(el.attrs || (el.attrs = [])).push({ name, value })
}
之后ast结构如下:
const element1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [],
plain: false,
attrs: [{name: "id", value: "'app'"}]
}
第六步root
还是undefined
,所以执行root = element
并验证根节点是否合法。
if (!root) {
root = element
checkRootConstraints(root)
} else if (!stack.length) {
...
}
第七步,
// currentParent = undefined
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else if (element.slotScope) { // scoped slot
currentParent.plain = false
const 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 {
endPre(element)
}
执行完这一步,currentParent
值为element1
,且stack
栈中有了一个元素element1
。
第八步没有任何操作。
round two
start
函数执行完之后,我们再回到parseHTML
中,接着会再次执行while
循环。
此时html
字符串的<div id="app">
部分已经被截掉,只剩下后半部分。!isPlainTextElement('div') == true
所以还是会走if
。
接着继续查找<
的位置,如下:
let textEnd = html.indexOf('<') // 20
所以会直接跳过下面的if
块。
let text, rest, next
if (textEnd >= 0) {
rest = html.slice(textEnd)
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)
advance(textEnd)
}
这里vue
其实会对文本中的小于号进行处理,如果文本中包含了<
,rest
会等于从<
开始的文本,
然后如果rest
不是结束标签、不是起始标签、不是注释,则说明它在文本中,之后跳过<
继续向后寻找,以此循环。
text
用与保存文本,这里是这里是文本<箭头之后的文本
。
if (textEnd < 0) {
text = html
html = ''
} if (options.chars && text) {
options.chars(text)
}
options.chars
用于解析文本,我们接着回到parse
函数中。
chars
函数比较简单,主要分两种情况,一种是文本需要解析,一种是纯文本,我们这里是纯文本,所以整体的ast变为:
const element1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [{
type: 3,
text: '这里是文本<箭头之后的文本'
}],
plain: false,
attrs: [{name: "id", value: "'app'"}]
}
再回到parseHTML
函数,这时html
又被截去了上面的文本,所以和last
不相等,再次执行循环。
round three
这一次的循环与第一次很类似,因为是a
标签的起始部分,所以同样会走到:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
continue
}
parseStartTag
的解析过程上面已经说过,这里返回的值为:
startTagMatch = {
tagName: "a",
attrs: [[" :href="url"", ":href", "=", "url", undefined, undefined, index: 0, input: " :href="url" target="_blank">{{title}}</a>↵ <img :src="img">↵</div>"], [" target="_blank"", "target", "=", "_blank", undefined, undefined, index: 0, input: " target="_blank">{{title}}</a>↵ <img :src="img">↵</div>"]],
start: 34,
end: 65,
unarySlash: ""
}
接着也同样交给handleStartTag
函数处理,具体过程也不再赘述,这其中,stack
和lastTag
的值改变了。
stack = [{
attrs: [{name: "id", value: "app"}]
lowerCasedTag: "div",
tag: "div"
},{
attrs: [{name: ":href", value: "url"}, {name: "target", value: "_blank"}]
lowerCasedTag: "a",
tag: "a"
}]
lastTag = "a"
start
函数也会再次执行,同样按照刚才的步骤来:
第一步定义基本的ast结构:
const element2 = {
type: 1,
tag: 'a',
attrsList: [{name: ":href", value: "url"}, {name: "target", value: "_blank"}],
attrsMap: {':href': 'url', 'target': '_blank'},
parent: element1,
children: []
};
第二步到第四步这里同样没有操作。
第五步processAttrs
函数中多了绑定属性的判断。
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
// dirRE.test(':href') = true
if (dirRE.test(name)) {
el.hasBindings = true
// 解析修饰符
modifiers = parseModifiers(name)
if (modifiers) {
name = name.replace(modifierRE, '')
}
if (bindRE.test(name)) { // v-bind
name = name.replace(bindRE, '')
value = parseFilters(value) // filter的解析,这里也没有
...
if (isProp || platformMustUseProp(el.tag, el.attrsMap.type, name)) {
addProp(el, name, value)
} else {
addAttr(el, name, value)
}
} else if (onRE.test(name)) { // v-on
...
} else { // normal directives
...
}
} else {
...
}
}
}
之后ast结构如下:
const element2 = {
type: 1,
tag: 'a',
attrsList: [{name: ":href", value: "url"}, {name: "target", value: "_blank"}],
attrsMap: {':href': 'url', 'target': '_blank'},
attrs: [{name: "href", value: "url"}, {name: "target", value: "'_blank'"}],
parent: 外层div的ast,
children: [],
hasBindings: true,
plain: false
};
第六步都不符合,直接跳过。
第七步,
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
...
} else if (element.slotScope) { // scoped slot
...
} else {
currentParent.children.push(element)
element.parent = currentParent
}
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
endPre(element)
}
执行完这一步,currentParent
、stack
以及整个模板的ast如下:
currentParent = element2
stack = [element1, element2]
element1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [{
type: 3,
text: '这里是文本<箭头之后的文本'
},
element2
],
plain: false,
attrs: [{name: "id", value: "'app'"}]
}
第八步还是没有任何操作。
round four
这一次循环和第二次循环类似,都是解析文本,然后传入options.chars
函数。与第二次不同的是,这次解析出来的文本是前面的文本{{title}}后面的文本
,它绑定了数据title
。
在chars
函数中,会走到如下流程:
if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {
children.push({
type: 2,
expression,
text
})
}
我们来看看parseText
函数:
export function parseText (
text: string,
delimiters?: [string, string]
): string | void {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
if (!tagRE.test(text)) {
return
}
const tokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index
// match = match = ["{{title}}", "title", index: 5, input: "前面的文本{{title}}后面的文本"]
while ((match = tagRE.exec(text))) {
index = match.index
// push text token
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
// tag token
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp})`)
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return tokens.join('+')
}
整个函数还是比较简单的,首先判断text
中有没有分隔符,没有就直接返回,这也是第二步中没有走到这个循环的原因。
在parseText
中的while
循环把我们的文本分为"前面的文本"
、{{title}}
、"后面的文本"
三部分,
并对绑定数据的部分通过filter
处理,包裹为_s(${exp})
,_s
是什么在vdom
里面我们再做解释,最后返回的值为"前面的文本"+_s(title)+"后面的文本"
。
到这里,我们整个模板的ast变为如下:
element1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [{
type: 3,
text: '这里是文本<箭头之后的文本'
},
{
type: 1,
tag: 'a',
attrsList: [{name: ":href", value: "url"}, {name: "target", value: "_blank"}],
attrsMap: {':href': 'url', 'target': '_blank'},
attrs: [{name: "href", value: "url"}, {name: "target", value: "'_blank'"}],
parent: element1,
children: [{
type: 2,
expression: '"前面的文本"+_s(title)+"后面的文本"',
text: '前面的文本{{title}}后面的文本'
}],
hasBindings: true,
plain: false
}
],
plain: false,
attrs: [{name: "id", value: "'app'"}]
}
round five
再一次回到parseHTML
中的while
循环,这时我们的html
还剩下</a><img :src="img"></div>
。
显然这回会走到endTagMatch
const endTagMatch = html.match(endTag)
// ["</a>", "a", index: 0, input: "</a>↵ <img :src="img">↵</div>"]
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
我们这一次走进parseEndTag
来一探究竟:
// tagName = 'a', start = 84, end = 88
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
} if (tagName) {
// 寻找最近的起始`a`标签
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
pos = 0
} if (pos >= 0) {
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.`
)
}
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') {
...
} else if (lowerCasedTagName === 'p') {
...
}
}
上面的代码,我们会走到options.end
中,我们返回parse
中,看一看这个方法干了啥。
end () {
// remove trailing whitespace
const element = stack[stack.length - 1]
const lastNode = element.children[element.children.length - 1]
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
element.children.pop()
}
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
endPre(element)
}
代码很短,看着就神清气爽,它其实就是一个简单的出栈操作,具体过程如下:
1、取出stack
中的最后一个元素。
2、取出该元素的最后一个子元素。
3、如果最后一个子元素是纯文本' '
则删除,这是因为我们的模板一般都会缩进,都会有换行,所以这里是清除换行等添加的内容。
4、stack
长度减一
5、currentParent
变为栈中最后一个元素
6、 处理v-pre
或pre
的结束标签
这时stack
和currentParent
分别变为:
stack = [element1]
currentParent = element
回到parseEndTag
中,同样也做了一个出栈的操作:
stack
长度变为pos
,lastTag
变为栈中最后一个元素的tag
,又回到了:
stack = [{
attrs: [{name: "id", value: "app"}]
lowerCasedTag: "div",
tag: "div"
}]
lastTag = "div"
round six
这一次因为我们的换行,所以会给div
添加一个空的纯文本结点,也就是end
中要去掉的' '
文本结点,模板ast变为:
element1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [{
type: 3,
text: '这里是文本<箭头之后的文本'
},
{
type: 1,
tag: 'a',
attrsList: [{name: ":href", value: "url"}, {name: "target", value: "_blank"}],
attrsMap: {':href': 'url', 'target': '_blank'},
attrs: [{name: "href", value: "url"}, {name: "target", value: "'_blank'"}],
parent: element1,
children: [{
type: 2,
expression: '"前面的文本"+_s(title)+"后面的文本"',
text: '前面的文本{{title}}后面的文本'
}],
hasBindings: true,
plain: false
},
{
text: " ",
type: 3
}
],
plain: false,
attrs: [{name: "id", value: "'app'"}]
}
round seven
这次我们又会走到startTagMatch
:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
continue
}
这一次由于我们的img
是一个单标签,所以startTagMatch
如下:
startTagMatch = {
tagName: "img",
attrs: [[" :src="img"", ":src", "=", "img", undefined, undefined, index: 0, input: " :src="img" /> </div>"]],
start: 91,
end: 109,
unarySlash: "/"
}
因为单标签不会包含其他内容,所以不会影响栈的深度。
在handleStartTag
处理时,单标签会直接执行start
函数,跳过stack
和lastTag
的修改。
在start
函数中,单标签和双标签的区别也是直接跳过stack
和currentParent
,属性的解析都相同,这时整个模板的ast又有所改变了。
element1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [{
type: 3,
text: '这里是文本<箭头之后的文本'
},
{
type: 1,
tag: 'a',
attrsList: [{name: ":href", value: "url"}, {name: "target", value: "_blank"}],
attrsMap: {':href': 'url', 'target': '_blank'},
attrs: [{name: "href", value: "url"}, {name: "target", value: "'_blank'"}],
parent: element1,
children: [{
type: 2,
expression: '"前面的文本"+_s(title)+"后面的文本"',
text: '前面的文本{{title}}后面的文本'
}],
hasBindings: true,
plain: false
},
{
text: " ",
type: 3
},
{
type: 1,
tag: 'img',
attrsList: [{name: ":src", value: "img"}],
attrsMap: {':src': 'img'},
attrs: [{name: "src", value: "url"}],
parent: element1,
children: [],
hasBindings: true,
plain: false
}
],
plain: false,
attrs: [{name: "id", value: "'app'"}]
}
round eight
这一次与第六次循环一模一样,同样添加了一个空文本子节点,模板ast变为:
element1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [{
type: 3,
text: '这里是文本<箭头之后的文本'
},
{
type: 1,
tag: 'a',
attrsList: [{name: ":href", value: "url"}, {name: "target", value: "_blank"}],
attrsMap: {':href': 'url', 'target': '_blank'},
attrs: [{name: "href", value: "url"}, {name: "target", value: "'_blank'"}],
parent: element1,
children: [{
type: 2,
expression: '"前面的文本"+_s(title)+"后面的文本"',
text: '前面的文本{{title}}后面的文本'
}],
hasBindings: true,
plain: false
},
{
text: " ",
type: 3
},
{
type: 1,
tag: 'img',
attrsList: [{name: ":src", value: "img"}],
attrsMap: {':src': 'img'},
attrs: [{name: "src", value: "url"}],
parent: element1,
children: [],
hasBindings: true,
plain: false
},
{
text: " ",
type: 3
}
],
plain: false,
attrs: [{name: "id", value: "'app'"}]
}
round nine
这一次,找到了我们整个模板的闭合标签,然后执行end
函数,最终生成的ast就是:
element1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [{
type: 3,
text: '这里是文本<箭头之后的文本'
},
{
type: 1,
tag: 'a',
attrsList: [{name: ":href", value: "url"}, {name: "target", value: "_blank"}],
attrsMap: {':href': 'url', 'target': '_blank'},
attrs: [{name: "href", value: "url"}, {name: "target", value: "'_blank'"}],
parent: element1,
children: [{
type: 2,
expression: '"前面的文本"+_s(title)+"后面的文本"',
text: '前面的文本{{title}}后面的文本'
}],
hasBindings: true,
plain: false
},
{
text: " ",
type: 3
},
{
type: 1,
tag: 'img',
attrsList: [{name: ":src", value: "img"}],
attrsMap: {':src': 'img'},
attrs: [{name: "src", value: "url"}],
parent: element1,
children: [],
hasBindings: true,
plain: false
}
],
plain: false,
attrs: [{name: "id", value: "'app'"}]
}
结语
至此,我们通过一个保护绑定属性、包含纯文本、包含文本模板、包含单标签、包含双标签的实例,
一步一步详细的分析了把html
字符串解析为ast
的过程。之后对于各种复杂的指令等,我们单独讲解。
compile——生成ast的更多相关文章
- [Vue源码]一起来学Vue模板编译原理(一)-Template生成AST
本文我们一起通过学习Vue模板编译原理(一)-Template生成AST来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vu ...
- 大白话Vue源码系列(03):生成AST
阅读目录 AST 节点定义 标签的正则匹配 解析用到的工具方法 解析开始标签 解析结束标签 解析文本 解析整块 HTML 模板 未提及的细节 本篇探讨 Vue 根据 html 模板片段构建出 AST ...
- Atitit.注解and属性解析(2)---------语法分析 生成AST attilax总结 java .net
Atitit.注解and属性解析(2)---------语法分析 生成AST attilax总结 java .net 1. 应用场景:::因为要使用ui化的注解 1 2. 使用解释器方式来实现生成 ...
- [Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串
本文我们一起通过学习Vue模板编译原理(二)-AST生成Render字符串来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学V ...
- 转换器5:参考Python源码,实现Php代码转Ast并直接运行
前两个周末写了<手写PHP转Python编译器>的词法,语法分析部分,上个周末卡文了. 访问器部分写了两次都不满意,没办法,只好停下来,参考一下Python的实现.我实现的部分正好和Pyt ...
- Clang之语法抽象语法树AST
语法分析器的任务是确定某个单词流是否能够与源语言的语法适配,即设定一个称之为上下文无关语言(context-free language)的语言集合,语法分析器建立一颗与(词法分析出的)输入单词流对应的 ...
- Python Ast介绍及应用
Abstract Syntax Trees即抽象语法树.Ast是python源码到字节码的一种中间产物,借助ast模块可以从语法树的角度分析源码结构.此外,我们不仅可以修改和执行语法树,还可以将Sou ...
- 打造自己的Vue组件文档生成工具
程序员最讨厌的两件事情,第一种是写文档,另一种是别人没有写文档.有没有直接根据vue组件生成文档的呢?当然是有的的.但第三方使用起来不一定能和现有项目结合使用,往往需要额外的注释用来标记提取信息.使用 ...
- WP8.1开发中关于媒体(图片)文件的生成操作,属性如何设置(内容/嵌入资源等);
(转载)WindowsPhone问题笔记-- 正确选择build action 解决媒体资源无法读取问题 链接:http://www.cnblogs.com/qinxg/archive/2012/07 ...
随机推荐
- HDU 6178 Monkeys(树上的二分匹配)
http://acm.hdu.edu.cn/showproblem.php?pid=6178 题意:现在有一n个顶点的树形图,还有k只猴子,每个顶点只能容纳一只猴子,而且每只猴子至少和另外一只猴子通过 ...
- CSU 1805 Three Capitals(矩阵树定理+Best定理)
http://acm.csu.edu.cn/csuoj/problemset/problem?pid=1805 题意: A和B之间有a条边,A和G之间有b条边,B和G之间有c条边.现在从A点出发走遍所 ...
- jquery 封装插件
如今做web开发,jquery 几乎是必不可少的,就连vs神器在2010版本开始将Jquery 及ui 内置web项目里了.至于使用jquery好处这里就不再赘述了,用过的都知道.今天我们来讨论下jq ...
- annovar积累
20170222 ANNOVAR简介 ANNOVAR是由王凯编写的一个注释软件,可以对SNP和indel进行注释,也可以进行变异的过滤筛选. ANNOVAR能够利用最新的数据来分析各种基因组中的遗传变 ...
- 【Django】【五】开发Web接口
[HTTP协议与JSON] 1. HTTP协议 特点如下: (1)支持客户/服务器模式. 简单快速:客户向服务器请求服务时,只需传送请求方法和路径.请求方法常用的有GET.POST.每种方法规定了客户 ...
- An error occurred during installation: No such plugin: cloudbees-folder
解决办法:下载cloudbees-folder.hpi放在目录/usr/local/tomcat/webapps/jenkins/WEB-INF/detached-plugins/下,重启tomcat ...
- SQLServer 大小写转换
SQLServer中小转大也是同样的默认快捷键,大转小换成L了 1.小写转换大写 Ctrl + shift + U 2.大写转换小写 Ctrl + shift + L
- Codeforces D - GCD of Polynomials
D - GCD of Polynomials 逆推,根据(i-2)次多项f(i-2)式和(i-1)次多项式f(i-1)推出i次多项式f(i) f(i)=f(i-1)*x+f(i-2) 样例已经给出0次 ...
- TestNG 101
最近看了点TestNG,做个入门笔记 0.Maven + TestNG 0a. 创建Maven 项目,pom中添加依赖(可能还需要安装TestNG插件 <dependencies> < ...
- 显示Unicode 字节的对应字符的小技巧
在一段smali代码里看到这样的代码 const-string v0, "\u7528\u6237\u9a8c\u8bc1\u8fc7\u671f\uff0c\u8bf7\u91cd\u65 ...