前言
模板渲染离不开对性能的要求。Vue2.0引入了Diff的概念,通过一个js对象去储存DOM的节点信息。然后每次渲染的时候先比较一下这个js对象,来达到优化的目的。而Diff的前提就是AST(抽象语法树,abstract syntax code)
源码解析
版本:vue-2.5.17
生成AST的位置:
src\platforms\web\entry-runtime-with-compiler.js
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
调用了方法compileToFunctions,而方法的定义:
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
最终由方法parse生成ast:
const ast = parse(template.trim(), options)
下面来解析这个parse:
parse、parseHTML
路径:src\compiler\parser\index.js
parse里主要是对一些工具方法做了定义,其核心逻辑还是调用parseHTML方法。所以我们先看parseHTML:
parseHTML方法主要作用是对一段template做分解工作。把里面的节点(div等节点,空格,注释)逐一解析。其用的是while:
while (html) {
last = html
// ...处理完一个html片段后,执行advance截断。再处理下一个片段
}
function advance (n) {
index += n
html = html.substring(n)
}
里面解析的流程,
1.先判断是不是最后一段tag,或者是isPlainTextElement: script,style,textarea
先用正则匹配出头部直到下一个'<'符号的内容,然后判断其是一段内容,还是一个tag,通过判断头部是不是有'<'符号
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))
}
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
}
- 是否是注释
- 是否是Doctype
- 是否是结束的tag
- 是否是开始的tag
- 如果不是,则判定其是一段内容,比如一段字符串'hello world',使用options.chars方法处理:
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)
}
if (textEnd < 0) {
text = html
html = ''
}
if (options.chars && text) {
options.chars(text)
}
这五步,我认为比较重要的是345,所以下面着重解析这部分源码。12两点只要看一看源码基本都可以明白。
解析tag start
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
function parseStartTag () {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index
}
advance(start[0].length)
let end, attr
// 没匹配到下一个开头,并且有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
}
}
}
// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
解析Tag头的时候,关键的一些操作:
- tag类型,比如div还是span
- attr参数,比如id="app" class="app"
- 匹配的开头和结尾
- 是否一元标签(unarySlash ),比如input,img这些就是一元标签,他们可以不需要去闭合
- 把节点的信息存储在一个数组stack里,用于等所有节点解析后,按顺序的渲染
- 执行options.start,该方法在parse定义
options.start
关键逻辑:
为第一个节点设置为根节点
// tree management
if (!root) {
root = element
checkRootConstraints(root)
} else if (!stack.length) {
判断当前tag时候是否有父节点,把节点挂载在父节点上:
// 里面做了一些特殊判断,比如判断是否slot节点,这些部分不做详细解析了
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
}
}
判断当前tag是否元节点:
如果不是,把当前节点设置为父节点,这是为了解析下一个子节点时,可以让子节点挂载到父节点。并加入到stack数组
解析tag end
// End tag:
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
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.`
)
}
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)
}
}
}
关键操作:
从stack数组尾部开始遍历,寻找能对应tag尾的tag头。找到以后执行options.end方法,下面我们来解析options.end:
- 获取stack数组尾部数第一个节点,判断这个节点的最后一个子节点,判断是否需要children.pop()
- stack.length减1
- 更新currentParent
解析chars
假如我们的template是<div>hello world</div>,那么hello world就是这段template的chars。
遇到chars解析是这样的:
执行options.chars,
对text进行格式化:
text = inPre || text.trim()
? isTextTag(currentParent) ? text : decodeHTMLCached(text)
使用方法parseText对text进行解析,主要是用于解析节点里的内容。把{{ xx }}节点为一个render函数使用的表达式:
// tag token
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })
##总结
自此生成AST的过程已经解析完成,里面过程复杂,如有错漏,请大家多多指教。
前言
模板渲染离不开对性能的要求。Vue2.0引入了Diff的概念,通过一个js对象去储存DOM的节点信息。然后每次渲染的时候先比较一下这个js对象,来达到优化的目的。而Diff的前提就是AST(抽象语法树,abstract syntax code)
源码解析
版本:vue-2.5.17
生成AST的位置:
src\platforms\web\entry-runtime-with-compiler.js
调用了方法compileToFunctions,而方法的定义:
最终由方法parse生成ast:
下面来解析这个parse:
parse、parseHTML
路径:src\compiler\parser\index.js
parse里主要是对一些工具方法做了定义,其核心逻辑还是调用parseHTML方法。所以我们先看parseHTML:
parseHTML方法主要作用是对一段template做分解工作。把里面的节点(div等节点,空格,注释)逐一解析。其用的是while:
里面解析的流程,
1.先判断是不是最后一段tag,或者是isPlainTextElement: script,style,textarea
先用正则匹配出头部直到下一个'<'符号的内容,然后判断其是一段内容,还是一个tag,通过判断头部是不是有'<'符号
如果是,然后依照以下顺序判断:
这五步,我认为比较重要的是345,所以下面着重解析这部分源码。12两点只要看一看源码基本都可以明白。
解析tag start
解析Tag头的时候,关键的一些操作:
options.start
关键逻辑:
为第一个节点设置为根节点
判断当前tag时候是否有父节点,把节点挂载在父节点上:
判断当前tag是否元节点:
如果不是,把当前节点设置为父节点,这是为了解析下一个子节点时,可以让子节点挂载到父节点。并加入到stack数组
解析tag end
关键操作:
从stack数组尾部开始遍历,寻找能对应tag尾的tag头。找到以后执行options.end方法,下面我们来解析options.end:
解析chars
假如我们的template是
<div>hello world</div>,那么hello world就是这段template的chars。遇到chars解析是这样的:
执行options.chars,
对text进行格式化:
使用方法parseText对text进行解析,主要是用于解析节点里的内容。把{{ xx }}节点为一个render函数使用的表达式:
##总结
自此生成AST的过程已经解析完成,里面过程复杂,如有错漏,请大家多多指教。