看vue源码已经有一段时间了,可是不少东西都是看的时候当时记住了,过段时间不去使用或者回顾又忘记了,因此如今开始以博客的方式写一下本身的理解,也方便后续本身的回顾。html
这一篇主要讲的是Vue源码解析parse的相关内容,主要分红三大块vue
首先让咱们简单的了解一下parser,简单来讲就是把源代码转换为目标代码的工具。引用下基维百科对于parser的解释。node
语法分析器(parser)一般是做为编译器或解释器的组件出现的,它的做用是进行语法检查、并构建由输入的单词组成的数据结构(通常是语法分析树、抽象语法树等层次化的数据结构)。语法分析器一般使用一个独立的词法分析器从输入字符流中分离出一个个的“单词”,并将单词流做为其输入。express
vue其实也是使用解析器来对模板代码进行解析。数组
/*!
* HTML Parser By John Resig (ejohn.org)
* Modified by Juriy "kangax" Zaytsev
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*/
复制代码
它的源代码上有这样一段注释,是Vue fork
自 John Resig
所写的一个开源项目:erik.eae.net/simplehtmlp… 而后在这个解析器上面作了一些扩展。bash
大概了解了解析器后让咱们来看看parse的总体流程,这边经过精简后的代码能够看到parse实际上就是先处理了一些传入的options,而后执行了parseHTML
函数,传入了template
,options
和相关钩子
。数据结构
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// 处理传入的options合并的实例vm的options上
dealOptions(options)
// 模板和相关的配置和钩子传入到parseHTML中
parseHTML(template, {
someOptions,
start (tag, attrs, unary, start) {...},
end (tag, start, end) {...},
chars (text: string, start: number, end: number) {...},
comment (text: string, start, end) {}...,
})
}
复制代码
这边咱们继续看parseHTML
函数里面作了什么?app
parseHTML
是定义在src/compiler/parser/html-parser.js
这个文件中,首先文件头部是定义了一些后续须要使用的正则,不太懂正则的能够先看看正则先关的知识。dom
// Regular Expressions for parsing tags and attributes
// 匹配标签的属性
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeLetters}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
// #7298: escape - to avoid being pased as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/
复制代码
而后先看看parseHTML
的伪代码,大概流程就是先定义须要的变量,而后循环遍历template,经过正则匹配到对应的标签后,经过进行处理经过传入的钩子函数把处理后的对象转换成ast。ide
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) {
if (!lastTag || !isPlainTextElement(lastTag)){
let textEnd = html.indexOf('<')
if (textEnd === 0) {
if(matchComment) {
advance(commentLength)
continue
}
if(matchDoctype) {
advance(doctypeLength)
continue
}
if(matchEndTag) {
advance(endTagLength)
parseEndTag()
continue
}
if(matchStartTag) {
parseStartTag()
handleStartTag()
continue
}
}
handleText()
advance(textLength)
} else {
handlePlainTextElement()
parseEndTag()
}
}
}
复制代码
让咱们分别看看parseHTML
中的四个辅助函数他们的实现。
// 为计数index加上n,同时,使html到n个字符之后到位置做为起始位
function advance (n) {
index += n
html = html.substring(n)
}
复制代码
// 解析开始标签
function parseStartTag () {
//正则匹配获取HTML开始标签
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
}
}
}
复制代码
// 处理开始标签,将开始标签中的属性提取出来。
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]
// hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
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] || ''
const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
}
// 将标签及其属性推如堆栈中
if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
lastTag = tagName
}
// 触发 options.start 方法。
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
复制代码
// 解析结束TAG
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
}
// 找到同类的开始 TAG 在堆栈中的位置
if (tagName) {
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
}
// 对堆栈中的大于等于 pos 的开始标签使用 options.end 方法。
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.`
)
}
if (options.end) {
options.end(stack[i].tag, start, end)
}
}
// Remove the open elements from the stack
// 从栈中移除元素,并标记为 lastTag
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)
}
}
}
复制代码
这边详细讲解html解析,下面简单的template模板就是做为此次详解的例子,有可能不能覆盖所有场景。
<div id="app">
<!-- 注释 -->
<div v-if="show" class="message">{{message}}</div>
</div>
复制代码
上面这一段是做为字符串来处理,首先咱们的开头是<app
,全部这个是会走到parseStartTag
这个函数中
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(lastTag, html)) {
advance(1)
}
continue
}
}
复制代码
返回值就是一个这样的简单匹配出来的js对象,而后再经过handleStartTag
这个函数把这里面一些无用的和须要添加的处理的数据处理后,执行最开始的start
钩子函数,这个在后面的ast生成中描述。
{
attrs: [
{
0: " id="app"",
1: "id",
2: "=",
3: "app",
4: undefined,
5: undefined,
end: 13,
groups: undefined,
index: 0,
input: " id="app">↵ <!-- 注释 -->↵ <div v-if="show" class="message">{{message}}</div>↵ </div>",
start: 4,
}
],
end: 14,
start: 0,
tagName: "div",
unarySlash: "",
}
复制代码
由于有advanceh函数,如今咱们的代码已经变成下面这样了,而后继续循环。
<!-- 注释 -->
<div v-if="show" class="message">{{message}}</div>
</div>
复制代码
如今就是走到了注释节点这一块,匹配到后执行开始传入的comment
钩子函数建立注释节点ast
// 注释匹配
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
// 若是须要保留注释,执行 option.comment 方法
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
advance(commentEnd + 3)
continue
}
}
复制代码
这个时候咱们的代码已经变成了下面这个样子,有没有观察到一个细节,就是注释虽然没有了,可是注释距离下一个<div
这个中间仍是有留白的。
<div v-if="show" class="message">{{message}}</div>
</div>
复制代码
这样的话html.indexOf('<')
这个匹配出来的就不是0,会走到else逻辑,而后做为文本节点给处理掉。 继续走一次处理开始标签<div v-if="show" class="message">
,文本标签`{{message}}后咱们的template变成了
</div>
</div>
复制代码
这个时候就会经过结束标签的判断走到parseEndTag
函数,调用开始传入的end
钩子函数。
// End tag: 结束标签
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
// 解析结束标签
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
复制代码
这样咱们的template已经被所有解析完成了,可能还有一些别的匹配没有在这个例子中,可是思路都是同样的,就是正则匹配到后解析成js对象而后交给传入的钩子函数生成ast,下面是循环处理html
的详解。
while (html) {
last = html
// 若是没有lastTag,且不是纯文本内容元素中:script、style、textarea
if (!lastTag || !isPlainTextElement(lastTag)) {
// 文本结束,经过<查找。
let textEnd = html.indexOf('<')
// 文本结束位置在第一个字符,即第一个标签为<
if (textEnd === 0) {
// 注释匹配
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
// 若是须要保留注释,执行 option.comment 方法
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
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(lastTag, html)) {
advance(1)
}
continue
}
}
// < 标签位置大于等于0,即标签中有内容
let text, rest, next
if (textEnd >= 0) {
// 截取从 0 - textEnd 的字符串
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)
}
if (textEnd < 0) {
text = html
html = ''
}
// 绘制文本内容,使用 options.char 方法。
if (options.chars && text) {
options.chars(text)
}
} else {
// 若是lastTag 为 script、style、textarea
let endTagLength = 0
const stackedTag = lastTag.toLowerCase()
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
const rest = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(/<!\--([\s\S]*?)-->/g, '$1') // <!--xxx-->
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1') //<!CDATAxxx>
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1)
}
// 处理文本内容,并使用 options.char 方法。
if (options.chars) {
options.chars(text)
}
return ''
})
index += html.length - rest.length
html = rest
// 解析结束tag
parseEndTag(stackedTag, index - endTagLength, index)
}
// html文本到最后
if (html === last) {
// 执行 options.chars
options.chars && options.chars(html)
if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
options.warn(`Mal-formatted tag at end of template: "${html}"`)
}
break
}
}
复制代码
这个地方附上个流程示意图。
到这个地方其实已经把整个html解析完成了,后面咱们就开始说传入的那几个钩子函数是怎么把咱们解析生成的js对象变成ast的。
当html解析完成后就须要进行词法分析,把处理好的js对象变成一个ast。咱们首先来看看createASTElement
这个函数,看名字就是一个建立ast元素,在vue里面ast其实就一个有特定格式的js对象。
export function createASTElement (
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1, // 节点类型 type = 1 为dom节点
tag, // 节点标签
attrsList: attrs, // 节点属性
attrsMap: makeAttrsMap(attrs), // 节点映射
parent, // 父节点
children: [] // 子节点
}
}
复制代码
start
这个钩子函数就是慢慢地给这个AST进行装饰,添加更多的属性和标志,如今让咱们具体看看这个函数,首先接着上面的例子,start如今接收的四个参数分别为
tag: div (元素的标签名)
attrs: [ {end: 13, name: "id", start: 5, value: "app"} ] (元素上面的属性)
unary: false (是不是一元)
start: 0 (开始位置)
复制代码
start (tag, attrs, unary, start) {
// 获取命名空间
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
// 建立一个ast基础元素
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
// 服务端渲染的状况下是否存在被禁止标签
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true
process.env.NODE_ENV !== 'production' && warn(
'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.',
{ start: element.start }
)
}
// 预处理一些动态类型:v-model
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
// 对vue的指令进行处理v-pre、v-if、v-for、v-once、slot、key、ref这里就不细说了
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)
}
// 限制根节点不能是slot,template,v-for这类标签
if (!root) {
root = element
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(root)
}
}
// 不是单标签就入栈,是的话结束这个元素的
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
},
复制代码
处理完成以后element元素就变成了,就是上面说的ast对象的格式。
{
attrsList: [{name: "id", value: "app", start: 5, end: 13}]
attrsMap: {id: "app"}
children: []
parent: undefined
start: 0
tag: "div"
type: 1
}
复制代码
而后是咱们代码里面的一串空格进入了char方法,这个空格被trim
后就变成空了,走到判断text
的地方直接跳过了,这个钩子char函数在咱们列子中{{message}}
这个也会做为文本进入。
chars (text: string, start: number, end: number) {
// 判断有没有父元素
if (!currentParent) {
if (process.env.NODE_ENV !== 'production') {
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.',
{ start }
)
} else if ((text = text.trim())) {
warnOnce(
`text "${text}" outside root element will be ignored.`,
{ start }
)
}
}
return
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
// 储存下currentParent的子元素
const children = currentParent.children
if (inPre || text.trim()) {
text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
} else if (!children.length) {
// remove the whitespace-only node right after an opening tag
text = ''
} else if (whitespaceOption) {
if (whitespaceOption === 'condense') {
// in condense mode, remove the whitespace node if it contains
// line break, otherwise condense to a single space
text = lineBreakRE.test(text) ? '' : ' '
} else {
text = ' '
}
} else {
text = preserveWhitespace ? ' ' : ''
}
if (text) {
if (whitespaceOption === 'condense') {
// condense consecutive whitespaces into single space
text = text.replace(whitespaceRE, ' ')
}
let res
let child: ?ASTNode
// 解析文本,动态属性状况
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
child = {
type: 3,
text
}
}
if (child) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
children.push(child)
}
}
}
复制代码
生成注释ast的函数仍是比较简单的,设置type=3
为注释类型,把text放入对象中而后push
到currentParent.children
comment (text: string, start, end) {
const child: ASTText = {
type: 3,
text,
isComment: true
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
currentParent.children.push(child)
}
复制代码
最后例子进入到end
钩子
end (tag, start, end) {
const element = stack[stack.length - 1]
if (!inPre) {
// 删除无用的最后一个空注释节点
const lastNode = element.children[element.children.length - 1]
if (lastNode && lastNode.type === 3 && lastNode.text === ' ') {
element.children.pop()
}
}
// 修改栈,让父级变成上一级
stack.length -= 1
currentParent = stack[stack.length - 1]
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
element.end = end
}
// 关闭当前元素
closeElement(element)
},
复制代码
到这个结尾能够看一下如今返回的root也就是ast是什么样子,和预想中的基本一致。
这样其实咱们已经把js对象生成ast的过程已经完整的走了一遍,我建议你们仍是本身去对着源码跑一个例子看一下,看看整个流程会对自身对源码理解更加深刻一些。
本文主要围绕着Vue的parse过程进行解析,这个文中确定会有一些不够严谨的思考和错误,欢迎你们指正,有兴趣欢迎一块儿探讨和改进~