# 编译函数
# 相比vue2的优化点
最终生成的编译函数其实就是调用compiler-dom/src/index.ts里面的compile方法
export function compile(
template: string,
options: CompilerOptions = {}
): CodegenResult {
return baseCompile(template, {
...options,
...(__BROWSER__ ? parserOptionsMinimal : parserOptionsStandard),
nodeTransforms: [transformStyle, ...(options.nodeTransforms || [])],
directiveTransforms: {
cloak: transformCloak, // 根据浏览器实现特定的指令编译方法,传给核心编译函数
html: transformVHtml,
text: transformVText,
model: transformModel, // override compiler-core
on: transformOn,
...(options.directiveTransforms || {})
}
})
}
核心的编译函数compiler-core文件夹下面目录结构:
- tests 测试用例
- src/ast ts语法的大佬的类型定义,比如type,enum,interface等
- src/codegen 将生成的ast转换成render字符串
- src/errors 定义 compiler 错误类型
- src/index 入口文件,主要有一个 baseCompile ,用来编译模板文件的
- src/parse 将模板字符串转换成 AST
- src/runtimeHelper 生成code的时候的定义常量对应关系
- src/transform 处理 AST 中的 vue 特有语法,比如 v-if ,v-on 的解析
// 可以运行一个简单的例子
const source = <div id="test" :class="cls">
<span>{{ name }}</span>
<MyCom></MyCom>
</div>.trim()
import { parse } from './compiler-core.cjs'
const result = parse(source)
一个简单的转换结果就呈现出来了,从生成的结构来看,相对于vue2.x有几个比较重要的变化:
- 新增了 loc 属性 每一个节点都记录了该节点在源码当中的 start 和 end,标识了代码的详细位置,column,line,offset, vu3.0对于开发遇到的问题都要详细的日志输出也基于此,另外支持 source-map
- 新增了 tagType 属性,tagType 属性标识该节点是什么类型的。我们知道 vue2.x 判断节点类型是运行时才有的,vu3.0将 判断提前到编译阶段了,提升了性能;目前tagType有三种类型:0 element,1 component,2 slot,3 template
- 新增 isStatic 属性将模板提前编译好,标识是否为动态变化的,比如动态指令
新版的 AST 明显比 vue2.x 要复杂些,可以看到vue3.0将很多可以在编译阶段就能确定的就在编译阶段确定,标识编译结果,不 需要等到运行时再去判断,节省内存和性能。这个也是尤大大重点说了的,优化编译,提升性能,转换的代码,主要有如下几个方法:
- parse & parseChildren 主入口
- parseTag 处理标签
- parseAttribute 处理标签上的属性
- parseElement 处理起始标签
- parseInterpolation 处理动态文本内容
- parseText 处理静态文本内容
# 整体编译流程
上面说到最终的编译方法其实就是调用compiler-core/index.ts下的baseCompile方法
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
/* istanbul ignore if */
if (__BROWSER__) {
const onError = options.onError || defaultOnError
if (options.prefixIdentifiers === true) {
onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
} else if (options.mode === 'module') {
onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
}
}
// 词法解析
const ast = isString(template) ? parse(template, options) : template
const prefixIdentifiers =
!__BROWSER__ &&
(options.prefixIdentifiers === true || options.mode === 'module')
// 对这个抽象语法树进行静态节点的标记,这样就可以优化渲染过程
// 优化器的目的就是去找出 AST 中纯静态的子树:
// 把纯静态子树提升为常量,每次重新渲染的时候就不需要创建新的节点了
// 并且要翻译if on等自定义的指令
transform(ast, {
...options,
prefixIdentifiers,
nodeTransforms: [
transformIf,
transformFor,
...(prefixIdentifiers
? [
// order is important
trackVForSlotScopes,
transformExpression
]
: []),
transformSlotOutlet,
transformElement,
trackSlotScopes,
optimizeText,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: {
on: transformOn,
bind: transformBind,
once: transformOnce,
model: transformModel,
...(options.directiveTransforms || {}) // user transforms
}
})
// 根据 AST 生成一个 render 函数字符串
return generate(ast, {
...options,
prefixIdentifiers
})
}
# transform方法的实现
// 大概的简化方法
function createTransformContext(
root: RootNode,
{
prefixIdentifiers = false,
hoistStatic = false, //标记是否是静态节点
nodeTransforms = [],
directiveTransforms = {},
onError = defaultOnError
}: TransformOptions
): TransformContext {
const context: TransformContext = {
root,
helpers: new Set(),
components: new Set(),
directives: new Set(),
hoists: [],
identifiers: {},
scopes: {
vFor: 0,
vSlot: 0,
vPre: 0,
vOnce: 0
},
prefixIdentifiers,
hoistStatic,
nodeTransforms,
directiveTransforms,
onError,
parent: null,
currentNode: root,
childIndex: 0,
helper(name) { //保存编译时候需要使用的vue的方法的映射
context.helpers.add(name)
return name
},
// 当有静态节点时候内部保存变量就是_hoisted_1慢慢往后加
hoist(exp) {
context.hoists.push(exp)
return createSimpleExpression(
`_hoisted_${context.hoists.length}`,
false,
exp.loc
)
}
}
}
export function transform(root: RootNode, options: TransformOptions) {
// 创建总节点上下文
const context = createTransformContext(root, options)
// 方法内部编译所有节点
traverseNode(root, context)
if (options.hoistStatic) {
// 静态节点标记
hoistStatic(root, context)
}
finalizeRoot(root, context)
}
export function traverseNode(
node: RootNode | TemplateChildNode,
context: TransformContext
) {
// 该方法循环会触发多次,一层层往下遍历解析编译
// 获取所有编译指令的方法,如编译if on等指令
const { nodeTransforms } = context
const exitFns = []
for (let i = 0; i < nodeTransforms.length; i++) {
const onExit = nodeTransforms[i](node, context)
if (onExit) {
if (isArray(onExit)) {
exitFns.push(...onExit)
} else {
exitFns.push(onExit)
}
}
if (!context.currentNode) {
// node was removed
return
} else {
// 编译解析子children节点,在traverseChildren方法里面会赋值
node = context.currentNode
}
}
// 文本节点时候就不会走进switch方法,而且上面的exitFns为空
switch (node.type) {
case NodeTypes.COMMENT:
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.helper(CREATE_VNODE)
context.helper(COMMENT)
break
case NodeTypes.INTERPOLATION:
// no need to traverse, but we need to inject toString helper
context.helper(TO_STRING)
break
// if容器类型,进一步遍历他的分支
case NodeTypes.IF:
for (let i = 0; i < node.branches.length; i++) {
// 内部还是调用traverseNode方法
traverseChildren(node.branches[i], context)
}
break
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
traverseChildren(node, context)
break
}
// exit transforms
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}
静态节点标记的方法hoistStatic.ts
export function hoistStatic(root: RootNode, context: TransformContext) {
walk(
root.children,
context,
new Map(),
isSingleElementRoot(root, root.children[0])
)
}
function walk(
children: TemplateChildNode[],
context: TransformContext,
resultCache: Map<TemplateChildNode, boolean>,
doNotHoistNode: boolean = false
) {
for (let i = 0; i < children.length; i++) {
const child = children[i]
// only plain elements are eligible for hoisting.
// 只有标签节点才可能是静态的, 这里循环处理动态节点,保存到上下文context的hoist上
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT
) {
if (
!doNotHoistNode &&
isStaticNode(child, resultCache) && //彻底检查一个节点是否是纯静态节点
!hasDynamicKeyOrRef(child)
) {
// 如果整个节点是静止的,那么将其原始的codegenNode保存在上下文的context上
child.codegenNode = context.hoist(child.codegenNode!)
continue
} else {
// 节点可能包含动态子节点,但是他的props属性或许可以是静态的
const flag = getPatchFlag(child)
if (
(!flag ||
flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) &&
!hasDynamicKeyOrRef(child)
) {
let codegenNode = child.codegenNode as ElementCodegenNode
if (codegenNode.callee === APPLY_DIRECTIVES) {
codegenNode = codegenNode.arguments[0]
}
const props = codegenNode.arguments[1]
if (props && props !== `null`) {
codegenNode.arguments[1] = context.hoist(props)
}
}
}
}
if (child.type === NodeTypes.ELEMENT) {
walk(child.children, context, resultCache)
} else if (child.type === NodeTypes.FOR) {
// Do not hoist v-for single child because it has to be a block
walk(child.children, context, resultCache, child.children.length === 1)
} else if (child.type === NodeTypes.IF) {
for (let i = 0; i < child.branches.length; i++) {
const branchChildren = child.branches[i].children
// Do not hoist v-if single child because it has to be a block
// 有单个子节点的v-if不要做静态标注
walk(branchChildren, context, resultCache, branchChildren.length === 1)
}
}
}
}
function getPatchFlag(node: PlainElementNode): number | undefined {
// 例如:<button @click="increment" v -if= "state.show" >
// Count is: { { state.count } }
// </button>
// 则对应的flag为字符串 9 /* TEXT, PROPS */ parseInt之后为9
let codegenNode = node.codegenNode as ElementCodegenNode
if (codegenNode.callee === APPLY_DIRECTIVES) {
codegenNode = codegenNode.arguments[0]
}
const flag = codegenNode.arguments[3]
return flag ? parseInt(flag, 10) : undefined
}
# generate方法实现
export function generate(
ast: RootNode,
options: CodegenOptions = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
const {
mode,
push,
helper,
prefixIdentifiers,
indent,
deindent,
newline
} = context
const hasHelpers = ast.helpers.length > 0
const useWithBlock = !prefixIdentifiers && mode !== 'module'
// 根据最终需要的编译模式倒入不同字符串
if (mode === 'function') {
// Generate const declaration for helpers
// In prefix mode, we place the const declaration at top so it's done
// only once; But if we not prefixing, we place the declaration inside the
// with block so it doesn't incur the `in` check cost for every helper access.
if (hasHelpers) {
if (prefixIdentifiers) {
push(`const { ${ast.helpers.map(helper).join(', ')} } = Vue\n`)
} else {
// "with" mode.
// 保存独立的vue映射避免冲突
push(`const _Vue = Vue\n`)
// in "with" mode, helpers are declared inside the with block to avoid
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
// 给静态资源提供编译方法createVNode,因为它被从function抽离了出来需要单独提供
if (ast.hoists.length) {
push(`const _${helperNameMap[CREATE_VNODE]} = Vue.createVNode\n`)
if (ast.helpers.includes(COMMENT)) {
push(`const _${helperNameMap[COMMENT]} = Vue.Comment\n`)
}
}
}
}
genHoists(ast.hoists, context)
newline()
push(`return `)
} else {
// 支持模块化使用的是import export
if (hasHelpers) {
push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
}
genHoists(ast.hoists, context)
newline()
push(`export default `)
}
// 打入渲染函数
push(`function render() {`)
indent()
if (useWithBlock) {
push(`with (this) {`)
indent()
// function mode const declarations should be inside with block
// also they should be renamed to avoid collision with user properties
if (hasHelpers) {
push(
`const { ${ast.helpers
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')} } = _Vue`
)
newline()
newline()
}
} else {
push(`const _ctx = this`)
newline()
}
// generate asset resolution statements
if (ast.components.length) {
genAssets(ast.components, 'component', context)
}
if (ast.directives.length) {
genAssets(ast.directives, 'directive', context)
}
if (ast.components.length || ast.directives.length) {
newline()
}
// 解析整个vnode树表达式,生成最终的运行方法
push(`return `)
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
} else {
push(`null`)
}
if (useWithBlock) {
deindent()
push(`}`)
}
deindent()
push(`}`)
return {
ast,
code: context.code,
map: context.map ? context.map.toJSON() : undefined
}
}