Vue 的渲染过程:
这一篇我们来看一下 Vue 是如何解析(compile)模板(template)为 render
函数。
文章目录
1、编译入口
注意:这个源码引用关系有点绕,可以先找到 compileToFunctions
方法(最后面),然后往回看。
当我们使⽤ Runtime + Compiler 的 Vue.js,它的⼊⼝是 src/platforms/web/entry-runtime-with- compiler.js
,看⼀下它对 $mount
函数的定义:
//缓存原型上的 $mount 方法
const mount = Vue.prototype.$mount
//重新定义 $mount 方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
//选择器没有则创建一个 div,有则返回
el = el && query(el)
// 不可把 Vue 挂载到 html body 上
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
//获取参数
const options = this.$options
//没有 render , 解析模板/el并转换为渲染函数
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
//获取 template 选择器的 innerHtml
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
//如果元素节点则直接返回 innerHtml
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
//获取元素的outerHTML,同时处理IE中的SVG元素
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
//日志 开始编译
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
//编译
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
//日志 编译结束
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
//调⽤原先原型上的 $mount ⽅法挂载
return mount.call(this, el, hydrating)
}
这段函数逻辑之前分析过:参考(Vue2.x 源码 - 初始化:实例挂载($mount)的实现),关于编译的⼊⼝就是在这⾥:
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production', //记录模板解析
shouldDecodeNewlines, //ie
shouldDecodeNewlinesForHref,//chrome
delimiters: options.delimiters, //Vue 提供的选项,改变纯文本插入分隔符,默认{{}}
comments: options.comments //Vue 提供的选项,当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
如果 shouldDecodeNewlines = true
,意味着 Vue 在编译模板的时候,要对属性值中的换行符或制表符做兼容处理。而shouldDecodeNewlinesForHref = true
意味着 Vue 在编译模板的时候,要对 a 标签的 href 属性值中的换行符或制表符做兼容处理。
compileToFunctions
⽅法就是把模板 template
编译⽣成 render
以及 staticRenderFns
,它 的定义在 src/platforms/web/compiler/index.js
中:
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
可以看到 compileToFunctions
⽅法实际上是 createCompiler
⽅法的返回值,该⽅法接收⼀个编 译配置参数,接下来我们来看⼀下 createCompiler
⽅法的定义,在 src/compiler/index.js
中:
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
//解析模板字符串生成AST
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
//优化语法树,对AST进行静态节点标记
optimize(ast, options)
}
//抽象语法树(AST) 生成 render 函数代码字符串
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
createCompiler
⽅法实际上是通过调⽤ createCompilerCreator
⽅法返回的,该⽅法传⼊的参数 是⼀个函数,真正的编译过程都在这个 baseCompile
函数⾥执⾏,这个在后面细看;那么 createCompilerCreator
⼜是什么呢,它的定义在 src/compiler/create-compiler.js
中:
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
//缓存入参baseOptions,baseOptions是编译时默认参数
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
//warn方法,编译过程中对错误和提示收集
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
//编译时传入的参数
if (options) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
// 定义warn方法
const leadingSpaceLength = template.match(/^\s*/)[0].length
warn = (msg, range, tip) => {
const data: WarningMessage = { msg }
if (range) {
if (range.start != null) {
data.start = range.start + leadingSpaceLength
}
if (range.end != null) {
data.end = range.end + leadingSpaceLength
}
}
(tip ? tips : errors).push(data)
}
}
// 合并modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// 合并 directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
// 合并 options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
finalOptions.warn = warn
const compiled = baseCompile(template.trim(), finalOptions)
if (process.env.NODE_ENV !== 'production') {
detectErrors(compiled.ast, warn)
}
compiled.errors = errors
compiled.tips = tips
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
可以看到该⽅法返回了⼀个 createCompiler
的函数,它接收⼀个 baseOptions
的参数,返回的 是⼀个对象,包括 compile
⽅法属性和 compileToFunctions
属性:
compile
方法是用于编译的,这个方法的核心代码是 const compiled = baseCompile(template.trim(), finalOptions)
,调用 baseCompile
方法,返回编译后的 compiled 对象;
compileToFunctions
对应的就是 $mount
函数调⽤的 compileToFunctions
⽅法,它是调⽤ createCompileToFunctionFn
⽅法的返回值,我们接下来看⼀下 createCompileToFunctionFn
⽅ 法,它的定义在 src/compiler/to-function/js
中:
export function createCompileToFunctionFn (compile: Function): Function {
//缓存对象
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
//对象上的属性扩展一份到新对象上
options = extend({}, options)
//提取出warn错误提示信息函数
const warn = options.warn || baseWarn
//删除options里面的warn方法
delete options.warn
if (process.env.NODE_ENV !== 'production') {
// 检测可能的CSP限制(检测 new Function() 是否可用)
try {
new Function('return 1')
} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
warn(
'It seems you are using the standalone build of Vue.js in an ' +
'environment with Content Security Policy that prohibits unsafe-eval. ' +
'The template compiler cannot work in this environment. Consider ' +
'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
'templates into render functions.'
)
}
}
}
// 检查缓存里是否已经存在
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// 调用compile来解析 template和options(主要操作)
const compiled = compile(template, options)
// 检查编译errors/tips,并打印
if (process.env.NODE_ENV !== 'production') {
if (compiled.errors && compiled.errors.length) {
if (options.outputSourceRange) {
compiled.errors.forEach(e => {
warn(
`Error compiling template:\n\n${e.msg}\n\n` +
generateCodeFrame(template, e.start, e.end),
vm
)
})
} else {
warn(
`Error compiling template:\n\n${template}\n\n` +
compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
vm
)
}
}
if (compiled.tips && compiled.tips.length) {
if (options.outputSourceRange) {
compiled.tips.forEach(e => tip(e.msg, vm))
} else {
compiled.tips.forEach(msg => tip(msg, vm))
}
}
}
// 将代码转换为函数
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// 检查函数生成错误。只有在编译器本身存在错误时才会发生这种情况。主要用于代码生成开发
if (process.env.NODE_ENV !== 'production') {
if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
warn(
`Failed to generate render function:\n\n` +
fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
vm
)
}
}
//缓存
return (cache[key] = res)
}
}
⾄此我们总算找到了 compileToFunctions
的最终定义,它接收 3 个参数、编译模板 template , 编译配置 options 和 Vue 实例 vm ;核⼼的编译过程就⼀⾏代码:const compiled = compile(template, options)
;然后对 error 和 tips 进行处理;
总结:
1、在
$mount
挂载的时候执行compileToFunctions
方法;
2、在compileToFunctions
方法中调用作为参数传入的compile
方法编译模板,处理编译后的 errors 和 tips,将编译后的 render 和 staticRenderFns 转化为函数然后返回,并缓存编译结果,防止重复编译;
3、compile
方法中将默认参数 baseOptions 和编译传入的参数 options 进行合并,调用baseCompile
方法返回编译后的 compiled 对象;
4、然后在 最最核心的方法baseCompile
里面分别调用parse 、optimize 、generate
这三个方法;
2、parse
parse
方法的作用是对 template 模板进行编译,编译的结果是一个 AST 抽象语法树,AST 是对源代码抽象语法结构的树状表现形式。
模板编译阶段,是使用createASTElement
方法来创建 AST。
export function createASTElement (
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1, //元素类型
tag, // 元素标签
attrsList: attrs, // 元素属性数组
attrsMap: makeAttrsMap(attrs), // 元素属性key-value
rawAttrsMap: {}, //ASTAttr类型的元素属性
parent, // 父元素
children: [] // 子元素集合
}
}
返回的是一个 ASTElement 类型的对象。
插入一下:AST是什么?
看看下面这段代码:
<div>
<p>{{name}}</p>
</div>
转成 AST 就是:
{
type: 1,
tag:"div",
staticRoot: false,
static: false,
plain: true,
attrsList: [],
attrsMap: {},
children: [
{
type: 1,
tag: "p",
staticRoot: false,
static: false,
plain: true,
attrsList: [],
attrsMap: {},
parent: {type: 1, tag: "div", ...},
children: [{
type: 2,
text: "{{name}}",
static: false,
expression: "_s(name)"
}]
}
]
}
不难看出,AST 就是利用 javascript 中的对象来描述节点,一个对象对应一个节点,对象中的属性保存的都是对应节点相关的数据;
下面我们看看parse
方法,在src/compiler/parser/index.js
文件中:
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
....
let root
parseHTML(template, {
....
//开始标签钩子
start (tag, attrs, unary, start, end) {
....
},
//结束标签钩子
end (tag, start, end) {
....
},
//文本钩子
chars (text: string, start: number, end: number) {
....
},
//注释钩子
comment (text: string, start, end) {
....
}
})
return root
}
parse
方法主要是通过调用parseHTML
函数对模板字符串进行解析,通过传入不同的钩子函数分别对 开始标签、结束标签
文本、注释 进行处理,实际上parseHTML
函数的作用就是用来做词法分析的,而parse
函数的作用则是在词法分析的基础上做句法分析从而生成一棵 AST。接下来看一下整个解析的过程;
parseHTML 解析
在 src/compiler/parser/html-parser.js
文件中:
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
// 确保我们不是在像script/style这样的纯文本内容元素中
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
//html 字符串的第一个字符就是左尖括号
if (textEnd === 0) {
// 注释节点,前进⾄末尾位置
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
}
}
// 条件注释节点,前进⾄末尾位置
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}
// 文本类型节点,前进它⾃⾝⻓度的距离
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
}
// 开始标签:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
}
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)
}
if (textEnd < 0) {
text = html
}
if (text) {
advance(text.length)
}
if (options.chars && text) {
options.chars(text, index - text.length, index)
}
} else {
// parse 的内容是在纯文本标签里 (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') // #7298
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1)
}
if (options.chars) {
options.chars(text)
}
return ''
})
index += html.length - rest.length
html = rest
parseEndTag(stackedTag, index - endTagLength, index)
}
//相等说明循环结束
if (html === last) {
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}"`, { start: index + html.length })
}
break
}
}
// 清理任何残留的标签
parseEndTag()
//字符串前进方法,不断截取
function advance (n) {
index += n
html = html.substring(n)
}
}
parseHTML
解析器是通过 while 循环解析模板字符串 template,用正则做各种匹配,对不同的情况分别进行不同的处理,直到整个 template 被解析完毕;整个匹配的过程中会用到 advance
方法来不断移动字符串,直到字符串的末尾;
注意:script,style,textarea 是纯文本标签解析的时候会用文本标签来解析;
HTML解析类型
1、开始标签(<>):textEnd ( < 位置)等于0,正则校验是开始标签,遍历开始标签里面的属性并解析保存,然后截取掉开始标签,将二元标签的开始标签放入 stack 数组中,调用 start 方法;
2、结束标签(</>):textEnd ( < 位置)等于0,正则校验是结束标签,截取掉结束标签,然后检查 stack 数组里面是否有缺少闭合标签的二元标签,会特殊处理:将</br>
标签解析成<br>
标签,将</p>
标签正常解析成<p></p>
;最后缺少闭合标签的回逐个发出警告,并调用 end 方法将其闭合;
3、文本:textEnd ( < 位置)小于0或者大于等于0,将其截取,调用 chars 方法;(其他类型都匹配不上则认为是文本,文本中可以有 < 符号)
4、注释:textEnd ( < 位置)等于0,正则校验是注释,前进⾄末尾位置,调用 comment 方法;
5、doctype:textEnd ( < 位置)等于0,正则校验是 doctype,只需要将其截取;
6、条件注释:textEnd ( < 位置)等于0,正则校验是 ‘]>’,只需要将其截取;
下面介绍一下解析过程中用到的其他解析器:
属性解析
makeAttrsMap
方法把 name/value
对象数组形式,转换成 key/value
对象 ,AST 如下:
attrsList: [
{ name: 'id', value: 'box' },
{ name: 'class', value: 'box-class' },
{ name: ':class', value: 'boxClass' }
],
attrsMap: {
id: 'box',
class: 'box-class',
:class: 'boxClass'
}
指令解析
这里只说一下 v-if
(其他的就不细说了,大致思路都差不多)
function processIf (el) {
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
el.if = exp
addIfCondition(el, {
exp: exp,
block: el
})
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
if (!el.ifConditions) {
el.ifConditions = []
}
el.ifConditions.push(condition)
}
if 指令会对 AST 上添加一个 if 属性和一个 ifConditions 属性,for 指令类似,添加之后的 AST:
attrsList: [
{ name: 'v-if', value: 'list.length' }
],
attrsMap: {
v-if: 'list.length'
},
if: 'list.length',
ifConditions: [
{ exp: 'list.length', block: 'ast对象自身', }
],
文本解析
对 html 解析器解析出来的文本二次加个,区分纯文本、带变量的文本;
在 src/compiler/parser/text-parser.js
文件里:
export function parseText (
text: string,
delimiters?: [string, string]
): TextParseResult | void {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
if (!tagRE.test(text)) {
return
}
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
while ((match = tagRE.exec(text))) {
index = match.index
// 先把 { { 前边的文本添加到 tokens 中
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index))
tokens.push(JSON.stringify(tokenValue))
}
// 把变量改成 `_s(x)` 这样的形式也添加到数组中
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })
lastIndex = index + match[0].length
}
// 当所有变量都处理完毕后, 如果最后一个变量右边还有文本, 就将文本添加到数组中
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex))
tokens.push(JSON.stringify(tokenValue))
}
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
如果是纯文本, 直接 return,在chars 方法中创建一个 type = 3
的对象。 如果是带变量的文本, 使用正则表达式匹配出文本中的变量, 先把变量左边的文本添加到数组中, 然后把变量改成 _s(x)
这样的形式也添加到数组中.;如果变量后面还有变量, 则重复以上动作, 直到所有变量都添加到数组中.,最后创建一个 type = 2
的对象;
处理后的 AST:
parseText('文本{{name}}');
->
'"文本" + _s(name)'
过滤器解析
vue 的 filter 允许用在两个地方,一个是双括号插值,一个是 v-bind
表达式后面,在 src/compiler/parser/filter-parser.js
文件里:
export function parseFilters (exp: string): string {
let inSingle = false
let inDouble = false
let inTemplateString = false
let inRegex = false
let curly = 0
let square = 0
let paren = 0
let lastFilterIndex = 0
let c, prev, i, expression, filters
//循环变量属性
for (i = 0; i < exp.length; i++) {
prev = c
//exp第i个字符的ASCII码
c = exp.charCodeAt(i)
if (inSingle) {
if (c === 0x27 && prev !== 0x5C) inSingle = false
} else if (inDouble) {
if (c === 0x22 && prev !== 0x5C) inDouble = false
} else if (inTemplateString) {
if (c === 0x60 && prev !== 0x5C) inTemplateString = false
} else if (inRegex) {
if (c === 0x2f && prev !== 0x5C) inRegex = false
} else if (
c === 0x7C && // pipe
// 前后都不是| 确定不是或运算 ||
exp.charCodeAt(i + 1) !== 0x7C &&
exp.charCodeAt(i - 1) !== 0x7C &&
不在各种括弧内
!curly && !square && !paren
) {
if (expression === undefined) {
// first filter, end of expression
lastFilterIndex = i + 1
expression = exp.slice(0, i).trim()
} else {
pushFilter()
}
} else {
switch (c) {
case 0x22: inDouble = true; break // "
case 0x27: inSingle = true; break // '
case 0x60: inTemplateString = true; break // `
case 0x28: paren++; break // (
case 0x29: paren--; break // )
case 0x5B: square++; break // [
case 0x5D: square--; break // ]
case 0x7B: curly++; break // {
case 0x7D: curly--; break // }
}
if (c === 0x2f) { // /
let j = i - 1
let p
// find first non-whitespace prev char
for (; j >= 0; j--) {
p = exp.charAt(j)
if (p !== ' ') break
}
if (!p || !validDivisionCharRE.test(p)) {
inRegex = true
}
}
}
}
// 如果expression 为空,则说明没有没有filter函数,或者是写法出了问题。某些符号没闭合
if (expression === undefined) {
expression = exp.slice(0, i).trim()
} else if (lastFilterIndex !== 0) {
pushFilter()
}
// 初始化filters变量。将过滤器函数推入filters数组中
function pushFilter () {
(filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
lastFilterIndex = i + 1
}
// filter函数结合expression生成最终的表达式
if (filters) {
for (i = 0; i < filters.length; i++) {
expression = wrapFilter(expression, filters[i])
}
}
return expression
}
parseFilters
方法是循环 exp,找出过滤器放到 expression 中,然后执行 pushFilter
将过滤器推到 filters 数组中,最后循环调用 wrapFilter
方法对每一个过滤器做处理;这里区分过滤器的写法:函数名、fn(sss)
生成的AST:
let html = '<div>{{ msg | reverse() | toUpperCase() }}</div>'
//filter解析之后
const expression = '_f("toUpperCase")(_f("reverse")(msg))'
//在文本中再次解析
const tokens = ['_s(_f("toUpperCase")(_f("reverse")(msg)))']
最后来看一下 parse
方法里面的四个钩子函数:
start
处理开始标签,这个钩子函数就是慢慢地给这个AST进行装饰,添加更多的属性和标志;
start (tag, attrs, unary, start, end) {
// 检查命名规范,父元素有则继承
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// 兼容IE svg
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
//通过createASTElement创建AST
let element: ASTElement = createASTElement(tag, attrs, currentParent)
//为AST添加相关属性
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
}, {})
}
attrs.forEach(attr => {
if (invalidAttributeRE.test(attr.name)) {
warn(
`Invalid dynamic argument expression: attribute names cannot contain ` +
`spaces, quotes, <, >, / or =.`,
{
start: attr.start + attr.name.indexOf(`[`),
end: attr.start + attr.name.length
}
)
}
})
}
// 服务端渲染的情况下是否存在被禁止标签
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)
}
}
end
对缓存开始标签的 stack 数组操作,去除最后一个,记录结束位置,最后关闭元素;
end (tag, start, end) {
//缓存栈顶元素
const element = stack[stack.length - 1]
// 栈顶元素推出
stack.length -= 1
currentParent = stack[stack.length - 1]
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
element.end = end
}
//关闭元素
closeElement(element)
}
chars
文本标签,没有父元素将会报错,然后对空格做一些处理,后面就是根据文本类型生成对于的 AST 对象;
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
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) {
// 将连续的空格压缩为单个空格
text = ''
} else if (whitespaceOption) {
if (whitespaceOption === 'condense') {
// 在压缩模式下,如果包含换行符,则删除空白节点,否则压缩为单个空格
text = lineBreakRE.test(text) ? '' : ' '
} else {
text = ' '
}
} else {
text = preserveWhitespace ? ' ' : ''
}
if (text) {
if (!inPre && whitespaceOption === 'condense') {
// 将连续的空格压缩为单个空格
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)
}
}
}
comment
直接生成 type = 3
的 AST 对象,放到 currentParent.children
里面;
comment (text: string, start, end) {
// adding anything as a sibling to the root node is forbidden
// comments should still be allowed, but ignored
if (currentParent) {
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)
}
}
3、optimize
我们的模板 template 经过 parse
解析之后,会返回一个 AST 树,而 optimize
就是对这个树的优化;那么为什么要经过这个过程呢?
因为 Vue 是数据驱动,很多数据都是响应式的,但是我们的模板中并不是所有的数据都是响应式的,在 patch 的过程中需要跳过这些非响应式数据的对比来提升 patch 的性能,而 optimize 会将一些 AST 节点优化成静态节点;
在 src/compiler/optimizer.js
文件中:
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// 第一次:标记所有非静态节点。
markStatic(root)
// 第二步:标记静态根。
markStaticRoots(root, false)
}
静态节点标记
function markStatic (node: ASTNode) {
node.static = isStatic(node)
//type = 2 、3 已经在isStatic里面处理过了
if (node.type === 1) {
// node.tag 不是 HTML 保留标签时、标签不是slot、node不是一个内联模板容器;直接返回false
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
//递归子节点,根据子节点的static设置父节点的static
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
//如果当前节点有v-if/v-else-if/v-else等指令
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}
//当前节点是否是静态节点
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
静态节点:有一个连续判断,当前节点的tag不是 HTML 保留标签也不是 slot ,同时当前节点不是一个内联模板容器,则直接返回当前节点的 static 为false,不在循环子节点;否则循环子节点,在对 children 子节点标记完毕后,会根据子节点的 static 属性来设置父节点的 static 属性,只要有一个子节点的 static 属性不为 true ,那么父节点也一定不为 true 。
下面这三种情况肯定是静态节点:
1、纯文本节点;
2、普通元素节点并且使用了v-pre指令都是静态节点;
3、在没有使用v-pre指令的情况下,还必须同时满足:没有动态绑定属性、没有使用v-if、没有使用v-for、不是内置组件slot/component、是平台保留标签、不是带有v-for的template标签的直接子节点、节点的所有属性的key都是静态key;
静态根节点标记
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor
}
// 当前节点是静态的,子节点不能只是一个且是纯文本节点
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
当前节点是静态的,那么它所有的子节点都应该是静态的,这是在标记静态节点的时候就处理好的;对于只有一个纯文本节点的根节点不做优化处理,每次都会重新渲染;
在 optimize
优化的过程中,它的处理方式是深度遍历 AST 树形结构,遇到静态节点的时候把它的 ast.static
属性设置为 true。同时对于一个父 AST 节点来说,当其 children 子节点全部为静态节点的时候,那么其本身也是一个静态节点,我们把它的 ast.staticRoot
设置为 true。
4、generate
在compileToFunctions
中,会把这个 render 代码串转换成函数,它的定义在 src/compler/to-function.js
中
const compiled = compile(template, options)
res.render = createFunction(compiled.render, fnGenErrors)
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop }
}
把 render 代码串通过 new Function
的⽅式转换成可执⾏的函数,赋值给 vm.options.render
,这样当组件通过 vm._render
的时候,就会执⾏这个 render
函数,那么接下来就开始 generate
的部分看看 render 代码串的生成过程;
generate
这里是编译的最后一步,将 AST 树转换成可执行的代码(render),在src/compiler/codegen/index.js
里面:
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
可以看出,关键部分在 code,ast 存在 则调用 genElement
,否则调用 _c
,_c
是 createElement
方法用来创建 vnode;code 最后会用
with(this){return ${code}}
包裹起来,转换成方法:
const func = function () {
with (this) {
return ${code}
}
}
接下来看看 genElement
:
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
这里没什么好说的,就是根据 AST 属性的不同来分别调用不同的代码生成函数,最后返回 code;
genStatic
在第一次执行genElement
方法时,节点的 staticRoot 为 true,则会走这个方法;
function genStatic (el: ASTElement, state: CodegenState): string {
//避免重复调用
el.staticProcessed = true
// 在v-pre节点中,有些元素(模板)的行为需要有所不同。所有的pre节点都是静态根节点
// 所以我们可以使用它作为一个位置来包装状态更改,并在退出pre节点时重置它。
const originalPreState = state.pre
if (el.pre) {
state.pre = el.pre
}
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
state.pre = originalPreState
return `_m(${
state.staticRenderFns.length - 1
}${
el.staticInFor ? ',true' : ''
})`
}
这里特殊处理了 v-pre
节点,默认state 的 pre 是 false,如果当前节点有 pre 属性,则赋值过来,push 完之后重置 pre 的值;然后递归调用 genElement
方法处理子节点,将所有是静态根节点都放到 staticRenderFns 数组中;最后返回一个用 _m
方法处理过的值;
_m
方法是一个辅助器,看看是走缓存还是重新渲染新的树;
这里可以看出:静态根节点代码生成的 render 存放在 staticRenderFns 中,而不是 render
genOnce
用来处理 v-once
指令的
function genOnce (el: ASTElement, state: CodegenState): string {
el.onceProcessed = true
if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.staticInFor) {
let key = ''
let parent = el.parent
while (parent) {
if (parent.for) {
key = parent.key
break
}
parent = parent.parent
}
if (!key) {
process.env.NODE_ENV !== 'production' && state.warn(
`v-once can only be used inside v-for that is keyed. `,
el.rawAttrsMap['v-once']
)
return genElement(el, state)
}
return `_o(${genElement(el, state)},${state.onceId++},${key})`
} else {
return genStatic(el, state)
}
}
如果与 if 并存,就先执行 genIf
再执行 genOnce
;如果在 for 循环中且为静态节点,用 _o
方法进行标记;否则使用 genStatic
方法生成节点,genStatic
具有缓存性;
genFor
用来处理 v-for
指令的
export function genFor (
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
if (process.env.NODE_ENV !== 'production' &&
state.maybeComponent(el) &&
el.tag !== 'slot' &&
el.tag !== 'template' &&
!el.key
) {
state.warn(
`<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
`v-for should have explicit keys. ` +
`See https://vuejs.org/guide/list.html#key for more info.`,
el.rawAttrsMap['v-for'],
true /* tip */
)
}
el.forProcessed = true // avoid recursion
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
从 AST 节点获取和 for 相关属性,然后返回一个循环的 function 字符串;然后调用 genElement
方法处理子节点;
genIf
用来处理 v-if/v-else
等指令的;
export function genIf (
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
el.ifProcessed = true // avoid recursion
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
function genIfConditions (
conditions: ASTIfConditions,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
if (!conditions.length) {
return altEmpty || '_e()'
}
//获取数组的第一个元素
const condition = conditions.shift()
if (condition.exp) {
return `(${condition.exp})?${
genTernaryExp(condition.block)
}:${
genIfConditions(conditions, state, altGen, altEmpty)
}`
} else {
return `${genTernaryExp(condition.block)}`
}
// v-if with v-once should generate code like (a)?_m(0):_m(1)
function genTernaryExp (el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}
主要是使用 genIfConditions
方法;入参 el.ifConditions
调用了 slice()
返回的其实就是 el.ifConditions
;每次使用 shift()
获取数组第一项,如果有 exp 属性则拼接并递归调用 genIfConditions
方法处理子节点;最后会调用 genTernaryExp
方法转换成 (a)?_m(0):_m(1)
这种模式;然后调用 genElement
方法处理子节点;
genChildren
处理子节点
export function genChildren (
el: ASTElement,
state: CodegenState,
checkSkip?: boolean,
altGenElement?: Function,
altGenNode?: Function
): string | void {
const children = el.children
if (children.length) {
const el: any = children[0]
// optimize single v-for
if (children.length === 1 &&
el.for &&
el.tag !== 'template' &&
el.tag !== 'slot'
) {
const normalizationType = checkSkip
? state.maybeComponent(el) ? `,1` : `,0`
: ``
return `${(altGenElement || genElement)(el, state)}${normalizationType}`
}
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
const gen = altGenNode || genNode
return `[${children.map(c => gen(c, state)).join(',')}]${
normalizationType ? `,${normalizationType}` : ''
}`
}
}
如果是node.type
为1,那么又会回到genElement
方法继续生成 vnode ;否则生成文本节点或注释节点;
genSlot
处理插槽
function genSlot (el: ASTElement, state: CodegenState): string {
const slotName = el.slotName || '"default"'
const children = genChildren(el, state)
let res = `_t(${slotName}${children ? `,${children}` : ''}`
const attrs = el.attrs || el.dynamicAttrs
? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
// slot props are camelized
name: camelize(attr.name),
value: attr.value,
dynamic: attr.dynamic
})))
: null
const bind = el.attrsMap['v-bind']
if ((attrs || bind) && !children) {
res += `,null`
}
if (attrs) {
res += `,${attrs}`
}
if (bind) {
res += `${attrs ? '' : ',null'},${bind}`
}
return res + ')'
}
genComponent
处理组件
function genComponent (
componentName: string,
el: ASTElement,
state: CodegenState
): string {
const children = el.inlineTemplate ? null : genChildren(el, state, true)
return `_c(${componentName},${genData(el, state)}${
children ? `,${children}` : ''
})`
}
和普通节点生成类似,不同点则是标签名称是组件名称;
genData
处理节点上各种属性、指令、事件、作用域插槽以及 ref 等等;
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// directives first.
// directives may mutate the el's other properties before they are generated.
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','
// key
if (el.key) {
data += `key:${el.key},`
}
// ref
if (el.ref) {
data += `ref:${el.ref},`
}
if (el.refInFor) {
data += `refInFor:true,`
}
// pre
if (el.pre) {
data += `pre:true,`
}
// record original tag name for components using "is" attribute
if (el.component) {
data += `tag:"${el.tag}",`
}
// module data generation functions
for (let i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el)
}
// attributes
if (el.attrs) {
data += `attrs:${genProps(el.attrs)},`
}
// DOM props
if (el.props) {
data += `domProps:${genProps(el.props)},`
}
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)},`
}
// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += `slot:${el.slotTarget},`
}
// scoped slots
if (el.scopedSlots) {
data += `${genScopedSlots(el, el.scopedSlots, state)},`
}
// component v-model
if (el.model) {
data += `model:{value:${
el.model.value
},callback:${
el.model.callback
},expression:${
el.model.expression
}},`
}
// inline-template
if (el.inlineTemplate) {
const inlineTemplate = genInlineTemplate(el, state)
if (inlineTemplate) {
data += `${inlineTemplate},`
}
}
data = data.replace(/,$/, '') + '}'
// v-bind dynamic argument wrap
// v-bind with dynamic arguments must be applied using the same v-bind object
// merge helper so that class/style/mustUseProp attrs are handled correctly.
if (el.dynamicAttrs) {
data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
}
// v-bind data wrap
if (el.wrapData) {
data = el.wrapData(data)
}
// v-on data wrap
if (el.wrapListeners) {
data = el.wrapListeners(data)
}
return data
}
根据 AST 元素节点的属性构造出⼀个 data 对象字符串,这个在后⾯创建 vnode 的时候的时候会作为参数传⼊;
总结:
parse
方法将 tempalte 模板通过正则匹配进行词法语法分析,编译成 AST 树(抽象语法树); optimize
方法 将解析后的 AST 树通过静态根节点标记的方式进行优化; generate
方法将优化后的 AST 树转换成可执行的代码(字符串);
最后会通过 createFunction
方法将 compile
转化的结果通过 new Function(code)
转成函数,这样在 $mount
阶段的 render
和 staticRenderFns
就得到了,后面就是 render
比对和真实 DOM 的渲染;
编译的部分就到这里了! 有不对的地方欢迎留言指正~~