compiler-core核心原理
1.怎么使用 compiler-core
import { baseCompile } from '@vue/compiler-core';
const templates = `<div @click="handleClick">Hello, {{ name }}!</div>`;
const result = baseCompile(templates, { prefixIdentifiers: true});
console.log(result)
运行之后的结果是一个对象,属性 ast 表示抽象语法树,属性 code 表示根据 ast 生成出来的可执行代码也就是 render 函数。preamble 和 map 暂时用不到。
{
ast: {
type: 0,
source: '<div @click="handleClick">Hello, {{ name }}!</div>',
children: [ [Object] ],
helpers: Set(3) {
Symbol(toDisplayString),
Symbol(openBlock),
Symbol(createElementBlock)
},
components: [],
directives: [],
hoists: [],
imports: [],
cached: [],
temps: 0,
codegenNode: {
type: 13,
tag: '"div"',
props: [Object],
children: [Object],
patchFlag: 9,
dynamicProps: '["onClick"]',
directives: undefined,
isBlock: true,
disableTracking: false,
isComponent: false,
loc: [Object]
},
loc: {
start: [Object],
end: [Object],
source: '<div @click="handleClick">Hello, {{ name }}!</div>'
},
transformed: true,
filters: []
},
code: 'const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue\n' +
'\n' +
'return function render(_ctx, _cache) {\n' +
' return (_openBlock(), _createElementBlock("div", { onClick: _ctx.handleClick }, "Hello, " + _toDisplayString(_ctx.name) + "!", 9 /* TEXT, PROPS */, ["onClick"]))\n' +
'}',
preamble: '',
map: undefined
}
- 生成的 render 函数能用来干嘛?
import * as Vue from 'vue';
const render = new Function('Vue', result.code)(Vue);
const app = createApp({
setup(props, { expose }) {
const name = ref('world');
const handleClick = () => {
name.value = 'hello'
};
return () => {
return render({ name, handleClick })
}
}
});
app.mount('#app')
了解 baseCompile 编译原理
源码位置:https://github.com/vuejs/core/blob/main/packages/compiler-core/src/compile.ts
export function baseCompile(
source: string | RootNode,
options: CompilerOptions = {},
): CodegenResult {
const resolvedOptions = extend({}, options, {
prefixIdentifiers,
})
const ast = isString(source) ? baseParse(source, resolvedOptions) : source
const [nodeTransforms, directiveTransforms] =
getBaseTransformPreset(prefixIdentifiers)
transform(
ast,
extend({}, resolvedOptions, {
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []), // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {}, // user transforms
),
}),
)
return generate(ast, resolvedOptions)
}
步骤1,通过 baseParse 函数是把 HTML 字符串处理为ast语法树,但是这个时候没有处理指令语法。
步骤2,通过 transform 函数遍历 ast 上的每个节点,处理节点上的指令信息。
这个地方有点是vue3比较好的设计思路,把生成ast和处理ast的逻辑分开了,框架维护起来也方便。这个思路根 babel 很类似。
Babel 的遍历实现
Babel 的遍历是通过 @babel/traverse 模块实现的。这个模块提供了一个 traverse 函数,用于递归地遍历 AST,并在每个节点上调用相应的访问者方法。
使用 traverse 函数
const babel = require("@babel/core");
const traverse = require("@babel/traverse").default;
const code = `function square(n) { return n * n; }`;
const ast = babel.parseSync(code);
traverse(ast, {
enter(path) {
console.log(path.node.type);
}
});
了解 NodeTransform 转换规则
vue 的每个指令都是一个 NodeTransform,比如下面的 transformOnce 处理的是 v-once
export function getBaseTransformPreset(
prefixIdentifiers?: boolean,
): TransformPreset {
return [
[
transformOnce,
transformIf,
transformMemo,
transformFor,
...(__COMPAT__ ? [transformFilter] : []),
...(!__BROWSER__ && prefixIdentifiers
? [
// order is important
trackVForSlotScopes,
transformExpression,
]
: __BROWSER__ && __DEV__
? [transformExpression]
: []),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText,
],
{
on: transformOn,
bind: transformBind,
model: transformModel,
},
]
}
了解 v-once 的转换原理
const seen = new WeakSet()
export const transformOnce: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
if (seen.has(node) || context.inVOnce || context.inSSR) {
return
}
seen.add(node)
context.inVOnce = true
context.helper(SET_BLOCK_TRACKING)
return () => {
context.inVOnce = false
const cur = context.currentNode as ElementNode | IfNode | ForNode
if (cur.codegenNode) {
cur.codegenNode = context.cache(
cur.codegenNode,
true /* isVNode */,
true /* inVOnce */,
)
}
}
}
}