我们了解了在vue-loader对.vue文件进行处理的过程中
在vue-loader中:
- 使用了
compiler-sfc
的parse
对.vue转换成import形式。 - 使用
compileTemplate
转换template
为render函数。 - 使用
compileStyle
处理style
中socpe。
所以下来我们来了解下compileTemplate
处理的大致流程,以便我们后期深入到vue运行机制。
compileTemplate如何转化
我们跟踪调层层调用
最终是
compileTemplate -->
@vue/compiler-dom
的compile
-->
compiler-core
的baseCompile
所以我们直接来看baseCompile
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
const onError = options.onError || defaultOnError
const isModuleMode = options.mode === 'module'
...
// 对tempalet转化成ast。
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
prefixIdentifiers
)
// 对ast进行node转换,此处预感比较重要待补充。
transform(ast, {
...options,
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: {
...directiveTransforms,
...(options.directiveTransforms || {}) // user transforms
}
})
// 将ast 转换成render函数
return generate(ast, {
...options,
prefixIdentifiers
})
}
baseParse生成AST
这个就不分析源码了,太累了,我们直接几个简单的测试就行
// <template><div></div id=""></template>
exports[`compiler: parse Errors END_TAG_WITH_ATTRIBUTES <template><div></div id=""></template> 1`] = `
Object {
"cached": 0,
"children": Array [
...//省略
],
"codegenNode": undefined,
"components": Array [],
"directives": Array [],
"helpers": Array [],
"hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"column": 39,
"line": 1,
"offset": 38,
},
"source": "<template><div></div id=\\"\\"></template>",
"start": Object {
"column": 1,
"line": 1,
"offset": 0,
},
},
"temps": 0,
"type": 0,
}
`;
通过上面可以看到会解析成如上格式的Root
接下来我们可以看到对于template里面的children解析
test('simple div', () => {
const ast = baseParse('<div>hello</div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT, //Vnode类型
ns: Namespaces.HTML,
tag: 'div',
tagType: ElementTypes.ELEMENT,
codegenNode: undefined,
props: [],
isSelfClosing: false,
children: [
{
type: NodeTypes.TEXT,
content: 'hello',
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 10, line: 1, column: 11 },
source: 'hello'
}
}
],
loc: {//对应的代码位置,后期map处理
start: { offset: 0, line: 1, column: 1 },
end: { offset: 16, line: 1, column: 17 },
source: '<div>hello</div>'
}
})
})
如上我们知道他就是生成了VNode(虚拟dom),在这个过程中对vue的语法如,@/v-for/v-model并没有做处理,只是将其转化为props
下面才是对这些进行处理的
transform做了哪些处理
transform对vue的模版语法进行了处理,继续看测试:
以v-on举例:
test('basic', () => {
const { node } = parseWithVOn(`<div v-on:click="onClick"/>`)
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: {
content: `onClick`,
isStatic: true,
loc: {
start: {
line: 1,
column: 11
},
end: {
line: 1,
column: 16
}
}
},
value: {
content: `onClick`,
isStatic: false,
loc: {
start: {
line: 1,
column: 18
},
end: {
line: 1,
column: 25
}
}
}
}
]
})
})
generate做了什么
上面已经将vue的模版语法处理为浏览器支持的标准属性形式。
generate就是将ast转化成render方法。
export function generate(
ast: RootNode,
options: CodegenOptions = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
const {
mode,
push,
prefixIdentifiers,
indent,
deindent,
newline,
scopeId,
ssr
} = context
...
// 这里加入import的各种下面需要的方法,如:createVNode
if (!__BROWSER__ && mode === 'module') {
genModulePreamble(ast, context, genScopeId)
} else {
genFunctionPreamble(ast, context)
}
// 建立render
if (genScopeId && !ssr) {
push(`const render = _withId(`)
}
if (!ssr) {
push(`function render(_ctx, _cache) {`)
} else {
push(`function ssrRender(_ctx, _push, _parent) {`)
}
indent()
// 生成
if (useWithBlock) {
push(`with (_ctx) {`)
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`
)
push(`\n`)
newline()
}
}
//生成component的引用
if (ast.components.length) {
genAssets(ast.components, 'component', context)
if (ast.directives.length || ast.temps > 0) {
newline()
}
}
//生成directives的引用
if (ast.directives.length) {
genAssets(ast.directives, 'directive', context)
if (ast.temps > 0) {
newline()
}
}
if (ast.temps > 0) {
push(`let `)
for (let i = 0; i < ast.temps; i++) {
push(`${i > 0 ? `, ` : ``}_temp${i}`)
}
}
if (ast.components.length || ast.directives.length || ast.temps) {
push(`\n`)
newline()
}
//生成VNode的code,通过解析VNode生成调用createVNode形式的js逻辑。
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
} else {
push(`null`)
}
if (useWithBlock) {
deindent()
push(`}`)
}
deindent()
push(`}`)
if (genScopeId && !ssr) {
push(`)`)
}
return {
ast,
code: context.code, //将编译后结果返回。
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined
}
}
generate转化的步骤:
- 生成import
渲染所需要的方法
代码 - 生成
render
代码
2.1 生成引入components/directives
的代码
2.2 通过解析VNode生成调用createVNode形式的js创建dom逻辑
。
3.返回以上生成的code。
生成如下形式的code
image.png
总结
- 将tempalet内容转化成ast。
- 对ast中vue的模板语法进行转换。
- 对ast生成
render
形式的code
至此,我们基本已经搞定了,.vue文件到最终生成的js过程。后面我们将继续开始了解,vue如何运行起来,render函数在什么时候调用,以及常说的diff在什么时候触发。