本文为原创文章,未获授权禁止转载,侵权必究!
本篇是 Vue3 源码解析系列第 21 篇,关注专栏
前言
前面我们分别分析了 compiler
编译器中 ast
对象、transform
函数,接下来我们就来分析下编译器的最后一部分 generate
函数,该函数主要将 AST
转换为 render
函数。
案例
首先引入 compile
、h
、render
三个函数,声明了一个 template
模板,通过 compile
函数将模板编译成 render
函数,之后创建一个 component
组件,再通过 h
函数生成 vnode
对象,最后执行 render
函数进行渲染。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { compile, h, render } = Vue
const template = `<div>hello world</div>`
const renderFn = compile(template)
const component = {
render: renderFn
}
const vnode = h(component)
render(vnode, document.querySelector('#app'))
</script>
</body>
</html>
Generate 函数
generate
函数定义在 packages/compiler-core/src/codegen.ts
文件中:
export function generate(
ast: RootNode,
options: CodegenOptions & {
onContextCreated?: (context: CodegenContext) => void
} = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
if (options.onContextCreated) options.onContextCreated(context)
const {
mode,
push,
prefixIdentifiers,
indent,
deindent,
newline,
scopeId,
ssr
} = context
const hasHelpers = ast.helpers.length > 0
const useWithBlock = !prefixIdentifiers && mode !== 'module'
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
const isSetupInlined = !__BROWSER__ && !!options.inline
// preambles
// in setup() inline mode, the preamble is generated in a sub context
// and returned separately.
const preambleContext = isSetupInlined
? createCodegenContext(ast, options)
: context
if (!__BROWSER__ && mode === 'module') {
genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
} else {
genFunctionPreamble(ast, preambleContext)
}
// enter render function
const functionName = ssr ? `ssrRender` : `render`
const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
if (!__BROWSER__ && options.bindingMetadata && !options.inline) {
// binding optimization args
args.push('$props', '$setup', '$data', '$options')
}
const signature =
!__BROWSER__ && options.isTS
? args.map(arg => `${arg}: any`).join(',')
: args.join(', ')
if (isSetupInlined) {
push(`(${signature}) => {`)
} else {
push(`function ${functionName}(${signature}) {`)
}
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(aliasHelper).join(', ')} } = _Vue`)
push(`\n`)
newline()
}
}
// generate asset resolution statements
if (ast.components.length) {
genAssets(ast.components, 'component', context)
if (ast.directives.length || ast.temps > 0) {
newline()
}
}
if (ast.directives.length) {
genAssets(ast.directives, 'directive', context)
if (ast.temps > 0) {
newline()
}
}
if (__COMPAT__ && ast.filters && ast.filters.length) {
newline()
genAssets(ast.filters, 'filter', context)
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()
}
// generate the VNode tree expression
if (!ssr) {
push(`return `)
}
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
} else {
push(`null`)
}
if (useWithBlock) {
deindent()
push(`}`)
}
deindent()
push(`}`)
return {
ast,
code: context.code,
preamble: isSetupInlined ? preambleContext.code : ``,
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined
}
}
我们可以发现 generate
函数最终返回的是一个对象,但是 compile
函数最终会返回一个函数,这是怎么回事呢?
Vue
其实会将返回对象中 code
值创建一个函数 new Function(code)()
,即 render
函数,该逻辑定义在 packages/vue-compat/src/index.ts
文件中:
function compileToFunction(
template: string | HTMLElement,
options?: CompilerOptions
): RenderFunction {
// 省略
const { code } = compile(
template,
extend(
{
hoistStatic: true,
whitespace: 'preserve',
onError: __DEV__ ? onError : undefined,
onWarn: __DEV__ ? e => onError(e, true) : NOOP
} as CompilerOptions,
options
)
)
// 省略
// The wildcard import results in a huge object with every export
// with keys that cannot be mangled, and can be quite heavy size-wise.
// In the global build we know `Vue` is available globally so we can avoid
// the wildcard object.
const render = (
__GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
) as RenderFunction
// mark the function as runtime compiled
;(render as InternalRenderFunction)._rc = true
return (compileCache[key] = render)
}
registerRuntimeCompiler(compileToFunction)
const Vue = createCompatVue()
Vue.compile = compileToFunction
export default Vue
我们再看下 code
属性的值:
所以,函数
本质上是一段 字符串
,而函数的生成主要是对 字符串的拼接方式
以及 字符串的拼接格式
的处理。
通过观察,该函数大致分为四部分:
- 函数的前置代码:
const _Vue = vue
- 函数名:
function render
- 函数参数:
_ctx, _cache
- 函数体:
const...return...
之后通过 push
函数进行字符串拼接,而对于格式化,比如 缩进
、换行
等都是定义了不同方法来处理。了解完这些,我们再回过来看下 generate
函数执行逻辑。
依旧同前面一样,通过 createCodegenContext
方法创建 generate
函数的上下文对象 context
:
function createCodegenContext(
ast: RootNode,
{
mode = 'function',
prefixIdentifiers = mode === 'module',
sourceMap = false,
filename = `template.vue.html`,
scopeId = null,
optimizeImports = false,
runtimeGlobalName = `Vue`,
runtimeModuleName = `vue`,
ssrRuntimeModuleName = 'vue/server-renderer',
ssr = false,
isTS = false,
inSSR = false
}: CodegenOptions
): CodegenContext {
const context: CodegenContext = {
mode,
prefixIdentifiers,
sourceMap,
filename,
scopeId,
optimizeImports,
runtimeGlobalName,
runtimeModuleName,
ssrRuntimeModuleName,
ssr,
isTS,
inSSR,
source: ast.loc.source,
code: ``,
column: 1,
line: 1,
offset: 0,
indentLevel: 0,
pure: false,
map: undefined,
helper(key) {
return `_${helperNameMap[key]}`
},
push(code, node) {
context.code += code
if (!__BROWSER__ && context.map) {
if (node) {
let name
if (node.type === NodeTypes.SIMPLE_EXPRESSION && !node.isStatic) {
const content = node.content.replace(/^_ctx\./, '')
if (content !== node.content && isSimpleIdentifier(content)) {
name = content
}
}
addMapping(node.loc.start, name)
}
advancePositionWithMutation(context, code)
if (node && node.loc !== locStub) {
addMapping(node.loc.end)
}
}
},
indent() {
newline(++context.indentLevel)
},
deindent(withoutNewLine = false) {
if (withoutNewLine) {
--context.indentLevel
} else {
newline(--context.indentLevel)
}
},
newline() {
newline(context.indentLevel)
}
}
function newline(n: number) {
context.push('\n' + ` `.repeat(n))
}
function addMapping(loc: Position, name?: string) {
context.map!.addMapping({
name,
source: context.filename,
original: {
line: loc.line,
column: loc.column - 1 // source-map column is 0 based
},
generated: {
line: context.line,
column: context.column - 1
}
})
}
if (!__BROWSER__ && sourceMap) {
// lazy require source-map implementation, only in non-browser builds
context.map = new SourceMapGenerator()
context.map!.setSourceContent(filename, context.source)
}
return context
}
该对象中包含了 push 拼接
、indent 缩进
、deindent 回退
、newline 换行
等方法,之后执行 genFunctionPreamble
函数:
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const {
ssr,
prefixIdentifiers,
push,
newline,
runtimeModuleName,
runtimeGlobalName,
ssrRuntimeModuleName
} = context
const VueBinding =
!__BROWSER__ && ssr
? `require(${JSON.stringify(runtimeModuleName)})`
: runtimeGlobalName
// 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 (ast.helpers.length > 0) {
if (!__BROWSER__ && prefixIdentifiers) {
push(
`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`
)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = ${VueBinding}\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.
if (ast.hoists.length) {
const staticHelpers = [
CREATE_VNODE,
CREATE_ELEMENT_VNODE,
CREATE_COMMENT,
CREATE_TEXT,
CREATE_STATIC
]
.filter(helper => ast.helpers.includes(helper))
.map(aliasHelper)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
}
}
}
// generate variables for ssr helpers
if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
// ssr guarantees prefixIdentifier: true
push(
`const { ${ast.ssrHelpers
.map(aliasHelper)
.join(', ')} } = require("${ssrRuntimeModuleName}")\n`
)
}
genHoists(ast.hoists, context)
newline()
push(`return `)
}
执行完 push
方法,函数的前置代码就拼接完成:
之后再执行 newline
换行,再拼接 return
,此时 code
:
跳出该函数,继续执行后续逻辑。接着处理 函数名
和 参数
,拼接完,此时 code
:
接着执行 indent
缩进方法,然后执行到 push(const { ${ast.helpers.map(aliasHelper).join(', ')} } = _Vue)
该逻辑,我们知道 helpers
是存放转换的方法名:
而 aliasHelper
方法会将方法名转换为字符串,即 openBlock: _openBlock
:
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
所以上述逻辑执行完,此时 code
:
之后执行到 if(ast.codegenNode)
,当前根节点存在 codegenNode
,执行 genNode
方法:
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
if (isString(node)) {
context.push(node)
return
}
if (isSymbol(node)) {
context.push(context.helper(node))
return
}
switch (node.type) {
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:
__DEV__ &&
assert(
node.codegenNode != null,
`Codegen node is missing for element/if/for node. ` +
`Apply appropriate transforms first.`
)
genNode(node.codegenNode!, context)
break
case NodeTypes.TEXT:
genText(node, context)
break
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context)
break
case NodeTypes.INTERPOLATION:
genInterpolation(node, context)
break
case NodeTypes.TEXT_CALL:
genNode(node.codegenNode, context)
break
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context)
break
case NodeTypes.COMMENT:
genComment(node, context)
break
case NodeTypes.VNODE_CALL:
genVNodeCall(node, context)
break
case NodeTypes.JS_CALL_EXPRESSION:
genCallExpression(node, context)
break
case NodeTypes.JS_OBJECT_EXPRESSION:
genObjectExpression(node, context)
break
case NodeTypes.JS_ARRAY_EXPRESSION:
genArrayExpression(node, context)
break
case NodeTypes.JS_FUNCTION_EXPRESSION:
genFunctionExpression(node, context)
break
case NodeTypes.JS_CONDITIONAL_EXPRESSION:
genConditionalExpression(node, context)
break
case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context)
break
case NodeTypes.JS_BLOCK_STATEMENT:
genNodeList(node.body, context, true, false)
break
// SSR only types
case NodeTypes.JS_TEMPLATE_LITERAL:
!__BROWSER__ && genTemplateLiteral(node, context)
break
case NodeTypes.JS_IF_STATEMENT:
!__BROWSER__ && genIfStatement(node, context)
break
case NodeTypes.JS_ASSIGNMENT_EXPRESSION:
!__BROWSER__ && genAssignmentExpression(node, context)
break
case NodeTypes.JS_SEQUENCE_EXPRESSION:
!__BROWSER__ && genSequenceExpression(node, context)
break
case NodeTypes.JS_RETURN_STATEMENT:
!__BROWSER__ && genReturnStatement(node, context)
break
/* istanbul ignore next */
case NodeTypes.IF_BRANCH:
// noop
break
default:
if (__DEV__) {
assert(false, `unhandled codegen node type: ${(node as any).type}`)
// make sure we exhaust all possible types
const exhaustiveCheck: never = node
return exhaustiveCheck
}
}
}
根据逻辑,执行 genVNodeCall
方法:
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const { push, helper, pure } = context
const {
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking,
isComponent
} = node
if (directives) {
push(helper(WITH_DIRECTIVES) + `(`)
}
if (isBlock) {
push(`(${helper(OPEN_BLOCK)}(${disableTracking ? `true` : ``}), `)
}
if (pure) {
push(PURE_ANNOTATION)
}
const callHelper: symbol = isBlock
? getVNodeBlockHelper(context.inSSR, isComponent)
: getVNodeHelper(context.inSSR, isComponent)
push(helper(callHelper) + `(`, node)
genNodeList(
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
context
)
push(`)`)
if (isBlock) {
push(`)`)
}
if (directives) {
push(`, `)
genNode(directives, context)
push(`)`)
}
}
通过 helper
获取到对应的方法名,此时拼接完 code
为:
接着再执行 genNodeList
方法:
function genNodeList(
nodes: (string | symbol | CodegenNode | TemplateChildNode[])[],
context: CodegenContext,
multilines: boolean = false,
comma: boolean = true
) {
const { push, newline } = context
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (isString(node)) {
push(node)
} else if (isArray(node)) {
genNodeListAsArray(node, context)
} else {
genNode(node, context)
}
if (i < nodes.length - 1) {
if (multilines) {
comma && push(',')
newline()
} else {
comma && push(', ')
}
}
}
}
第一个参数通过 genNullableArgs
方法生成参数:
function genNullableArgs(args: any[]): CallExpression['arguments'] {
let i = args.length
while (i--) {
if (args[i] != null) break
}
return args.slice(0, i + 1).map(arg => arg || `null`)
}
该方法最终返回结果:
之后执行遍历,如果为字符串直接进行拼接否则执行 genNodeListAsArray
或 genNode
方法,所有拼接完后,最终 code
值为:
至此 generate
函数执行完毕,也代表着 compile
函数执行完成。
总结
- 编译器的处理过程分成了三部分:
ast
、transform
、generate
。 - 第一步,通过
baseParse
函数将模板template
转换为AST
。在这一步中我们使用了有限状态自动机解析模板得到了tokens
,通过扫描tokens
最终得到了AST
。 - 第二步,通过
transform
函数将AST
转换为Javascript AST
。这一步是为了最终生成render
函数做准备,利用了深度优先方式,进行了自下向上的逐层转化。 - 第三步,通过
generate
函数生成render
函数。这一步是最后的解析环节,我们需要对Javascript AST
处理,最终得到render
函数。
Vue3 源码实现
Vue3 源码解析系列
- Vue3源码解析之 源码调试
- Vue3源码解析之 reactive
- Vue3源码解析之 ref
- Vue3源码解析之 computed
- Vue3源码解析之 watch
- Vue3源码解析之 runtime
- Vue3源码解析之 h
- Vue3源码解析之 render(一)
- Vue3源码解析之 render(二)
- Vue3源码解析之 render(三)
- Vue3源码解析之 render(四)
- Vue3源码解析之 render component(一)
- Vue3源码解析之 render component(二)
- Vue3源码解析之 render component(三)
- Vue3源码解析之 render component(四)
- Vue3源码解析之 render component(五)
- Vue3源码解析之 diff(一)
- Vue3源码解析之 diff(二)
- Vue3源码解析之 compiler(一)
- Vue3源码解析之 compiler(二)
- Vue3源码解析之 compiler(三)
- Vue3源码解析之 createApp