Vue3 编译流程-源码解析

前言:

Vue3 发布已经很长一段时间了,最近也有机会在公司项目中用上了 Vue3 + TypeScript + Vite 的技术栈,所以闲暇之余抽空也在抽空阅读 Vue3 的源码。本着好记性不如烂笔头的想法,在阅读源码时顺便记录了一些笔记,也希望能争取写一些源码阅读笔记,帮助每个想看源码但可能存在困难的同学减少理解成本。

Vue2.x 的源码我也有过一些简单的阅读,自 Vue3 重构后,Vue 项目的目录结构也发生了很大的变化,各个功能模块被分别放入了 packages 目录下,职责更加清晰,通过目录名就可以一目了然。今天将从 Vue 的入口文件开始,看看声明了一个 Vue 的单文件之后是如何被 compile-core 编译核心模块编译成渲染函数的。

为了大家的阅读方便,以及控制文章篇幅,我会把阅读源码时不太需要在意的逻辑进行折叠,或者通过注释 /* 忽略逻辑 */ 这样的标识进行忽略处理。

我个人是不太喜欢在看源码分析文章时一上来就怼出一大段代码,这容易让没阅读的同学有点懵逼。所以这个系列的文章我会尽量对关键的代码画出一张流程图。目的还是一个,帮助大家降低理解成本,同时也让各位同学在下次自主阅读时有张流程图能参考。

1、解读Vue 入口文件

我们会先从一个 Vue 对象的入口来开始我们的源码阅读, packages/vue/index.ts 。这个入口文件的代码比较简单,只有一个 compileToFunction 函数,但函数体内的内容却又比较关键,所以先看一张图,来理解这个函数体究竟完成了哪些事情。

在看完流程图之后,我们来对照代码一起看,我相信大部分同学在此时可能对下发图片中的代码一目了然了。

直接跳过所有代码,看文件的末尾 35 行,调用了 registerRuntiomCompiler 函数,将 compileToFunction 函数作为参数传入,这行代码即对应流程图的起始,通过依赖注入的方式,将 compile 函数注入至 runtime 运行时中,依赖注入是一种比较巧妙的解耦方式,此时运行时再调用 compile 编译函数,就是在调用当前的 compileToFunction 函数了。

再看代码中的第 17 行,调用了 compile-dom 库提供的 compile 函数,从返回值中解构出了 code 变量。这个就是编译器执行之后生成的编译结果,code 是编译结果的其中一个参数,是一个代码字符串。比如

<template>
  <div>
    Hello World
  </div>
</template>

这个简单的模板,在经过编译后,code 返回的字符串为

const _Vue = Vue return function render(_ctx, _cache) {  with (_ctx) {    const { openBlock: _openBlock, createBlock: _createBlock } = _Vue     return (_openBlock(), _createBlock("div", null, "Hello World"))  } }

这个神奇的 compile 函数内部的奥妙在之后我会详细讲解。

在拿到这个这个代码字符串的结果后,我们再顺着代码往下看,第 25 行声明了一个 render 变量,并且将生成的代码字符串 code 作为参数传入了 new Function 构造函数。这就是流程图中的倒数第二步,生成了 render 函数。可以将我放在上面的 code 字符串格式化,能够发现 render 函数是一个柯里化的函数,返回了一个函数,函数内部通过 with 来扩展作用域链。

而最后入口文件返回了 render 变量,并且顺手缓存了 render 函数。

上方源码的第 1 行,我们看到入口文件创建了一个 compileCache 对象,用以缓存 compileToFunction 函数生成的 render 函数,将 template 参数作为缓存的 key, 并在 11 行的位置有一个 if 分支做缓存的判断,如果该模板之前被缓存过,则不再进行编译,直接返回缓存中的 render 函数,以此提高性能。

至此 package/vue/index.ts 的入口文件就解读完了。相信大家也都看出来了,最有意思的部分就是调用 compile 函数编译出了代码字符串,所以接下来我将围绕 compile 函数来接着唠。compile 函数牵扯到 compile-dom compile-core 两个模块,本篇文章我只会解读关键流程。细节分析的话会放在后续文章中。一起来看一下 compile 的运行流程:

2、compile 的运行流程

compile 函数内部直接返回 baseCompile 函数的结果,而 baseCompile 函数在执行过程中会生成 AST 抽象语法树,并调用 transform 对 每个 AST 节点进行处理,例如转换vOn、v-if、v-for 等指令,最后将处理后的 AST 抽象语法树通过 generate 函数生成之前提及的代码字符串,并返回编译结果,至此 compile 函数执行完毕。明白了大体的流程后,接着来看源码。

compile 函数的源码路径是 packages/compiler-dom/src/index.ts, 我们看到在 compile 的函数体内,直接 return 了 baseCompile 的处理结果。而 baseCompile 的源码路径是 packages/compiler-core/src/compile.ts 。为什么会有 baseCompile 这样的命名呢?因为 compile-core 是编译的核心模块,接受外部的参数来按照规则完成编译,而 compile-dom 是专门处理浏览器场景下的编译,在这个模块下导出的 compile 函数是入口文件真正接收的编译函数。而 compile-dom 中的 compile 函数相对 baseCompile 也是更高阶的一个编译器。例如当 Vue 在 weex 在 iOS 或者 Android 这些 Native App 中工作时,compile-dom 可能会被相关的移动端编译库来取代。

顺着往下一起看一下 baseCompile 函数:

先从函数声明中来看,baseCompile 接收 template 模板以及上层高阶编译器中处理过 options 编译选项,最终返回一个 CodegenResult 类型的编译结果。

export interface CodegenResult {
  code: string
  preamble: string
  ast: RootNode
  map?: RawSourceMap
}

通过 CodegenResult 的接口声明能清晰的看到返回结果中存在 code 代码字符串、处理后的 AST 抽象语法树,以及 sourceMap。

看上方源码的第 12 行,判断 template 模板是否为字符串,如果是的话则会对字符串进行解析,否则直接将 template 作为 AST 。其实我们平时在写的单文件 vue 代码,都是以字符串的形式传递进去的。

接下来源码是 16 行调用了 transform 函数,以及传入了指令转换、节点转换等工具函数,对由模板生成的 AST 进行转换。

最终的 32 行位置,我们将转换好的 AST 传入 generate,生成 CodegenResult 类型的返回结果。

在 compile-core 模块中,AST 解析、transformcodegencompileparse 这些函数都是一个单独的小模块,内部的实现都非常精妙,在编译器的后续文章中,会逐个进行介绍。

本文通过从入口文件开始,对编译的大体流程进行解释,希望可以帮助大家在阅读编译器这个模块的代码时能有一个清晰的流程概念,配合流程图食用更香哟。

到此这篇关于Vue3 编译流程-源码解析的文章就介绍到这了,更多相关Vue3 编译流程内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • vue3.0实现下拉菜单的封装

    vue3.0出来已经有段时间的了,也与必要开始研究它了! 先看下我们要实现的效果 很常见的展开显示菜单项的内容,在vue3.0里面怎么开发,这里样式我们用的是bootstrap的默认样式 思路一: <DropDown :title="'退出'" :list="menuLists" /> 思路二: <drop-down :title="'退出'"> <drop-dowm-item>新建文章</drop-do

  • vue3 与 vue2 优点对比汇总

    目录 优点1:diff算法的优化 优点2:hoistStatic 静态提升 优点3:cacheHandlers 事件侦听器缓存 优点4:ssr渲染 优点5:更好的Ts支持 优点6:Compostion API: 组合API/注入API 优点7:更先进的组件 优点8:自定义渲染API 优点9:按需编译,体积比vue2.x更小 优点10:支持多根节点组件 ​​​​​​摘要: Vue3新版本的理念成型于 2018 年末,当时的 Vue 2 已经有两岁半了.比起通用软件的生命周期来这好像也没那么久,Vu

  • Vue3结合TypeScript项目开发实战记录

    目录 概述 1.compositon Api 1.ref 和 reactive的区别? 2.周期函数 3.store使用 4.router的使用 2.关注点分离 3.TypeScript支持 总结 概述 Vue3出来已经有一段时间了,在团队中,也进行了大量的业务实践,也有了一些自己的思考. 总的来说,Vue3无论是在底层的原理上,还是在业务的实际开发中,都有了长足的进步. 使用 proxy 代替之前的 Object.defineProperty 的API,性能更加优异,也解决了之前vue在处理对

  • Vue3封装 Message消息提示实例函数详解

    目录 Vue3封装 消息提示实例函数 样式布局封装 message.vue 功能实现 message.js 注册 自定义指令 使用 : 总结 Vue3封装 消息提示实例函数 Vue2.0使用 Vue.prototype.$message = function () {} vue3.0使用app.config.globalProperties挂载原型方法app.config.globalProperties.$message = Message 也支持直接导入函数使用 import Message

  • Vue2与Vue3兄弟组件通讯bus的区别及用法

    目录 vue2.x vue3.x tiny-emitter插件用法 mitt插件用法 vue2.x Vue.prototype.$bus=new Vue() 监听: this.$bus.$on('方法名',(参数)=>{}),它可以累加 触发: this.$bus.$emit('方法名',实参值) 销毁:this.$bus.$off('方法名'),谁监听谁销毁 注意: 由于监听可以累加, 所以必须要有第四步销毁 vue3.x tiny-emitter插件用法 安装插件 npm i tiny-em

  • 利用vue3自己实现计数功能组件封装实例

    目录 前言 一.封装的意义 二.如何封装? 1. 思路 2. 准备 2. 使用 三. 效果演示 总结 前言 本文将带你用vue3自己封装一个实现计数功能的全局组件,其应用场景相信各位一看便知,那就是购物网站中常见的数量选择模块,一起来看看如何实现哇 一.封装的意义 项目中需要用到的地方较多 模块化开发,降低了代码冗余,是开发更加高效 一次封装,到处使用 二.如何封装? 1. 思路 使用vue3中v-model来完成父子组件之间的相互传值,本文章使用vueuse/core中封装好的useVMode

  • Vue3结合TypeScript项目开发实践总结

    目录 概述 1.compositon Api 1.ref 和 reactive的区别? 2.周期函数 3.store使用 4.router的使用 2.关注点分离 3.TypeScript支持 概述 Vue3出来已经有一段时间了,在团队中,也进行了大量的业务实践,也有了一些自己的思考. 总的来说,Vue3无论是在底层的原理上,还是在业务的实际开发中,都有了长足的进步. 使用 proxy 代替之前的 Object.defineProperty 的API,性能更加优异,也解决了之前vue在处理对象.数

  • Vue3的7种种组件通信详情

    目录 1.Vue3 组件通信方式 2.Vue3 通信使用写法 2.1 props 2.2 $emit 2.3 expose / ref 2.4 attrs 2.5 v-model 2.6 provide / inject 2.7 Vuex 1.Vue3 组件通信方式 props $emit expose / ref $attrs v-model provide / inject Vuex 2.Vue3 通信使用写法 2.1 props 用 props 传数据给子组件有两种方法,如下 方法一:混合

  • Vue3 AST解析器-源码解析

    目录 1.生成 AST 抽象语法树 2.创建 AST 的根节点 3.解析子节点 4.解析模板元素 Element 5.示例:模板元素解析 上一篇文章Vue3 编译流程-源码解析中,我们从 packges/vue/src/index.ts 的入口开始,了解了一个 Vue 对象的编译流程,在文中我们提到 baseCompile 函数在执行过程中会生成 AST 抽象语法树,毫无疑问这是很关键的一步,因为只有拿到生成的 AST 我们才能遍历 AST 的节点进行 transform 转换操作,比如解析 v

  • vue3.0实现复选框组件的封装

    本文实例为大家分享了vue3.0实现复选框组件封装的具体代码,供大家参考,具体内容如下 大致步骤: 实现组件本身的选中与不选中效果 实现组件的v-model指令 改造成 @vueuse/core 的函数写法 <!-- 组件基本样式 --> <template> <div class="xtx-checkbox" @click="changeChecked()"> <i v-if="checked" cla

随机推荐