1.complie简述
complie
阶段主要是将Vue
代码通过一系列转换,最终生成对应的render
字符串,然后Vue
通过render
字符串里面的关键词或者说定义,对页面内容进行增删改,Vue
对象上有一个全局函数compile
import { compileToFunctions } from './compiler/index'
......
Vue.compile = compileToFunctions
可以看出,他是引用的./compiler/index
模块导出的compileToFunctions
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
baseOptions
,对解析模板的一些选项进行基本的定义,规定如果解析模板中的各种标签或代码
export const baseOptions: CompilerOptions = {
expectHTML: true,
modules,
directives,
isPreTag,
isUnaryTag,
mustUseProp,
canBeLeftOpenTag,
isReservedTag,
getTagNamespace,
staticKeys: genStaticKeys(modules)
}
expectHTML
:为true
时,解析HTML
的结束标签
modules
:包括klass
和style
,对模板中类和样式的解析。
directives
:这里包括model
(v-model
)、html
(v-html
)、text
(v-text
)三个指令
isPreTag
:是否是pre
标签
isUnaryTag
:是否是单标签,比如img
、input
、iframe
等
mustUseProp
:需要使用props
绑定的属性,比如value
、selected
等
canBeLeftOpenTag
:可以不闭合的标签,比如tr
、td
等
isReservedTag
:是否是保留标签,html
标签和SVG
标签
getTagNamespace
:获取命名空间,svg
和math
staticKeys
:静态关键词,包括staticClass,staticStyle
然后是createCompiler
export function createCompilerCreator (baseCompile: Function): Function {
// 接受一个基本的CompilerOptions
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
// finalOptions继承自baseOptions
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
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)
}
}
// 合并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
// 返回render字符串
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
导出的模块compileToFunctions
为createCompileToFunctionFn
执行返回的结果
createCompileToFunctionFn
代码
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// 拿到options,并且删除错误日志等数据
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn
......
// 如果有缓存,拿到缓存中的编译结果
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// 编译的结果
const compiled = compile(template, options)
......
const res = {}
const fnGenErrors = []
// render字符串
res.render = createFunction(compiled.render, fnGenErrors)
// 将render字符串中的标识符转为函数,如_c、_m
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
......
return (cache[key] = res)
}
}
2.生成ast
在src/compiler/parser/index.js
中,首先定义了各种
// 匹配@或v-on开头的属性
export const onRE = /^@|^v-on:/
// 是匹配v-或@或:开头的属性
export const dirRE = /^v-|^@|^:|^\./
// 匹配v-for中的属性值,比如item in items、(item, index) of items
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
// 是对forAliasRE中捕获的第一部分内容,进行拆解,v-for中in|of前最后可以有三个逗号分隔的参数
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
// 去掉字符串 '(item, index)' 中的左右括号
const stripParensRE = /^\(|\)$/g
// 用来匹配指令编写中的参数
const argRE = /:(.*)$/
// 匹配以字符:或字符串 v-bind: 开头的字符串
export const bindRE = /^:|^\.|^v-bind:/
parse
,里面解析的方法主要是parseHTML
,
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
warn = options.warn || baseWarn
......
parseHTML()
return root
}
// 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) {}
function advance (n) {
...
}
function parseStartTag () {
...
}
function handleStartTag (match) {
...
}
function parseEndTag (tagName, start, end) {
...
}
}
在parseHTML
的while
循环里
// advance这个方法,用于截取传入的字符串
// 过滤掉注释,doctype等
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
advance(commentEnd + 3)
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
}
// 匹配开始标签
// 传入handleStartTag处理完后,会生成一个包含标签各种属性的对象
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
continue
}
后续会调用parse
里调用parseHTML
传入的start
的方法
start () {
// 定义基本的ast结构
createASTElement()
// 解析v-pre、v-if、v-for、v-once、slot、key、ref等指令
processPre()
processFor()
......
}
处理完后会调用parse
里调用parseHTML
传入的end
的方法
1、取出stack
中的最后一个元素。
2、取出该元素的最后一个子元素。
3、如果最后一个子元素是纯文本' '
则删除,这是因为我们的模板一般都会缩进,都会有换行,所以这里是清除换行等添加的内容。
4、stack
长度减一
5、currentParent
变为栈中最后一个元素
6、 处理v-pre
或pre
的结束标签
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)
},
最后,得到一个包含名称、类型、属性、父子节点信息的一个ast
语法树,如
const element = {
type: 1,
tag: '',
attrsList: [],
attrsMap: {},
parent: {},
children: []
};
3.优化静态内容,打开src/compiler/index
,看到调用const ast = parse(template.trim(), options)
生成ast
后,调用optimize
,传入ast
,打开optimisze
源码
// 获取基本静态key的一个集合
const genStaticKeysCached = cached(genStaticKeys)
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
// 如果ast上有staticKeys选项
isStaticKey = genStaticKeysCached(options.staticKeys || '')
// 是不是平台保留tag,如HTML 标签
isPlatformReservedTag = options.isReservedTag || no
// 标记所有静态和非静态节点
markStatic(root)
// 标记静态根节点
markStaticRoots(root, false)
}
function genStaticKeys (keys: string): Function {
return makeMap(
'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,has$Slot' +
(keys ? ',' + keys : '')
)
}
markStatic
标记所有静态和非静态节点
function markStatic (node: ASTNode) {
// 判断当前节点是否为静态节点
// 静态节点包括普通元素、纯文本元素、v-pre、v-once
node.static = isStatic(node)
// 如果type为1,代表当前节点为元素节点
if (node.type === 1) {
if (
// 如果不是保留标签,即自定义标签时
!isPlatformReservedTag(node.tag) &&
// 如果不是component标签
!node.component &&
// 如果标签不是slot
node.tag !== 'slot' &&
// 不是一个内联模板容器??
node.attrsMap['inline-template'] == null
) {
// 直接return,不对子节点做处理,反之,递归对子节点做标记
return
}
......
}
}
function isStatic (node: ASTNode): boolean {
// 表达式
if (node.type === 2) {
return false
}
// text
if (node.type === 3) {
return true
}
return !!(node.pre || (
// 没有任何指令或事件绑定
!node.hasBindings &&
// 没有if 和 for 指令
!node.if && !node.for &&
// 不是内置标签,如slot
!isBuiltInTag(node.tag) &&
// 平台保留标签,如HTML
isPlatformReservedTag(node.tag) &&
// 不是template标签的直接子元素且没有包含在for循环中
!isDirectChildOfTemplateFor(node) &&
// 结点包含的属性只能有isStaticKey中指定的几个
Object.keys(node).every(isStaticKey)
))
}
markStaticRoots
标记静态根节点
function markStaticRoots (node: ASTNode, isInFor: boolean) {
// 只处理元素节点类型
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor
}
// 标记静态根节点的条件为本身static标记为true
// 并且该结点不是只有一个静态文本子节点
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
......
}
}
4.生成render字符串
在标记完静态节点之后,会调用generate
方法来生成render字符串,生成的字符串中
_c
该方法对应的是createElement
方法,顾名思义,它的含义是创建一个元素,它的第一个参数是要定义的元素标签名、第二个参数是元素上添加的属性,第三个参数是子元素数组,第四个参数是子元素数组进行归一化处理的级别。
_v
该方法是创建一个文本结点。
_s
是把一个值转换为字符串。
_m
是渲染静态内容,它接收的第一个参数是一个索引值,指向最终生成的staticRenderFns
数组中对应的内容,第二个参数是标识元素是否包裹在for
循环内。
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// 如果ast为空,直接返回一个空的div节点
// 有过有值,调用genElement
const code = ast ? genElement(ast, state) : '_c("div")'
// 返回render字符串
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
genElement
会通过节点上的标志来判断使用什么方法来生成render字符串,这里说一下genStatic
export function genElement (el: ASTElement, state: CodegenState): string {
// pre的状态用来判断是否跳过当前解析
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
}
......
else {
let data
// 如果节点没其他属性或者跳过解析,直接genData
// genData这个方法会给节点添加各种属性
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
// 如果不是内部模板,调用genChildren,返回对应的字符串
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : ''
}${
children ? `,${children}` : ''
})`
}
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
function genStatic (el: ASTElement, state: CodegenState): string {
el.staticProcessed = true
// 暂存pre
const originalPreState = state.pre
// 如果当前节点pre为true,更改pre的状态
if (el.pre) {
state.pre = el.pre
}
// 通过递归处理静态根节点及其子内容,添加到staticRenderFns
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
// 恢复pre状态
state.pre = originalPreState
return `_m(${
state.staticRenderFns.length - 1
}${
el.staticInFor ? ',true' : ''
})`
}
最后返回的结果就包含render
字符串以及staticRenderFns
静态节点