Vue3已经发布有一段时间了,最??近有机会在公司项目中使用到Vue3+TypeScript+Vite技术栈,所以也抽空看了下Vue3业余时间源码。本着好记性不如烂笔头的理念,我在阅读源码的过程中记录了一些笔记,希望能写一些源码阅读笔记,帮助到每一位想阅读源码但可能有问题的同学。减少理解成本的困难。我也对Vue2.x的源代码做了一些简单的阅读。自从Vue3重构之后,Vue项目的目录结构也发生了很大的变化。各个功能模块都放在了packages目录下,职责更加明确。通过目录名称可以一目了然。今天我们就从Vue的入口文件入手,看看一个Vue单文件是如何在声明单文件后被compile-core编译成渲染函数的。为了大家阅读方便和控制文章篇幅,我会把阅读源码时不需要关心的逻辑折叠起来,或者通过注释/*忽略逻辑*/来忽略。个人不喜欢看一篇源码分析文章,搞出一大段代码,容易让没看过的同学有点摸不着头脑。所以在本系列文章中,我会尽量把关键代码画成流程图。目的还是一个,帮助大家降低理解成本,同时让同学们下次独立阅读的时候有一个流程图可以参考。我们将从Vue对象packages/vue/index.ts的条目开始我们的源代码阅读。这个入口文件的代码比较简单,只有一个compileToFunction函数,但是函数体的内容比较关键,所以先看一张图了解一下函数体是做什么的。看完流程图,我们一起来看代码。相信此时大部分同学可能都能一眼看出图片中的代码。直接跳过所有代码,看文件末尾的35行,调用registerRuntiomCompiler函数,将compileToFunction函数作为参数传入,这行代码对应流程图的开头,将compile函数注入到runtime通过依赖注入来运行目前,依赖注入是一种比较巧妙的解耦方式。此时调用运行时的compile函数就是调用当前的compileToFunction函数。看代码中的第17行,调用了compile-dom库提供的compile函数,从返回值中解构出代码变量。这是编译器执行后生成的编译结果。code是编译结果的参数之一,是一个代码串。例如,HelloWorld
是一个简单的模板。编译后代码返回的字符串为const_Vue=Vuereturnfunctionrender(_ctx,_cache){with(_ctx){const{openBlock:_openBlock,createBlock:_createBlock}=_Vuereturn(_openBlock(),_createBlock("div",null,"HelloWorld"))}}后面我会详细解释这个神奇的编译函数的内部。得到这个代码串的结果后,我们再往下看代码。第25行声明了一个render变量,并将生成的代码字符串code作为参数传递给新的Function构造函数。这是流程图中的倒数第二步,生成渲染函数。你可以格式化我上面放的代码字符串,你会发现render函数是一个返回函数的柯里化函数,作用域链是通过函数内部的with扩展的。最后入口文件返回渲染变量,方便缓存渲染函数。在上面源码的第一行,我们看到入口文件创建了一个compileCache对象来缓存compileToFunction函数生成的render函数,使用模板参数作为缓存key,在第11行有一个if分支判断缓存,如果模板之前缓存过,则不再编译,直接返回缓存中的render函数,提高性能。至此package/vue/index.ts的入口文件已经解释完毕。相信大家都能看出来。最有意思的部分是调用编译函数编译代码字符串,所以接下来继续讲编译函数。compile函数涉及两个模块,compile-dom和compile-core。在这篇文章中,我将只解释关键过程。详细分析将放在后续文章中。我们看一下compile的运行过程:compile函数直接返回baseCompile函数的结果,baseCompile函数在执行过程中会生成一个AST抽象语法树,调用transform处理每个AST节点,比如transformvOn,v-if、v-for等指令。最后通过generate函数生成处理后的AST抽象语法树,生成上述代码串,并返回编译结果。至此,编译函数执行完毕。了解了大概的流程之后,我们来看一下源码。compile函数的源码路径为packages/compiler-dom/src/index.ts。我们可以看到在compile函数体中,直??接返回了baseCompile的处理结果。baseCompile的源码路径为packages/compiler-core/src/compile.ts。为什么会有baseCompile这个名字呢?因为compile-core是编译的核心模块,接受外部参数按照规则完成编译,compile-dom专门做浏览器场景下的编译。该模块下导出的编译函数是入口文件实际接收到的编译函数。compile-dom中的compile函数也是比baseCompile更高级的编译器。例如,当Vue在weex中工作在iOS或Android等原生应用时,compile-dom可能会被相关的移动端编译库替换。下面我们一起来看看baseCompile函数:首先,从函数声明中,baseCompile接收到template模板和在上层高级编译器处理过的options编译选项,最后返回一个CodegenResult类型的编译结果。exportinterfaceCodegenResult{code:stringpreamble:stringast:RootNodemap?:RawSourceMap}通过CodegenResult的接口声明,可以清楚的看到返回结果中有code代码串,处理过的AST抽象语法树,sourceMap。看上面源码第12行,判断模板模板是否为字符串,如果是则解析字符串,否则直接将模板作为AST。其实我们平时写的单文件vue代码都是以字符串的形式传入的。接下来源码在第16行调用transform函数,传入指令转换、节点转换等工具函数,对模板生成的AST进行转换。在最后的第32行,我们将转换后的AST传给generate,生成一个CodegenResult类型的返回结果。在compile-core模块中,AST解析、transform、codegen、compile、parse等功能都是一个单独的小模块,内部实现非常精致,将在后续文章中一一介绍编译器。本文从入口文件入手,讲解编译的大致流程,希望能帮助大家在阅读编译模块代码时有个清晰的流程概念,配合流程图吃起来更美味。