# vue3源码解析 **Repository Path**: CQLeiTao/vue3-source-code-analysis ## Basic Information - **Project Name**: vue3源码解析 - **Description**: 简单手写vue3的部分源码 - **Primary Language**: TypeScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2022-08-02 - **Last Updated**: 2023-03-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Vue3源码解析 ## 一、搭建一个Monorepo环境 > 什么是Monorepo,Monorepo是管理项目代码的方式之一,指在一个大的项目仓库(repo)中 管理多个模块/包(package),这种类型的项目大都在项目根目录下有一个packages文件夹,分多个项目管理。每个模块都是单独的一个项目,单独的package.json配置 ### 1.我们使用pnpm搭建环境 全局安装pnpm ``` npm install pnpm -g ``` 初始化配置文件 ``` pnpm init -y ``` 在 pnpm 中通过创建并配置 pnpm-workspace.yaml 设置 workspace工作区 详见文件代码 [pnpm-workspace.yaml](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/pnpm-workspace.yaml)。表示将所有的包都在packages中定义。比如我们要创建的shared和reactivity两个模块。 ### 2.安装依赖时指定安装模块区域 ``` pnpm install xxx -w ``` 这样安装就会依赖安装到packages的根目录下的node_modules中 ### 3.解决幽灵依赖问题 什么是幽灵依赖?比如我们安装了vue依赖,在vue依赖中,包含了abc依赖。虽然我们可以使用到abc。但是如果某天删除了vue后,abc就不能用了。 解决方式,创建[.npmrc](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/.npmrc)文件,添加配置`shamefully-hoist = true` 目的就是修饰提升,将下载的依赖包所依赖的其他的依赖包,放到公共的依赖区域,使其全局可用 ### 4.安装使用typescript ``` pnpm install typescript -w -D ``` 配置ts支持导入其他模块: 添加ts的配置文件: ``` pnpm tsc --init ``` 该命令会创建一个配置文件[tsconfig.json](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/tsconfig.json), 参考代码。 ### 5.安装minimist、esbuild minimist 命令行解析工具 esbuild 打包工具 ### 6.实现构建流程 创建一个目录scripts,存放打包脚本。在打包脚本中使用minimist工具进行命令行参数解析,具体参考[dev.js](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/scripts/dev.js) 在根目录的package.json中,修改打包命令。 ``` "scripts": { "dev": "node scripts/dev.js runtime-dom -f global", // node + 执行的打包脚本 + 传入的参数(打包的模块名,打包格式等) }, ``` 打包执行命令`pnpm run dev` 则会在相应的模块中生成dist ## 二、实现响应式模块 > 响应式的目的为监听数据的变化,然后做出相应的操作,比如渲染dom等 主要使用了Proxy 对象,对原对象属性进行了代理。 具体可参考vue官方的流程图 ![](https://static.vue-js.com/c2344a60-cd86-11ea-ae44-f5d67be454e7.png) 代码解析参考[reactivity模块](https://gitee.com/CQLeiTao/vue3-source-code-analysis/tree/master/packages/reactivity) ## 三、实现虚拟dom,以及dom渲染 ### 1.更新元素逻辑 1.如果新老元素完全没有关系,直接删除老的,创建新的 2.新老一样,属性不一样,对比属性,更新属性 3.对比子节点(diff算法) ### 2.diff算法 diff算法用于比较元素子节点,以及子节点的属性内容。 参考 [renderer.ts](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/packages/runtime-core/src/renderer.ts)中 patchKeyedChildren方法以及注释 **1.简单情况** > 有序简单元素的新增和删除,单纯在老元素的基础上,进行新增删除,不打乱老元素顺序。 通过两次遍历进行比对,一次是正序遍历,一次是倒序遍历。遍历结束条件为,谁先遍历完,就结束遍历。 这里有三个指针: i 指针: 记录old和new共同的位置,初始指向0 o 指针: 记录old遍历的位置, 初始指向老元素的末尾位置 n 指针: 纪律new遍历的位置, 初始指向新元素的末尾位置 正序遍历时,通过递增i,挨个对比新老元素。如果不一样,则退出循环,如果没有不一样的,则等到o 、n 中的一个指针遍历到结尾就结束。 纪录i 的位置,如果i不在新老节点的结尾,也就是新老节点没有一个遍历完的,则再经行倒序遍历,新老指针从数组最后往前递减,遍历比对,如果两个元素不一样,则推出循环,得到o n 的位置。 最后获取三个指针位置,进行判断是需要新增还是删除。 ----- 1)新增元素 新增元素在结尾,在老元素末尾添加新元素 ![](./imgs/reqTra.png) 这种情况,i会直接遍历到o的结尾,且 i < n。则直接在结尾添加新元素 新增元素在开头,在老元素开头插入新元素 ![](./imgs/desTra.png) 这种情况,i = 0,就推出的正序遍历,然后进行了倒序遍历。等到n=1, o=-1 时,遍历结束。 如果i > o, 则说明新增元素在前面 2)删除元素 相同的原理 如果i < o, 说明要删除元素 **2.乱序情况** 乱序的几种情况: 比如: 新老顺序不一样 a b c b c a i = 0, o = 2, n = 2 新老有新增删除的节点 a b c d d a g h i = 0, o = 3, n = 3 开头结尾一样,中间某部分有新增删除交换顺序的情况 a b c d e f g h a b e c d n g h i = 2, o = 5, n = 5 a b c d e g h a b e c d f g h i = 2, o = 4, n = 5 以上几种情况总结为: 通过 i o n 来确定不一样的部分,然后只对不一样的部分进行判断和替换。 大致的步骤为: 1.先将新元素中乱序部分提取出来 2.先不管元素移动和新增,确定哪些元素是删除的,哪些是可复用老节点的 3.通过标记的方式,确定哪些老元素位置改变了的,进行换位,以及新元素的新增 **3.最长递增子序列优化** 在乱序的情况中,乱序部分的索引变化可能为: [3,4,5] => [5,3,4,6]。如果按照上面的方式,这部分每个元素都要进行一个移位操作,4移动到6前面,3移动到4前面,5移动到3前面。但是这种我们发现,3和4的相对位置是没有发生改变的,他们两个是可以不用移动的,只需要将5移动到3前面就行,这样很节约性能。所以,为了找出那些不需要移动的元素,我们就需要在新的乱序数组中查找最长递增子序列。 计算最长递增子序列,可参考[sequence](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/packages/runtime-core/src/sequence.ts) 中的代码和注释 得到最长递增子序列后,我们就只移动不在该序列的元素即可 比如: [5,3,4,6] 最长递增子序列是 [3,4,6] , 在进行插入操作时,判断老元素的索引是否在该序列中,如果在则不进行操作,否则进行插入操作 ### 3.组件的渲染和更新 代码参考 [renderer.ts](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/packages/runtime-core/src/renderer.ts) 中的`processComponent`方法 通过函数式创建组件的方式为: ``` const VueComponent = { data() { return {name: 'lt', age: 18} }, render() { setTimeout(() => { this.age ++ this.name += 1 },1000) return h('h1',`${this.name}有${this.age}岁`) } } render(h(VueComponent), app) ``` 所以要支持组件渲染,我们需要在创建虚拟节点vnode时,判断第一个参数是不是对象,如果是对象则标记为有状态组件。 1. 创建一个组件实例 2. 给实例赋值 3. 创建一个effect 具体代码参考[renderer.ts](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/packages/runtime-core/src/renderer.ts) 的 mountComponent方法 ### 4.组件实现props 代码参考[component.ts](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/packages/runtime-core/src/component.ts) 使用例子参考[props.html](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/packages/runtime-dom/dist/props.html)、[propsReactive.html](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/packages/runtime-dom/dist/propsReactive.html) 使用props的场景 ``` const VueComponent = { // 定义组件props props: { address: String, info: {} }, render() { return h('p',[this.address, this.$attrs.a, this.$attrs.b, this.info.name]) } } // 通过h的第二个参数,传递props,但这里传递的props,并非都在组件的props中有定义 render(h(VueComponent, {address: '重庆', a:1, b:2, info:{name: 'lei'}}), app) ``` 1. 首先需要拿到h中传入的props,和组件中的props,对比分析,哪些属性是组件props中定义的 2. 对组件实例进行代理,监听data,props attrs属性。 ### 5.setup的实现 ``` setup方法:提供组合式API,可以覆盖以及添加组件的属性。 作用:可以将各个组件中重复的部分的代码提取出来,方便其他组件共享此代码 入参:(props | context)。 返回:支持返回一个对象,或者直接返回一个render方法 ``` 示例:[setup.html](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/packages/runtime-dom/dist/setup.html) 实现原理:在创建组件时,识别传入的setup属性,对其运行结果进行解析,返回方法则代替原有的render方法,返回对象在proxy中添加改对象属性值的get set ### 6.事件和插槽 **事件** 组件的事件在vue中,事件名会被解析成`onXxxx`。需要我们在emit方法中转换 主要实现逻辑如下 ``` const setupContext = { emit: (event, ...args) => { let eventName = 'on' + event[0].toUpperCase() + event.slice(1).toLowerCase() // 转换方法名 let handler = instance.vnode.props[eventName] // 获取到该方法 handler && handler(...args) } } ``` **插槽** 使用示例[componentSlot.html](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/packages/runtime-dom/dist/componentSlot.html) 插槽的作用是在组件中插入其他元素。 组件的插槽是一个对象,渲染组件的时候去映射表中查找对应方法 ### 7.生命周期 组件的生命周期常见的有以下几种:onBeforeMount, onMounted, onBeforeUpdate, onUpdated 大致实现步骤: 1.储存定义的生命周期函数到组件实例中 2.在组件进行到相关步骤时执行 3.保证钩子函数中能获取到当前实例 使用实例[componentLifecysle.html](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/packages/runtime-dom/dist/componentLifecysle.html) 主要逻辑代码[apiLifecycle.ts](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/packages/runtime-core/src/apiLifecycle.ts) ### 8.靶向更新 靶向更新是指,在数据发生变化是,指更新数据对应的dom模块。 实现原理: 通过patchFlag动态标记内容,优化diff算法,识别标记进行更新。 主要的方法有openBlock、createElementBlock、toDisplayString、createElementVNode 'block'相关方法,有收集动态节点的功能 动态标记的实现主要参见[vnode.ts](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/packages/runtime-core/src/vnode.ts)文件中的openBlock、createElementBlock、toDisplayString、createElementVNode 方法 识别标记进行更新的实现主要参见[renderer.ts](https://gitee.com/CQLeiTao/vue3-source-code-analysis/blob/master/packages/runtime-core/src/renderer.ts)中的patchElement方法,这里只简单实现对class标识进行靶向更新的例子,其他属性,样式等同理 ## 四、编译模块 作用:将html 转换为render函数可渲染的内容。 如 ```
123
要编译为: render(h('div','123')) ``` 实现步骤: 1. 将html语法转换成js语法,将模板转换为抽象语法树(ast) --- parse() 2. 将ast语法树进行一些预先处理,生成一些信息,为后期生成代码提前做准备 --- transform(ast) 3. 最终生成代码 --- generate(ast) ### 1.转换ast语法树 实现原理: 定义各种节点的枚举类型 ``` { ROOT, // 根节点 ELEMENT, // 元素 TEXT, // 文本 COMPENT, // 注释 SIMPLE_EXPRESSION, // 简单表达式 aaa :a="aa" INTERPOLATION, // 模板表达式 {{aaa}} ATTRIBUTE, DIRECTIVE, COMPOUND_EXPRESSION, // 复合表达式 {{aa}} abc IF, IF_BRANCH, FOR, TEXT_CALL, // 文本调用 VNODE_CALL, // 元素调用 JS_CALL_EXPRESSION, // js调用表达式 } ``` 解析规则:根据一个个字符进行判断,且解析一部分就删除一部分。 1. 创建解析的上下文,记录行、列、偏移量等信息 2. 循环获取每个字符,根据字符内容判断是什么类型。 '<' 说明是元素 '{{' 说明是表达式 其他就是文本 3. 解析文本主要是要确定哪部分是文本,文本到哪结束。 结束的标记一般有: '<'、'{{' 使用假设法进行判断。