前言#
我们从vue-loader开始,一步一步分析到vue/compiler-core包源码,那么这编译阶段我们就分析完了(这里忽略compiler-ssr) 。
这里涉及到了几个包,我们来整理一下。
入口#
我这里分析的是基于webpack的,所以入口是vue-loader。
简单的说下这里面做了什么。
首先在调用vue-loader之前需要先安装vueLoaderPlugin
vueLoaderPlugin#
这里面主要做了几件事
- 将
vue-loader标志位置为true,这样webpack编译器就可以调用vue-loader了。 - 复制一份包含所有
loader的rules(不包含vue-loader),分别给它们加上resouce和resourceQuery,这俩是用来确认请求的vue资源是否可以被该loader处理的,这里不用担心会影响到其它的loader。 - 往
copy的rules中注入templateLoader,这样在资源是vue的template的时候就会被识别到并且最终处理成render function。注意render function是js代码,所以它也需要被其它loader识别处理。 - 插入
css-post-loader用来处理vue的style,这里需要注意顺序,得在style loader和css loader之前执行。 - 改写
request资源的路径,这样才能被vue-loader识别到。 - 覆盖原来
webpack compiler的rules。
然后回到vue-loader中,根据请求路径返回对应的资源。这里说下script部分没有loader,所以在vue-loader识别到请求的参数是js的才会调用@vue/compiler-sfc的compileScript来处理并返回内容。
不同的loader调用不同的@vue/compiler-sfc方法处理不同的部分:
compileTemplate:被templateLoader调用,用来处理template部分;compileScript:被vue-loader自身调用,用来处理script部分;compileStyle:被css-post-style调用,用来处理style部分;
处理完后的资源在vue-loader做runtime拼接之后再传递给下一个loader。
compiler-sfc#
那么入口分析完了,就轮到被调用的包了。
坏蛋Dan:vue/compiler-sfc源码分析学习--汇总
compileScript#
坏蛋Dan:vue/compiler-sfc源码分析学习--part2:如何处理script--final章节整理
上面说了处理script部分是通过compileScript这个方法来处理的,在这个文件中核心是调用babel来处理js代码转换为AST节点,然后基于AST 节点做处理。
场景很多,这里简单的说下:
- 没有
setup语法糖block的script,按optionApi的方式来处理 - 有
setup语法糖block的script且有non setup block的script,都处理,其中non setup block处理之后整合到setup block的顶部。而setup block处理成非语法糖的写法。
注意:所有的语法糖都只是方便使用,而在编译阶段的都会转化成原本的写法。
compileTemplate#
坏蛋Dan:vue/compiler-sfc源码分析学习--part3:如何处理template--final章节整理
这里没怎么处理,调用的是在compiler-dom包里,但是核心处理是在compiler-core里。
compileStyle#
坏蛋Dan:vue/compiler-sfc源码分析学习--part4:如何处理style--final章节整理
核心是调用postcss[1] 来处理。
然后如果有lang的话,调用对应的预处理器先。比如lang="scss",那么再开始调用postcss解析之前会先调用sass-loader来处理一遍。
其他没什么好说的了,但是有个点这里要提一下,就是处理cssVar。
vue在调用之前自定义了一个postcss 插件cssVarsPlugin,这个插件就是用来处理vue3.x中css变量的方式。比如color: v-bind(color)会变成var(--color)的方式。
vue3.x中使用css变量: SFC CSS Features | Vue.js (vuejs.org)
另外还有一个点,就是scopeId。
每一个sfc文件都会有一个对应的scope Id,如果这个style block有要求scoped,那么就会给每个css rule加上scopeId。
compiler-dom#
坏蛋Dan:vue/compiler-dom源码分析学习--final:整理
你应该注意到了,上面compiler-sfc处理完了之后其实只剩下了template没有处理好了,而script、style都已经是可以运行在浏览器中的最终代码了。
而接下来compiler-dom以及compiler-core其实都是在处理template相关的,因为template只是一段长得像html的js代码。
不过尴尬的是html解析引擎不认识它,js的引擎也不认识它。
所以这里就需要自行去把这template处理成js代码。
而compiler-dom这个包是compiler-core的上游包,也就是基于compiler-core封装的,加上了一些插件。
这个包没啥好说的,感兴趣的可以去看下。
不过这里有几个点需要注意:
v-on/v-model等的modifiers也就是修饰符的处理是放到这个包里的- 过滤
style\script标签也是放在这个包里,这俩标签不应该再出现了(svg标签里的style和这里的不同) - 是否可以
staticStringify的判断和处理都是在这个包中。
compiler-core#
vue/compiler-core源码分析学习--final:汇总 - 知乎 (zhihu.com)
这个包是处理template的核心部分
我直接把之前画的图贴出来,应该已经很清晰了。

可以参考babel的core包的架构。
简单地说就是把template这一段字符串通过parse变成AST,然后通过transform遍历所有节点通过不同插件处理节点,接着通过generate生成render function。这个时候的render function已经是浏览器可以执行的runtime js代码了。
优化#
这里面关于优化的东西我都没有分析,这里就简单说下我遇到过的优化:
cache,这个cache有runtime的,比如指令的value是一个表达式,那么表达式的结果其实是可以缓存的,这样每次调用返回缓存即可。而compile阶段的缓存更多的是和hot-reload搭配的,在下一次编译的时候可以过滤掉。staticHoist,这个我们是有分析的,因为在主流程中。通过判断不同的点,比如属性是否是静态之类的来判断这个节点是否每次都需要create,如果不需要就把它提升到顶部,这样就只需要create一次。注意这个是runtime的优化。另外这里可以hoist不只有dom节点,还有属性节点等也是可以的。staticStringify,这个是在staticHoist的基础上,如果不满足hoist的条件那就一定不满足stringify的条件。stringify使用的是innerHTML的方式插入,所以只有这一块代码中节点数量/指令数量达到了一定的个数时才会stringify,因为如果数量少,其实innerHTML插入的性能没啥提升。PatchFlag,也就是插旗,给不同类型的节点插旗,这样可以快速定位需要如何处理,在patch的时候diff就能绕过一些逻辑,减少diff的时间和损耗。track,实际上和插旗一样,跟踪上面的flag,如果没有flag的节点,那diff阶段就可以skip了。
dev和prod输出内容差距大#
结果分析#
分析完了忘了把最终结果发出来了。。。

这就是最后的结果,一个render function和其它js组成的部分。
为什么会长这样呢?还记得我在vue-loader分析的文章中最后贴出来的runtime代码图吗?

经过eval[2] 执行之后就会变成上面那个object的样子,至于它们是怎么拼接到一起的,看下这图里有一个exportComponent的方法,之前也有说过,这里再看一次代码
// runtime helper for setting properties on components
// in a tree-shakable way
exports.default = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
}; 这也就是为什么被整合成一个对象的原因了。
- 源码
<template>
<div id="tttt" class="test" @click="test">
{{aa}}
{{cc.dd + 1}}
<div class="tt">
666
</div>
</div>
</template>
<script setup>
import { ref, reactive, defineComponent } from 'vue';
defineProps({
name: {
type: String,
default: ''
}
})
const aa = ref(0);
function test () {
aa.value = 456;
cc.dd = Math.random() * 10
}
const cc = reactive({ dd: 456 });
</script>render function
__vite_rsc_require__.r(__webpack_exports__);
/* harmony export */ __vite_rsc_require__.d(__webpack_exports__, {
/* harmony export */ "render": () => (/* binding */ render)
/* harmony export */ });
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __vite_rsc_require__(/*! vue */ "./node_modules/.pnpm/vue@3.2.38/node_modules/vue/dist/vue.runtime.esm-bundler.js");
const _withScopeId = n => ((0,vue__WEBPACK_IMPORTED_MODULE_0__.pushScopeId)("data-v-01a29953"),n=n(),(0,vue__WEBPACK_IMPORTED_MODULE_0__.popScopeId)(),n)
const _hoisted_1 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", { class: "tt" }, " 666 ", -1 /* HOISTED */))
function render(_ctx, _cache, $props, $setup, $data, $options) {
return ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", {
id: "tttt",
class: "test",
onClick: $setup.test
}, [
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createTextVNode)((0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.aa) + " " + (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.cc.dd + 1) + " ", 1 /* TEXT */),
_hoisted_1
]))
}script runtime
__vite_rsc_require__.r(__webpack_exports__);
/* harmony export */ __vite_rsc_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __vite_rsc_require__(/*! vue */ "./node_modules/.pnpm/vue@3.2.38/node_modules/vue/dist/vue.runtime.esm-bundler.js");
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({
__name: 'index',
props: {
name: {
type: String,
default: ''
}
},
setup(__props, { expose }) {
expose();
const aa = (0,vue__WEBPACK_IMPORTED_MODULE_0__.ref)(0);
function test () {
aa.value = 456;
cc.dd = Math.random() * 10
}
const cc = (0,vue__WEBPACK_IMPORTED_MODULE_0__.reactive)({ dd: 456 });
const __returned__ = { aa, test, cc, ref: vue__WEBPACK_IMPORTED_MODULE_0__.ref, reactive: vue__WEBPACK_IMPORTED_MODULE_0__.reactive, defineComponent: vue__WEBPACK_IMPORTED_MODULE_0__.defineComponent }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
});注意这里的都是dev阶段的,而对于prod的来说则是

可以看到render function不见了,其实是被整合到setup function里面。

依旧的可读性极差。
此时的render function变成了vnode,而这些vnode下一步就是patch/createElement渲染到页面上了。
至于为啥dev和prod的区别这么大,这个还需要进一步分析。
为什么prod版本没有render function字段#
让我们把mode改成production重新编译一次

可以看到template相关的import不见了。
也就是说没进入templateLoader,自然就不会调用compileTemplate。
那么返回去跟踪下,在vue-loader/index中生成请求url 那段代码中

没想到。。。居然是我们没有分析的inline template场景。。。。。
/**
* inline template mode can only be enabled if:
* - is production (separate compilation needed for HMR during dev)
* - template has no pre-processor (separate loader chain required)
* - template is not using src
*/
function canInlineTemplate(descriptor, isProd) {
const templateLang = descriptor.template && descriptor.template.lang;
const templateSrc = descriptor.template && descriptor.template.src;
return isProd && !!descriptor.scriptSetup && !templateLang && !templateSrc;
} 回想我们分析compiler-sfc的compileScript时确实遇到了判断inline然后执行compiler-dom.compile的场景。。。
// 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
}
})那么一切就都说得通了,这里当compileScript结束后,就已经变成vnode的样子并且inline到script的setup里面去了。。。。
总结#
那么编译阶段的源码我们就都分析完了,这一套分析下来其实我个人学到的东西非常非常多,整个vue的编译流程也比以前清晰的多。
现在也不再觉得这是魔法了~
最后如果觉得对你有帮助的话请务必点个赞~
最后的最后,祝大家新春快乐!!
参考#
- ^postcss https://postcss.org/
- ^eval https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
编辑于 2023-01-24 23:49・IP 属地广东
