Vue 源码阅读
最近公司项目逐渐从 Vue2 迁到 Vue3,我也趁机补一波干货,下面的一些篇幅将会记录我阅读 Vue3 源码的过程和一些思考。
环境搭建
首先下载源码,Vue3 尤大大切了一个新的仓库去写,仓库名是 vue-next
1 | git clone https://github.com/vuejs/vue-next.git |
首先看了一下项目结构,很明显是一个 Monorepo 组织的项目:
1 | ├── packages |
接着,我执行了一下 npm install
发现报错了:
1 | $ npm i |
看了下 scripts,原来是 preinstall 的时候执行了 checkYarn.js 这个脚本去判断包管理工具,所以我继续执行 yarn
安装依赖。然后执行 yarn dev
开发环境打包试了一下,编译结果放在 ./vue/dist/vue.global.js,但是没有生成 sourcemap,如果没有 sourcemap 后面就很不方便调试了。看了下打包脚本 dev.js
,发现是有 sourcemap 参数的:
1 | const sourceMap = args.sourcemap || args.s |
于是试了一下执行下面指令:
1 | $ npx node scripts/dev.js -s |
发现成功生成 vue.global.js.map,于是我在项目根目录,新建了一个 html:
1 |
|
然后新建了 launch 脚本 (需要安装 Debugger for Chrome VSCode 插件):
1 | { |
我在入口文件中第一个执行的函数 registerRuntimeCompiler
上下了个断点:
1 | > registerRuntimeCompiler(compileToFunction) |
然后执行调试 (F5),可以捕获到断点,至此 Vue3 的源码调试环境搭建完成。
入口点分析
首先从入口文件 ./packages/vue/src/index.ts 分析:
1 | import { initDev } from './dev' |
入口点的逻辑很简单,就是封装 @vue/compiler-dom 的 compile
函数,处理模板,添加缓存,注册编译函数。
createApp
当注册完编译函数后,紧接着就是调用 Vue.createApp
来创建 Vue 实例了,继续跟代码,在 @vue/runtime-dom 中找到 createApp
的定义。
1 | function ensureRenderer() { |
代码中首先间接调用 @vue/runtime-core 里的 createRenderer
函数创建全局 renderer 实例,然后调用它的 createApp
方法创建 app 实例,而后对返回的 app 实例的 mount
方法进行二次封装,开发模式注入了两个函数,一些兼容 Vue2 的代码等,这些就不深入去看了,所以其实核心都在 core 包里。
1 | export function createRenderer< |
继续跟进 createRenderer,它会调用 baseCreateRenderer,然后返回三个方法:
- render
- hydrate
- createApp
其中 hydrate 主要跟服务端渲染相关,先跳过,而 createApp 是通过 createAppAPI 创建的,我们继续跟进去看看。
1 | export interface App<HostElement = any> { |
基本上从 interface 就能猜出 app 实例有那些功能,首先它会创建一个上下文对象 context,比如我们 use components,mixin 等等就会挂载 app context 的实例上。
mount
然后我们重点看一下 mount
函数,看一下 Vue3 的首次渲染都做了什么:
1 | mount( |
render
上面 mount 中的调用的 render 函数,其实就是之前提到的 baseCreateRenderer
函数中返回的 render。
1 | const render: RootRenderFunction = (vnode, container, isSVG) => { |
我们发现,render 其实调用的是 patch
函数,或者说这就是老生常谈的 diff
,其实 baseRenderer 那两千多行的代码其实都是为了 patch
服务的。
1 | const patch: PatchFn = ( |
跟踪代码得知,首次渲染,n1 旧节点为 null,n2 为待渲染的节点,所以 patch
表现为挂载节点,后面替换节点的时候依然靠的是这个 patch
方法。相比于 Vue2 的 full diff,Vue3 显得智能很多,原因在 Vue3 会根据节点的类型分别调用不同的 process 函数,然后根据节点不同的 patchFlags,调用对应的 patch 方法。
reactive
响应式原理应该老生常谈了,Vue3 把响应式 API 单独抽出来一个 reactive 的包,并且改用 Proxy 作为它响应式的核心。记得之前写 Vue2 的时候最讨厌的就是碰到无法触发响应的数据要自己写一边 this.$set 方法,原因在于 defineProperty 对于类型支持的不完善,因此魔改了很多特殊数据类型的函数,比如 push、pop、slice 等,改成 Proxy 就舒服多了。下面我简单实现了一下 reactive 这个函数:
1 |
|