前言#
昨天我们分析了setup如何处理props等,今天我们接着往下分析。
坏蛋Dan:vue/compiler-sfc源码分析学习--part2:如何处理script--day4
感兴趣的大佬可以看下
正文#
今天是分析如何处理script的最后一天了,加油!
生成setup的返回语句#
// 10. generate return statement
let returned
if (!options.inlineTemplate || (!sfc.template && hasDefaultExportRender)) {
// non-inline mode, or has manual render in normal <script>
// return bindings from script and script setup
const allBindings: Record<string, any> = {
...scriptBindings,
...setupBindings
}
for (const key in userImports) {
if (!userImports[key].isType && userImports[key].isUsedInTemplate) {
allBindings[key] = true
}
}
returned = `{ ${Object.keys(allBindings).join(', ')} }`
} else {
// inline mode
if (sfc.template && !sfc.template.src) {
if (options.templateOptions && options.templateOptions.ssr) {
hasInlinedSsrRenderFn = true
}
// inline render function mode - we are going to compile the template and
// inline it right here
const { code, ast, preamble, tips, errors } = compileTemplate({
filename,
source: sfc.template.content,
inMap: sfc.template.map,
...options.templateOptions,
id: scopeId,
scoped: sfc.styles.some(s => s.scoped),
isProd: options.isProd,
ssrCssVars: sfc.cssVars,
compilerOptions: {
...(options.templateOptions &&
options.templateOptions.compilerOptions),
inline: true,
isTS,
bindingMetadata
}
})
if (tips.length) {
tips.forEach(warnOnce)
}
const err = errors[0]
if (typeof err === 'string') {
throw new Error(err)
} else if (err) {
if (err.loc) {
err.message +=
`\n\n` +
sfc.filename +
'\n' +
generateCodeFrame(
source,
err.loc.start.offset,
err.loc.end.offset
) +
`\n`
}
throw err
}
if (preamble) {
s.prepend(preamble)
}
// avoid duplicated unref import
// as this may get injected by the render function preamble OR the
// css vars codegen
if (ast && ast.helpers.includes(UNREF)) {
helperImports.delete('unref')
}
returned = code
} else {
returned = `() => {}`
}
}
if (!options.inlineTemplate && !__TEST__) {
// in non-inline mode, the `__isScriptSetup: true` flag is used by
// componentPublicInstance proxy to allow properties that start with $ or _
s.appendRight(
endOffset,
`\nconst __returned__ = ${returned}\n` +
`Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })\n` +
`return __returned__` +
`\n}\n\n`
)
} else {
s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
} allBindings: 俩script block数据的集合。hasDefaultExportRender: 是否有render函数,比如这种{ render: (h) => {} }。compileTemplate: 来自./compileTemplate.ts,看名字就知道是用来处理template模板的。generateCodeFrame: 生成错误提示语的,比如下划线标记之类的,这个就不看代码了,无关紧要。unref[1]:前面应该漏了这玩意儿,它其实是一种语法糖,是val = isRef(val) ? val.value : val的简写。
这一大段其实做的事情很简单,就是判断你是使用template还是inlineTemplate还是使用的render函数。
由于我们用的是常规的template,所以这里只会走第一部分的逻辑。

可以看到returned实际上就是我们的变量、函数、类、导入的方法等。不过有一点需要注意,那就是defineProps、defineEmits以及defineExpose的都不在这里面。至于他们会在哪,后面会说到。

至于inline或者render函数方式,我们有空再分析。
组转export default#
// 11. finalize default export
let runtimeOptions = ``
if (!hasDefaultExportName && filename && filename !== DEFAULT_FILENAME) {
const match = filename.match(/([^/\\]+)\.\w+$/)
if (match) {
runtimeOptions += `\n __name: '${match[1]}',`
}
}
if (hasInlinedSsrRenderFn) {
runtimeOptions += `\n __ssrInlineRender: true,`
}
if (propsRuntimeDecl) {
let declCode = scriptSetup.content
.slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
.trim()
if (propsDestructureDecl) {
const defaults: string[] = []
for (const key in propsDestructuredBindings) {
const d = genDestructuredDefaultValue(key)
if (d) defaults.push(`${key}: ${d}`)
}
if (defaults.length) {
declCode = `${helper(
`mergeDefaults`
)}(${declCode}, {\n ${defaults.join(',\n ')}\n})`
}
}
runtimeOptions += `\n props: ${declCode},`
} else if (propsTypeDecl) {
runtimeOptions += genRuntimeProps(typeDeclaredProps)
}
if (emitsRuntimeDecl) {
runtimeOptions += `\n emits: ${scriptSetup.content
.slice(emitsRuntimeDecl.start!, emitsRuntimeDecl.end!)
.trim()},`
} else if (emitsTypeDecl) {
runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
}
// <script setup> components are closed by default. If the user did not
// explicitly call `defineExpose`, call expose() with no args.
const exposeCall =
hasDefineExposeCall || options.inlineTemplate ? `` : ` expose();\n`
// wrap setup code with function.
if (isTS) {
// for TS, make sure the exported type is still valid type with
// correct props information
// we have to use object spread for types to be merged properly
// user's TS setting should compile it down to proper targets
// export default defineComponent({ ...__default__, ... })
const def = defaultExport ? `\n ...${DEFAULT_VAR},` : ``
s.prependLeft(
startOffset,
`\nexport default /*#__PURE__*/${helper(
`defineComponent`
)}({${def}${runtimeOptions}\n ${
hasAwait ? `async ` : ``
}setup(${args}) {\n${exposeCall}`
)
s.appendRight(endOffset, `})`)
} else {
if (defaultExport) {
// without TS, can't rely on rest spread, so we use Object.assign
// export default Object.assign(__default__, { ... })
s.prependLeft(
startOffset,
`\nexport default /*#__PURE__*/Object.assign(${DEFAULT_VAR}, {${runtimeOptions}\n ` +
`${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`
)
s.appendRight(endOffset, `})`)
} else {
s.prependLeft(
startOffset,
`\nexport default {${runtimeOptions}\n ` +
`${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`
)
s.appendRight(endOffset, `}`)
}
} hasDefaultExportName: 之前分析non script block的时候有说到过,这里就是判断你export的对象中是否有name字段。为啥这里一定要有个name呢?因为方便vue-tool在浏览器生成节点时命名节点。hasInlinedSsrRenderFn: 和ssr相关的,是否有ssr的render函数,我们不会分析ssr相关的,所以这里忽略。propsRuntimeDecl: 老朋友了,当然如果忘了这里再说下,就是你传入defineProps中的对象。propsDestructureDecl: 也是老朋友了,props解构赋值的那个节点。propsDestructuredBindings:节点转换成的数据

genDestructuredDefaultValue: 看名字就知道是用来获取解构赋值中的默认值的。
function genDestructuredDefaultValue(key: string): string | undefined {
const destructured = propsDestructuredBindings[key]
if (destructured && destructured.default) {
const value = scriptSetup!.content.slice(
destructured.default.start!,
destructured.default.end!
)
const isLiteral = destructured.default.type.endsWith('Literal')
return isLiteral ? value : `() => (${value})`
}
} helper: 前面也说过了,将runtime辅助函数存储到helperImport中。propsTypeDecl:defineProps使用only-type场景的类型定义。genRuntimeProps: 代码就不看了,简单的说下做了什么,里面会根据你的类型定义转化成runtime的props,毕竟浏览器不认识ts。另外还有个点我把代码贴出来

除了是boolean类型或者带有默认值的函数类型需要保留之外,其余的都会移除类型定义。
emitsRuntimeDecl: 同props。emitsTypeDecl: 同上。hasDefineExposeCall: 前面好像没说过?这个是判断是否用了defineExpose的,会被转换成export default对象中的expose()方法。defaultExport: 默认导出的,比如export { xx as default } from 'xx.js'。hasAwait: 是否有直接在setup block中使用await的,这个也是之前文章中提到好几次的,这里就不说如何处理这个场景的了,总之是允许你在setup语法糖下直接await而不需要async的。
这一大段看完代码估计大家都知道是在干啥了吧?
将之前的数据组装成非setup语法糖的样子,不多说,直接看数据。另外有个点就是之前的__default__,这里面包括non setup block的export以及别的包导入的比如export { x as default } from 'xx',最终都会合并到一起再export default导出。

而最终转换的script代码由于太长,这里就直接以代码的形式贴出来。
import { c as __default__ } from "d.js";
import { a as _a, e } from "b.js";
import c from "d.js";
export { a as _a } from "b.js";
const aaa = [1, 2, 3];
var bbb = 555;
let ccc = bbb;
const ddd = defineProps({});
const hhh = _ref(0);
hhh.value = 666;
console.log(6666);
function iAmInNonSetupScriptBlcok() {
console.log(666);
}
import Test from "./Test.vue";
import { ref, reactive, onMounted, onErrorCaptured } from "vue";
export default /*#__PURE__*/ Object.assign(__default__, {
props: _mergeDefaults(
{
e: String,
f: {
type: Array,
default: () => [],
},
},
{
f: () => [1, 2, 3],
}
),
emits: ["handleChangeColor"],
async setup(__props, { expose, emit: emits }) {
let __temp, __restore;
_useCssVars((_ctx) => ({
"xxxxxxxx-color": _unref(color),
}));
const testData =
(([__temp, __restore] = _withAsyncContext(() => getData())),
(__temp = await __temp),
__restore(),
__temp);
const a = ref(0);
const colorList = reactive(["#000", "#f00", "#0f0", "#00f", "#fff"]);
const color = colorList[0];
const b = reactive({
c: 1,
d: 2,
});
const input = ref(null);
expose({
input,
});
function handleChangeColor() {
const { random } = Math;
color.value = colorList(random() * 5);
}
onMounted(() => {
a.value = 1;
b.c = 2;
b.d = 3;
input.value.focus();
});
onErrorCaptured((err) => {
console.log(err);
});
return {
aaa,
bbb,
ccc,
ddd,
hhh,
iAmInNonSetupScriptBlcok,
emits,
testData,
a,
colorList,
color,
b,
input,
handleChangeColor,
_a,
e,
c,
Test,
ref,
reactive,
onMounted,
onErrorCaptured,
};
},
});
这回一目了然了,感觉前面说的一堆解析还不如这里直接贴出来最终代码。
这里有几处地方你们应该注意到了
import的语句包括non setup script block中的import都会被移动到头部,也就是顶级作用域。export { x as default } from 'xx'或者non setup block中的export default都会被合并到__default__中,然后和我们的setup block中的export default合并组合成最终的export defaultsetup函数中return出去的东西是包括non setup block的defineExpose会被转换为expose方法defineProps、defineEmits等方法的import都被移除了,实际上并不需要开发者去导入。props、emits的定义都会被移到setup外部,这也就解释了为什么你在defineProps、defineEmits中引用local变量(setup函数中的变量)会报错了。props、emits、expose赋值给的变量都会被处理迁移到setup函数的参数中,上面埋下的坑这里补上了。setup block中直接使用await的会被处理成一个立即执行函数,具体怎么做之前的文章里有说。- 如果用了
await,这里setup函数会给你加上async。 css变量会被处理成一个带有scopedId的字段,然后在组件创建时执行,具体怎么做的,之前的文章里也有说到过了,这里就不多说了。$ref语法糖被处理成ref,连同xxx = aa被处理成xxx.value = aa。
加入辅助函数到源码中#
// 12. finalize Vue helper imports
if (helperImports.size > 0) {
s.prepend(
`import { ${[...helperImports]
.map(h => `${h} as _${h}`)
.join(', ')} } from 'vue'\n`
)
}这个就不多说了,我们直接看下数据

这些都是runtime需要的辅助函数
这里面就mergeDefaults没说过,不过这个简单说就是合并默认值的操作。
返回最终数据#
return {
...scriptSetup,
bindings: bindingMetadata,
imports: userImports,
content: s.toString(),
map: genSourceMap
? (s.generateMap({
source: filename,
hires: true,
includeContent: true
}) as unknown as RawSourceMap)
: undefined,
scriptAst: scriptAst?.body,
scriptSetupAst: scriptSetupAst?.body
}没啥好说的。。。
总结#
今天是分析如何处理script的最后一天,我们终于看到了最终的代码长啥样了。
简单的说主要就是在将setup语法糖处理成非语法糖的样子,这里面最麻烦的就是props的处理了。。
参考#
发布于 2022-12-05 16:59・IP 属地广东
