Vue源码解读(七):模板编译
在最开始的章节提到过,我们在使用 vue-cli
创建项目的时候,提供了两个版本供我们使用, Runtime Only
版本和 Runtime + Compiler
版本。Runtime Only
版本是不包含编译器的,在项目打包的时候会把模板编译成 render
函数,也叫预编译。Runtime + Compiler
版本包含编译器,可以把编译过程放在运行时做。
入口
这一块代码量比较多,主要是对各种情况做了一些边界处理。这里只关注主流程。对细节感兴趣的伙伴们可以自行去研究。一般我们使用 Runtime + Compiler
版本可能比较多一些,先来找到入口:
// src/platforms/web/entry-runtime-with-compiler.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// ...
if (!options.render) {
// 模版就绪,进入编译阶段
if (template) {
// 编译模版,得到 动态渲染函数和静态渲染函数
const { render, staticRenderFns } = compileToFunctions(template, {
// 在非生产环境下,编译时记录标签属性在模版字符串中开始和结束的位置索引
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
// 界定符,默认 {{}}
delimiters: options.delimiters,
// 是否保留注释
comments: options.comments
}, this)
}
}
}
compileToFunctions
方法就是把 template
编译而得到 render
以及 staticRenderFns
。
compileToFunctions
// src/platforms/web/compiler/index.js
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
createCompiler
// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 将模版解析为 AST
const ast = parse(template.trim(), options)
// 优化 AST,静态标记
if (options.optimize !== false) {
optimize(ast, options)
}
// 生成渲染函数,,将 ast 转换成可执行的 render 函数的字符串形式
// code = {
// render: `with(this){return ${_c(tag, data, children, normalizationType)}}`,
// staticRenderFns: [_c(tag, data, children, normalizationType), ...]
// }
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 {
// 以平台特有的编译配置为原型创建编译选项对象
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
// 日志,负责记录将 error 和 tip
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
if (options) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
// $flow-disable-line
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)
}
}
// 合并配置项 options 到 finalOptions
// merge custom modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
// copy other 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)
}
}
}
createCompilerCreator
返回了一个 createCompiler
函数,createCompiler
返回了一个对象,包括了 compile
和 compileToFunctions
,这个 compileToFunctions
对应的就是 $mount
中调用的 compileToFunctions
方法。在 createCompiler
函数内定义了 compile
方法,并把它传递给 createCompileToFunctionFn
,compile
主要目的就是对特有平台的配置项做一些合并,如 web 平台和处理一些在编译期间产生的错误。
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)
const warn = options.warn || baseWarn
delete options.warn
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
// detect possible CSP restriction
// 检测可能的 CSP 限制
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.'
)
}
}
}
// check cache
// 如果有缓存,则跳过编译,直接从缓存中获取上次编译的结果
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// compile
// 执行编译函数,得到编译结果
const compiled = compile(template, options)
// check compilation errors/tips
// 检查编译期间产生的 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))
}
}
}
// turn code into functions
// 转换编译得到的字符串代码为函数,通过 new Function(code) 实现
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// check function generation errors.
// this should only happen if there is a bug in the compiler itself.
// mostly for codegen development use
/* istanbul ignore if */
// 处理上面代码转换过程中出现的错误
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)
}
}
createCompileToFunctionFn
返回了 compileToFunctions
这个就是我们要找的最终定义所在了,它主要做了这么几件事:
- 执行编译函数得到编译结果。
- 将编译得到的字符串代码转换成可执行的函数。
- 处理异常
- 缓存
小结
通过以上的代码可以看出真正编译个过程就在 createCompilerCreator
函数传递的 baseCompile
中,主要分为这么几个部分:
- 将模板解析成 AST
const ast = parse(template.trim(), options)
- 优化 AST (静态标记)
optimize(ast, options)
- 生成代码字符串
const code = generate(ast, options)
之所以在真正编译之前做了这么多前戏,目的就是为了对不同平台做一些处理。下面主要针对这三个部分看看做了一些什么事情。
parse
parse
主要作用就是将模板解析成 AST,它是一种抽象语法树。这个过程比较复杂,它会在每个节点的 AST 对象上设置元素的所有信息,比如:父节点、子节点、标签信息、属性信息、插槽信息等等。在期间会用到许多正则表达式对模板进行匹配。
parse
// src/compiler/parser/index.js
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
warn = options.warn || baseWarn
// 是否为 pre 标签
platformIsPreTag = options.isPreTag || no
// 必须使用 props 进行绑定的属性
platformMustUseProp = options.mustUseProp || no
// 获取标签的命名空间
platformGetTagNamespace = options.getTagNamespace || no
// 是否是保留标签(html + svg)
const isReservedTag = options.isReservedTag || no
// 是否为一个组件
maybeComponent = (el: ASTElement) => !!(
el.component ||
el.attrsMap[':is'] ||
el.attrsMap['v-bind:is'] ||
!(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag))
)
// 分别获取 options.modules 下的 class、model、style 三个模块中的 transformNode、preTransformNode、postTransformNode 方法
// 负责处理元素节点上的 class、style、v-model
transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
// 界定符,比如: {{}}
delimiters = options.delimiters
const stack = []
// 空格选项
const preserveWhitespace = options.preserveWhitespace !== false
const whitespaceOption = options.whitespace
// 根节点,以 root 为根,处理后的节点都会按照层级挂载到 root 下,最后 return 的就是 root,一个 ast 语法树
let root
// 当前元素的父元素
let currentParent
let inVPre = false
let inPre = false
let warned = false
function warnOnce (msg, range) {
if (!warned) {
warned = true
warn(msg, range)
}
}
// 由于代码比较长,后面几个方法单独拿出来。
function closeElement (element) { /*...*/ }
function trimEndingWhitespace (el) { /*...*/ }
function checkRootConstraints (el) { /*...*/ }
parseHTML(template, {
/*...*/
})
return root
}
parse
接收两个参数 template
和 options
,也就是模板字符串和配置选项,options
定义在 /src/platforms/web/compiler/options
中,这里主要是不同的平台(web 和 weex)的配置选项不同。
closeElement
// src/compiler/parser/index.js
function closeElement (element) {
// 移除节点末尾的空格
trimEndingWhitespace(element)
// 当前元素不再 pre 节点内,并且也没有被处理过
if (!inVPre && !element.processed) {
// 分别处理元素节点的 key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、其它指令和一些原生属性
element = processElement(element, options)
}
// 处理根节点上存在 v-if、v-else-if、v-else 指令的情况
// 如果根节点存在 v-if 指令,则必须还提供一个具有 v-else-if 或者 v-else 的同级别节点,防止根元素不存在
// 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 }
)
}
}
// 建立父子关系,将自己放到父元素的 children 数组中,然后设置自己的 parent 属性为 currentParent
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
}
}
// 设置子元素,将自己的所有非插槽的子元素设置到 element.children 数组中
// 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
}
// 分别为 element 执行 model、class、style 三个模块的 postTransform 方法
// 但是 web 平台没有提供该方法
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
closeElement
方法主要做了三件事:
- 如果元素没有被处理,调用
processElement
处理元素上的一些属性。 - 设置当前元素的父元素。
- 设置当前元素的子元素。
trimEndingWhitespace
// src/compiler/parser/index.js
function trimEndingWhitespace (el) {
// remove trailing whitespace node
if (!inPre) {
let lastNode
while (
(lastNode = el.children[el.children.length - 1]) &&
lastNode.type === 3 &&
lastNode.text === ' '
) {
el.children.pop()
}
}
}
trimEndingWhitespace
作用就是删除元素中空白的文本节点。
checkRootConstraints
// src/compiler/parser/index.js
function checkRootConstraints (el) {
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.',
{ start: el.start }
)
}
if (el.attrsMap.hasOwnProperty('v-for')) {
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.',
el.rawAttrsMap['v-for']
)
}
}
checkRootConstraints
的作用是对根元素的检查,不能使用 slot
和 template
作为根元素,不能在有状态组件的根元素上使用 v-for
,因为它会渲染出多个元素。
parseHTML
// src/compiler/parser/index.js
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
/**
* 开始处理标签
* @param {*} tag 标签名
* @param {*} attrs [{ name: attrName, value: attrVal, start, end }, ...] 形式的属性数组
* @param {*} unary 自闭合标签
* @param {*} start 标签在 html 字符串中的开始索引
* @param {*} end 标签在 html 字符串中的结束索引
*/
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)
}
// 创建 AST 对象
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
}, {})
}
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
}
)
}
})
}
// 非服务端渲染的情况下,模版中不应该出现 <style></style>、<script></script> 标签
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 }
)
}
// 为 element 对象分别执行 class、style、model 模块中的 preTransforms 方法
// 处理存在 v-model 指令的 input 标签,分别处理 input 为 checkbox、radio、其它的情况
// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
if (!inVPre) {
// 是否存在 v-pre 指令,存在则设置 element.pre = true
processPre(element)
if (element.pre) {
// 存在 v-pre 指令,则设置 inVPre 为 true
inVPre = true
}
}
// 如果 pre 标签,则设置 inPre 为 true
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
// 存在 v-pre 指令,这样的节点只会渲染一次,将节点上的属性都设置到 el.attrs 数组对象中,作为静态属性,数据更新时不会渲染这部分内容
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
// 处理 v-for 属性
processFor(element)
// 处理 v-if、v-else-if、v-else
processIf(element)
// 处理 v-once 指令
processOnce(element)
}
// root 不存在,当前处理的元素为第一个元素,即组件的 根 元素
if (!root) {
root = element
if (process.env.NODE_ENV !== 'production') {
// 检查根元素,不能使用 slot 、 template、 v-for
checkRootConstraints(root)
}
}
if (!unary) {
// 非自闭合标签,通过 currentParent 记录当前元素,下一个元素在处理的时候,就知道自己的父元素是谁
currentParent = element
// 然后将 element push 到 stack 数组,将来处理到当前元素的闭合标签时用
stack.push(element)
} else {
// 说明当前元素为自闭合标签
closeElement(element)
}
},
/**
* 处理结束标签
* @param {*} tag 结束标签的名称
* @param {*} start 结束标签的开始索引
* @param {*} end 结束标签的结束索引
*/
end (tag, start, end) {
// 结束标签对应的开始标签的 AST 对象
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)
},
// 处理文本,基于文本生成 ast 对象,将该 AST 放到它的父元素的 children 中。
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
}
const children = currentParent.children
if (inPre || text.trim()) {
// 文本在 pre 标签内 或者 text.trim() 不为空
text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
} else if (!children.length) {
// 说明文本不在 pre 标签内而且 text.trim() 为空,而且当前父元素也没有子节点,则将 text 置为空
// 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 ? ' ' : ''
}
// 如果经过处理后 text 还存在
if (text) {
if (!inPre && whitespaceOption === 'condense') {
// 不在 pre 节点中,并且配置选项中存在压缩选项,则将多个连续空格压缩为单个
// condense consecutive whitespaces into single space
text = text.replace(whitespaceRE, ' ')
}
let res
// 基于 text 生成 AST 对象
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
}
}
// push 到父元素的 children 中
if (child) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
children.push(child)
}
}
},
// 处理注释节点
comment (text: string, start, end) {
// adding anything as a sibling to the root node is forbidden
// comments should still be allowed, but ignored
// 禁止将任何内容作为 root 的节点的同级进行添加,注释应该被允许,但是会被忽略
// 如果 currentParent 不存在,说明注释和 root 为同级,忽略
if (currentParent) {
// 注释节点的 AST
const child: ASTText = {
type: 3,
text,
isComment: true
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
// 记录节点的开始索引和结束索引
child.start = start
child.end = end
}
// push 到父元素的 children 中
currentParent.children.push(child)
}
}
})
return root
}
对模板的解析主要是通过 parseHTML
函数,他定义在 src/compiler/parser/html-parser
中:
// src/compiler/parser/html-parser
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
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 passed as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/
// Special Elements (can contain anything)
export const isPlainTextElement = makeMap('script,style,textarea', true)
const reCache = {}
const decodingMap = {
'<': '<',
'>': '>',
'"': '"',
'&': '&',
' ': '\n',
'	': '\t',
''': "'"
}
const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g
// #5992
const isIgnoreNewlineTag = makeMap('pre,textarea', true)
const shouldIgnoreFirstNewline = (tag, html) => tag && isIgnoreNewlineTag(tag) && html[0] === '\n'
function decodeAttr (value, shouldDecodeNewlines) {
const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr
return value.replace(re, match => decodingMap[match])
}
/**
* 通过循环遍历 html 模版字符串,依次处理其中的各个标签,以及标签上的属性
* @param {*} html html 模版
* @param {*} options 配置项
*/
export function parseHTML (html, options) {
const stack = []
const expectHTML = options.expectHTML
// 是否是自闭合标签
const isUnaryTag = options.isUnaryTag || no
// 是否可以只有开始标签
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
// 记录当前在原始 html 字符串中的开始位置
let index = 0
let last, lastTag
while (html) {
last = html
// Make sure we're not in a plaintext content element like script/style
// 确保不是在 script、style、textarea 这样的纯文本元素中
if (!lastTag || !isPlainTextElement(lastTag)) {
// 找第一个 < 字符
let textEnd = html.indexOf('<')
// textEnd === 0 说明在开头找到了
// 分别处理可能找到的注释标签、条件注释标签、Doctype、开始标签、结束标签
// 每处理完一种情况,就会截断(continue)循环,并且重置 html 字符串,将处理过的标签截掉,下一次循环处理剩余的 html 字符串模版
if (textEnd === 0) {
// Comment:
// 处理注释标签 <!-- xx -->
if (comment.test(html)) {
// 注释标签的结束索引
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
// 是否应该保留 注释
if (options.shouldKeepComment) {
// 得到:注释内容、注释的开始索引、结束索引
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
}
// 调整 html 和 index 变量
advance(commentEnd + 3)
continue
}
}
// 处理条件注释标签:<!--[if IE]>
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
if (conditionalComment.test(html)) {
// 结束位置
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
// 调整 html 和 index 变量
advance(conditionalEnd + 2)
continue
}
}
// 处理 Doctype,<!DOCTYPE html>
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
/**
* 处理开始标签和结束标签是这整个函数中的核型部分
* 这两部分就是在构造 element ast
*/
// 处理结束标签,比如 </div>
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) {
// 进一步处理上一步得到结果,并最后调用 options.start 方法
// 真正的解析工作都是在这个 start 方法中做的
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
}
let text, rest, next
if (textEnd >= 0) {
// 能走到这儿,说明虽然在 html 中匹配到到了 <xx,但是这不属于上述几种情况,
// 它就只是一个普通的一段文本:<我是文本
// 于是从 html 中找到下一个 <,直到 <xx 是上述几种情况的标签,则结束,
// 在这整个过程中一直在调整 textEnd 的值,作为 html 中下一个有效标签的开始位置
// 截取 html 模版字符串中 textEnd 之后的内容,rest = <xx
rest = html.slice(textEnd)
// 这个 while 循环就是处理 <xx 之后的纯文本情况
// 截取文本内容,并找到有效标签的开始位置(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// 则认为 < 后面的内容为纯文本,然后在这些纯文本中再次找 <
next = rest.indexOf('<', 1)
// 如果没找到 <,则直接结束循环
if (next < 0) break
// 走到这儿说明在后续的字符串中找到了 <,索引位置为 textEnd
textEnd += next
// 截取 html 字符串模版 textEnd 之后的内容赋值给 rest,继续判断之后的字符串是否存在标签
rest = html.slice(textEnd)
}
// 走到这里,说明遍历结束,有两种情况,一种是 < 之后就是一段纯文本,要不就是在后面找到了有效标签,截取文本
text = html.substring(0, textEnd)
}
// 如果 textEnd < 0,说明 html 中就没找到 <,那说明 html 就是一段文本
if (textEnd < 0) {
text = html
}
// 将文本内容从 html 模版字符串上截取掉
if (text) {
advance(text.length)
}
// 处理文本
// 基于文本生成 ast 对象,然后将该 ast 放到它的父元素的中
// 即 currentParent.children 数组中
if (options.chars && text) {
options.chars(text, index - text.length, index)
}
} else {
// 处理 script、style、textarea 标签的闭合标签
let endTagLength = 0
// 开始标签的小写形式
const stackedTag = lastTag.toLowerCase()
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
// 匹配并处理开始标签和结束标签之间的所有文本,比如 <script>xx</script>
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)
}
// 到这里就处理结束,如果 stack 数组中还有内容,则说明有标签没有被闭合,给出提示信息
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
}
}
// Clean up any remaining tags
parseEndTag()
/**
* 重置 html,html = 从索引 n 位置开始的向后的所有字符
* index 为 html 在 原始的 模版字符串 中的的开始索引,也是下一次该处理的字符的开始位置
* @param {*} n 索引
*/
function advance (n) {
index += n
html = html.substring(n)
}
/**
* 解析开始标签,比如:<div id="app">
* @returns { tagName: 'div', attrs: [[xx], ...], start: index }
*/
function parseStartTag () {
const start = html.match(startTagOpen)
if (start) {
// 处理结果
const match = {
// 标签名
tagName: start[1],
// 属性,占位符
attrs: [],
// 标签的开始位置
start: index
}
/**
* 调整 html 和 index,比如:
* html = ' id="app">'
* index = 此时的索引
* start[0] = '<div'
*/
advance(start[0].length)
let end, attr
// 处理 开始标签 内的各个属性,并将这些属性放到 match.attrs 数组中
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)
}
// 开始标签的结束,end = '>' 或 end = ' />'
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
/**
* 进一步处理开始标签的解析结果 ——— match 对象
* 处理属性 match.attrs,如果不是自闭合标签,则将标签信息放到 stack 数组,待将来处理到它的闭合标签时再将其弹出 stack,表示该标签处理完毕,这时标签的所有信息都在 element ast 对象上了
* 接下来调用 options.start 方法处理标签,并根据标签信息生成 element ast,
* 以及处理开始标签上的属性和指令,最后将 element ast 放入 stack 数组
*
* @param {*} match { tagName: 'div', attrs: [[xx], ...], start: index }
*/
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)
}
}
// 一元标签,比如 <hr />
const unary = isUnaryTag(tagName) || !!unarySlash
// 处理 match.attrs,得到 attrs = [{ name: attrName, value: attrVal, start: xx, end: xx }, ...]
// 比如 attrs = [{ name: 'id', value: 'app', start: xx, end: xx }, ...
const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
// 比如:args[3] => 'id',args[4] => '=',args[5] => 'app'
const value = args[3] || args[4] || args[5] || ''
const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines
// attrs[i] = { id: 'app' }
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
}
}
// 如果不是自闭合标签,则将标签信息放到 stack 数组中,待将来处理到它的闭合标签时再将其弹出 stack
// 如果是自闭合标签,则标签信息就没必要进入 stack 了,直接处理众多属性,将他们都设置到 element ast 对象上,就没有处理 结束标签的那一步了,这一步在处理开始标签的过程中就进行了
if (!unary) {
// 将标签信息放到 stack 数组中,{ tag, lowerCasedTag, attrs, start, end }
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
// 标识当前标签的结束标签为 tagName
lastTag = tagName
}
/**
* 调用 start 方法,主要做了以下 6 件事情:
* 1、创建 AST 对象
* 2、处理存在 v-model 指令的 input 标签,分别处理 input 为 checkbox、radio、其它的情况
* 3、处理标签上的众多指令,比如 v-pre、v-for、v-if、v-once
* 4、如果根节点 root 不存在则设置当前元素为根节点
* 5、如果当前元素为非自闭合标签则将自己 push 到 stack 数组,并记录 currentParent,在接下来处理子元素时用来告诉子元素自己的父节点是谁
* 6、如果当前元素为自闭合标签,则表示该标签要处理结束了,让自己和父元素产生关系,以及设置自己的子元素
*/
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
/**
* 解析结束标签,比如:</div>
* 最主要的事就是:
* 1、处理 stack 数组,从 stack 数组中找到当前结束标签对应的开始标签,然后调用 options.end 方法
* 2、处理完结束标签之后调整 stack 数组,保证在正常情况下 stack 数组中的最后一个元素就是下一个结束标签对应的开始标签
* 3、处理一些异常情况,比如 stack 数组最后一个元素不是当前结束标签对应的开始标签,还有就是
* br 和 p 标签单独处理
* @param {*} tagName 标签名,比如 div
* @param {*} start 结束标签的开始索引
* @param {*} end 结束标签的结束索引
*/
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
// 倒序遍历 stack 数组,找到第一个和当前结束标签相同的标签,该标签就是结束标签对应的开始标签的描述对象
// 理论上,不出异常,stack 数组中的最后一个元素就是当前结束标签的开始标签的描述对象
// 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
}
// 如果在 stack 中一直没有找到相同的标签名,则 pos 就会 < 0,进行后面的 else 分支
if (pos >= 0) {
// 这个 for 循环负责关闭 stack 数组中索引 >= pos 的所有标签
// 为什么要用一个循环,上面说到正常情况下 stack 数组的最后一个元素就是我们要找的开始标签,
// 但是有些异常情况,就是有些元素没有给提供结束标签,比如:
// stack = ['span', 'div', 'span', 'h1'],当前处理的结束标签 tagName = div
// 匹配到 div,pos = 1,那索引为 2 和 3 的两个标签(span、h1)说明就没提供结束标签
// 这个 for 循环就负责关闭 div、span 和 h1 这三个标签,
// 并在开发环境为 span 和 h1 这两个标签给出 ”未匹配到结束标签的提示”
// 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 处理正常的结束标签
options.end(stack[i].tag, start, end)
}
}
// 将刚才处理的那些标签从数组中移除,保证数组的最后一个元素就是下一个结束标签对应的开始标签
// Remove the open elements from the stack
stack.length = pos
// lastTag 记录 stack 数组中未处理的最后一个开始标签
lastTag = pos && stack[pos - 1].tag
} else if (lowerCasedTagName === 'br') {
// 当前处理的标签为 <br /> 标签
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (lowerCasedTagName === 'p') {
// 处理 <p> 标签
if (options.start) {
options.start(tagName, [], false, start, end)
}
// 处理 </p> 标签
if (options.end) {
options.end(tagName, start, end)
}
}
}
}
parseHTML
的主要逻辑就是循环解析,用正则做匹配,然后做不同的处理,在匹配的过程中利用 advance
函数不断改变索引,直到解析完毕。
通过正则可以匹配到注释标签、文档类型标签、开始标签、结束标签。通过 handleStartTag
方法解析开始标签,将非一元标签构建出来的 AST 对象推入 stack
中,通过 parseEndTag
方法对闭合标签做解析,也就是倒序的 stack
,找到第一个和当前结束标签相同的标签,该标签就是结束标签对应的开始标签的描述对象。
AST 元素节点总共有 3 种类型,type
为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。parse
的作用就是利用正则将模板字符创解析成 AST 语法树。
optimize
在模板解析之后,生成 AST 树,接下来就是对 AST 的一些优化。
optimize
// src/compiler/optimizer.js
/**
* Goal of the optimizer: walk the generated template AST tree
* and detect sub-trees that are purely static, i.e. parts of
* the DOM that never needs to change.
*
* Once we detect these sub-trees, we can:
*
* 1. Hoist them into constants, so that we no longer need to
* create fresh nodes for them on each re-render;
* 2. Completely skip them in the patching process.
*/
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
/**
* options.staticKeys = 'staticClass,staticStyle'
* isStaticKey = function(val) { return map[val] }
*/
isStaticKey = genStaticKeysCached(options.staticKeys || '')
// 平台保留标签
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
// 标记静态节点
markStatic(root)
// second pass: mark static roots.
// 标记静态根
markStaticRoots(root, false)
}
markStatic
// src/compiler/optimizer.js
function markStatic (node: ASTNode) {
// 通过 node.static 来标识节点是否为 静态节点
node.static = isStatic(node)
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
/**
* 不要将组件的插槽内容设置为静态节点,这样可以避免:
* 1、组件不能改变插槽节点
* 2、静态插槽内容在热重载时失败
*/
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
// 递归终止条件,如果节点不是平台保留标签 && 也不是 slot 标签 && 也不是内联模版,则直接结束
return
}
// 遍历子节点,递归调用 markStatic 来标记这些子节点的 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 这些指令,则依次标记 block 中节点的 static
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
}
}
}
}
}
/**
* 判断节点是否为静态节点:
* 通过自定义的 node.type 来判断,2: 表达式 => 动态,3: 文本 => 静态
* 凡是有 v-bind、v-if、v-for 等指令的都属于动态节点
* 组件为动态节点
* 父节点为含有 v-for 指令的 template 标签,则为动态节点
*/
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
// 比如:{{ msg }}
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)
))
}
markStaticRoots
// src/compiler/optimizer.js
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
// 已经是 static 的节点或者是 v-once 指令的节点
node.staticInFor = isInFor
}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
// 除了本身是一个静态节点外,必须满足拥有 children,并且 children 不能只是一个文本节点
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)
}
}
// 如果节点存在 v-if、v-else-if、v-else 指令,则为 block 节点标记静态根
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
在模板不是所有数据都是响应式的,有些数据是首次渲染之后就不会在发生变化的,通过遍历生成的模板 AST 树,对这些节点进行标记,就可以在 patch
的过程中跳过它们,从而提高对比的性能。
在 optimize
中实际上就是通过 markStatic(root)
对静态节点进行标记和 使用 markStaticRoots(root, false)
标记静态根。
generate
在生成 AST 语法树后,对 AST 进行优化,标记静态节点和静态根。最后就是通过 generate
生成代码字符串。
generate
// src/compiler/codegen/index.js
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// fix #11483, Root level <script> tags should not be rendered.
const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
state
是 CodegenState
的一个实例,在生成代码的时候会用到其中的一些属性和方法。generate
函数主要是通过 genElement
生成 code
, 然后在用 with(this){return ${code}}
将其包裹起来。
genElement
// src/compiler/codegen/index.js
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) {
// 处理带有 v-once 指令的节点
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
// 处理 v-for
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
// 处理 v-if
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))) {
// 非普通元素或者带有 v-pre 指令的组件走这里,处理节点的所有属性
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
}
}
genElement
的作用主要就是对 AST 节点上的属性使用不同方法做处理,而生成代码函数。
genStatic
// src/compiler/codegen/index.js
// hoist static sub-trees out
function genStatic (el: ASTElement, state: CodegenState): string {
// 标记当前静态节点已经被处理
el.staticProcessed = true
// Some elements (templates) need to behave differently inside of a v-pre
// node. All pre nodes are static roots, so we can use this as a location to
// wrap a state change and reset it upon exiting the pre node.
const originalPreState = state.pre
if (el.pre) {
state.pre = el.pre
}
// 将生成的代码添加到 staticRenderFns 中
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
state.pre = originalPreState
// 返回 _m 函数,state.staticRenderFns.length - 1 表示数组中的下标
return `_m(${state.staticRenderFns.length - 1
}${el.staticInFor ? ',true' : ''
})`
}
genOnce
// src/compiler/codegen/index.js
function genOnce (el: ASTElement, state: CodegenState): string {
// 标记当前节点的 v-once 指令已被处理
el.onceProcessed = true
if (el.if && !el.ifProcessed) {
// 含有 v-if 并且 V-if没有被处理,则处理 V-if 最终生成一段三元运算符的代码
return genIf(el, state)
} else if (el.staticInFor) {
// 说明当前节点是被包裹在还有 v-for 指令节点内部的静态节点
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)
}
// 生成 _o 函数
return `_o(${genElement(el, state)},${state.onceId++},${key})`
} else {
// 上面情况都不符合,说明是简单的静态节点,生成 _m 函数
return genStatic(el, state)
}
}
genFor
// src/compiler/codegen/index.js
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}` : ''
// 提示 v-for 在组件上时必须使用 key
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 */
)
}
// 标记当前节点上的 v-for 指令已被处理
el.forProcessed = true // avoid recursion
/**
* 生成 _l 函数,比如:
* v-for="(item,index) in data"
*
* _l((data), function (item, index) {
* return genElememt(el, state)
* })
*/
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
genIf
// src/compiler/codegen/index.js
export function genIf (
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
// 标记当前节点的 v-if 指令已经被处理
el.ifProcessed = true // avoid recursion
// 得到三元表达式,condition ? render1 : render2
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()'
}
// 依次从 conditions 获取第一个 condition
const condition = conditions.shift()
if (condition.exp) {
// 通过对 condition.exp 去生成一段三元运算符的代码,
// : 后是递归调用,如果有多个 conditions,就生成多层三元运算
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)
}
}
genChildren
// src/compiler/codegen/index.js
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'
) {
// 一个子节点 && 节点上有 v-for && 不是 template 标签 && 不是 slot
// 则直接调用 genElement,从而进入到 genFor
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
// 返回一个数组,数组的每个元素都是一个子节点的渲染函数,
// 格式:['_c(tag, data, children, normalizationType)', ...]
return `[${children.map(c => gen(c, state)).join(',')}]${normalizationType ? `,${normalizationType}` : ''
}`
}
}
genSlot
// src/compiler/codegen/index.js
function genSlot (el: ASTElement, state: CodegenState): string {
// 插槽名称
const slotName = el.slotName || '"default"'
// 处理子节点
const children = genChildren(el, state)
// 最终返回 _t 函数
let res = `_t(${slotName}${children ? `,function(){return ${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 + ')'
}
genProps
// src/compiler/codegen/index.js
function genProps (props: Array<ASTAttr>): string {
// 静态属性
let staticProps = ``
// 动态属性
let dynamicProps = ``
// 遍历属性数组
for (let i = 0; i < props.length; i++) {
// 属性
const prop = props[i]
// 属性值
const value = __WEEX__
? generateValue(prop.value)
: transformSpecialNewlines(prop.value)
if (prop.dynamic) {
// 动态属性,`dAttrName,dAttrVal,...`
dynamicProps += `${prop.name},${value},`
} else {
// 静态属性,'attrName,attrVal,...'
staticProps += `"${prop.name}":${value},`
}
}
// 去掉静态属性最后的逗号
staticProps = `{${staticProps.slice(0, -1)}}`
if (dynamicProps) {
// 如果存在动态属性则返回:
// _d(静态属性字符串,动态属性字符串)
return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])`
} else {
// 说明属性数组中不存在动态属性,直接返回静态属性字符串
return staticProps
}
}
genData
// src/compiler/codegen/index.js
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}",`
}
// 为节点执行模块(class、style)的 genData 方法,
// 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
// 自定义事件,如 { `on${eventName}:handleCode` } 或者 { `on_d(${eventName}:handleCode`, `${eventName},handleCode`) }
if (el.events) {
data += `${genHandlers(el.events, false)},`
}
// 带 .native 修饰符的事件,
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
}
genData
函数就是根据 AST 元素节点的属性构造出一个 data
对象字符串,这个在后面创建 VNode 的时候的时候会作为参数传入。
genComponent
// src/compiler/codegen/index.js
function genComponent (
componentName: string,
el: ASTElement,
state: CodegenState
): string {
// 所有的子节点
const children = el.inlineTemplate ? null : genChildren(el, state, true)
// 返回 `_c(compName, data, children)`
// compName 是 is 属性的值
return `_c(${componentName},${genData(el, state)}${children ? `,${children}` : ''
})`
}
举个栗子
模板
<ul :class="bindCls" class="list" v-if="isShow">
<li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>
parse 生成 AST
ast = {
'type': 1,
'tag': 'ul',
'attrsList': [],
'attrsMap': {
':class': 'bindCls',
'class': 'list',
'v-if': 'isShow'
},
'if': 'isShow',
'ifConditions': [{
'exp': 'isShow',
'block': // ul ast element
}],
'parent': undefined,
'plain': false,
'staticClass': 'list',
'classBinding': 'bindCls',
'children': [{
'type': 1,
'tag': 'li',
'attrsList': [{
'name': '@click',
'value': 'clickItem(index)'
}],
'attrsMap': {
'@click': 'clickItem(index)',
'v-for': '(item,index) in data'
},
'parent': // ul ast element
'plain': false,
'events': {
'click': {
'value': 'clickItem(index)'
}
},
'hasBindings': true,
'for': 'data',
'alias': 'item',
'iterator1': 'index',
'children': [
'type': 2,
'expression': '_s(item)+":"+_s(index)'
'text': '{{item}}:{{index}}',
'tokens': [
{ '@binding': 'item' },
':',
{ '@binding': 'index' }
]
]
}]
}
optimize 优化 AST
ast = {
'type': 1,
'tag': 'ul',
'attrsList': [],
'attrsMap': {
':class': 'bindCls',
'class': 'list',
'v-if': 'isShow'
},
'if': 'isShow',
'ifConditions': [{
'exp': 'isShow',
'block': // ul ast element
}],
'parent': undefined,
'plain': false,
'staticClass': 'list',
'classBinding': 'bindCls',
'static': false,
'staticRoot': false,
'children': [{
'type': 1,
'tag': 'li',
'attrsList': [{
'name': '@click',
'value': 'clickItem(index)'
}],
'attrsMap': {
'@click': 'clickItem(index)',
'v-for': '(item,index) in data'
},
'parent': // ul ast element
'plain': false,
'events': {
'click': {
'value': 'clickItem(index)'
}
},
'hasBindings': true,
'for': 'data',
'alias': 'item',
'iterator1': 'index',
'static': false,
'staticRoot': false,
'children': [
'type': 2,
'expression': '_s(item)+":"+_s(index)'
'text': '{{item}}:{{index}}',
'tokens': [
{ '@binding': 'item' },
':',
{ '@binding': 'index' }
],
'static': false
]
}]
}
generate 生成代码
with (this) {
return (isShow) ?
_c('ul', {
staticClass: "list",
class: bindCls
},
_l((data), function (item, index) {
return _c('li', {
on: {
"click": function ($event) {
clickItem(index)
}
}
},
[_v(_s(item) + ":" + _s(index))])
})
) : _e()
}
最终生成了许多简写函数,比如 _c
、_t
、_l
、_m
, 这些函数都定义在:
// src/core/instance/render-helpers/index.js
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
到此,Vue 的源码大体梳理完毕,在此期间查看了许多大佬的文章,感谢大佬们的无私分享,感谢友好的前端圈。
相关链接
如果觉得还凑合的话,给个赞吧!!!也可以来我的 个人博客 逛逛!