本文是专栏的第一篇,主要了解一些基本概念,准备源码调试和实现的环境
一些概念
名词概念
编程范式
命令式
面向过程: 以描述怎么做的形式来实现功能
声明式
面向结果: 以描述做什么的形式来实现功能
声明式是建立在命令式之上的,因为一定是经过了某些过程,才能达到某个结果
命令 vs 声明
衡量某种编程范式可以从下面两个方面入手:
- 性能
- 可维护性:代码是否方便阅读和修改
对于命令式来说: 只做了能达到结果的必要步骤,其性能要更好一些,但是其随着过程的复杂而更加复杂,因此可维护性会较低
对于声明式来说: 声明式基于命令式所以其性能必然没有命令式好,但其只关心结果,不关心复杂的实现过程,代码复杂度不会随着业务的复杂而疯狂上涨,因此可维护性较高
应用开发和设计原则
企业中开发应用时,最关注两个点:
- 项目成本
- 开发体验
项目成本取决于开发周期,开发周期越长,人力成本越大,项目成本就越大
开发体验主要取决于开发时的难度和维护时的难度
声明式的高可维护性在这两点中发挥着至关重要的作用,因此声明式编程才会越来越成为主流.
运行时
运行时runtime,代码运行的过程
在vue框架中表示: 利用render函数将vnode渲染的过程,对应的就是h函数和render函数
编译时
编译时complier: 源代码转换为可执行代码的过程
在vue框架中表示: 将不同的template编译生成对应的render函数,对应的就是框架中的complie函数
运行时+编译时
了解了运行时和编译时的概念,不难发现,vue是一个编译时+运行时的框架.在编译时将template转换成render函数,在运行时通过render函数将vnode渲染
why
那么为什么vue设计成运行时+编译时的框架呢?
要解答这个问题,需要从DOM渲染开始聊:
DOM渲染分为两种:
- 初次渲染:也叫挂载,指
DOM的第一次渲染 - 更新渲染:也叫打补丁,指
DOM的后续更新
挂载过程就是将DOM从无到有的创建出来,更新过程却不只有一种选择:
- 全量更新: 无脑替换容器内所有
DOM - 增量更新:通过
js对比出前后差异,只更新差异的DOM(这个找差异过程需要使用到vDOM)
这两种方式同等量级下(渲染n个DOM/创建n个虚拟DOM对象)在性能上后者是前者的十倍.js的运行速度要比DOM更新速度快的多.
那么回到vue这个话题上,在保证更新渲染性能的前提下(这是必要的):
如果vue使用纯运行时,确实可以使用js来对比差异进行增量更新,那么就需要提供一个非常复杂的虚拟DOM对象供render函数进行使用
如果vue使用纯编译时,没有了运行时,那么对比差异只能在编译时进行,这种方式的灵活性将大大降低
因此,vue采用编译时+运行时来在性能和灵活性之间达到一种平衡
副作用
副作用指的是函数在计算过程中与函数外界产生的可观察交互,在vue中可以这样理解:当我们对数据进行getter和setter时产生的一切后果
setter:
const modify = () => {
msg = 'new msg'
}
在vue中对响应式数据赋值,会引起视图的自动改变,那么这个视图的改变就是这个setter操作引起的副作用
getter:
const msg = 'msg'
在vue中初始化了一个响应式数据,DOM挂载后,视图会显示这个响应式数据的值,那么这个视图的改变就是getter操作引起的副作用
vue的核心模块
现在我们了解了命令式,声明式,运行时,编译时,vue的运行时+编译时,副作用等等一些概念
下面是vue3三大核心模块:
- 响应性:
reactive - 运行时:
runtime - 编译器:
compiler
本次源码解析,主要从这三大模块入手.
模块对应的源码地址:vuejs,其中:
响应性在: 以reactive中
运行时在: 以runtime开头的文件中
编译器在: 以compiler开头的文件中
在一个单文件组件中,三个模块的配合而成的渲染流程为:在reactive模块定义响应式数据,通过compiler模块编译模板生成组件的render函数,在runtime模块将组件通过h转换成虚拟DOM通,最终过render函数渲染
vue3与ts
vue3对ts优良的支持,并不是因为vue3是由ts编写的,ts编写和提供ts类型支持是两码事,ts类型支持需要编写相应的类型,才能在使用的时候得到良好的类型支持,vue3对ts友好主要是因为:
vue3在核心库和插件中提供了完整的ts类型声明- 为组件提供更强大的类型推导,不再需要显示的定义类型
vue3对ts的装饰器做了改进,可以使用装饰器对组件类型进行声明- ...等等
正是vue3对这些方面做了优化,才使得vue3对ts提供友好的支持
环境准备
vue源码下载
-
这里为了方便下载,我把
vue3.2.37版本的代码上传到了个人仓库vue-3.2.37,需要的直接clone即可 -
如果想从官网下载,则通过以下方式
1.拉取官方仓库代码: git clone https://github.com/vuejs/core.git 2.根据3.2.37版本的tag创建新的分支: git checkout tags/v3.2.37 -b v3.2.37
vue项目结构
vue-next-3.2.37
├─ packages//核心代码
│ ├─ compiler-core//重要: 编译器核心代码
│ ├─ compiler-dom//重要: 浏览器相关编译模块
│ ├─ compiler-sfc//单文件组件的编译模块
│ ├─ compiler-ssr//服务端渲染的编译模块
│ ├─ reactivity// 重要: 响应式的核心模块
│ ├─ reactivity-transform// 已过期,不关注
│ ├─ runtime-core// 重要: 运行时核心代码,内部针对不同平台进行了实现
│ ├─ runtime-dom// 重要: 浏览器相关的编译模块
│ ├─ runtime-test// runtime的测试模块
│ ├─ server-renderer// 服务器渲染
│ ├─ sfc-playground// sfc工具,比如:https://sfc.vuejs.org/
│ ├─ shared// 重要: 共享工具类
│ ├─ size-check// 测试运行时包的大小
│ ├─ template-explorer//提供一个线上的测试,https://template-explorer.vuejs.org),用于把 tempalte 转化为 render
│ ├─ vue//重要: 测试实例,打包之后的dist都会放在这里
│ ├─ vue-compat// 用于兼容vue2的代码
│ └─ global.d.ts// 全局ts声明
├─ scripts//配置文件相关,无需关注
├─ test-dts//测试文件相关,无需关注
├─ api-extractor.json// TS的API分析工具
├─ BACKERS.md// 赞助声明
├─ CHANGELOG.md // 更新日志
├─ jest.config.js// 测试相关配置
├─ LICENSE //开源协议
├─ netlify.toml// 自动化部署相关
├─ package.json// npm包管理工具
├─ pnpm-lock.yaml// 使用pnpm下载的依赖包版本
├─ pnpm-workspace.yaml//pnpm相关配置
├─ README.md// 项目声明文件
├─ rollup.config.js// rollup配置文件
├─ SECURITY.md//报告漏洞,维护安全的声明文件
└─ tsconfig.json// ts配置文件
打包并测试
-
安装
pnpm,vue使用的pnpm,所以先安装一下npm i -g pnpm -
安装依赖
pnpm i -
打包,将在
/packages/vue文件夹下生成dist文件夹,这个过程会比较耗时npm run build打包完成之后会在
/packages/vue/dist中生成打包文件 -
编写测试
新建文件夹
packages/vue/examples中新建自己的测试文件夹myTest,新建测试文件reactive.html:编写测试,
open live server,查看效果:<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>Page Title</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <script src="../../dist/vue.runtime.global.js"></script> </head> <body> <div id="app"></div> <script> const { reactive, effect } = Vue const data = reactive({ name: '哈哈哈' }) effect(() => { document.querySelector('#app').innerHTML = data.name }) setTimeout(() => { data.name = '嘿嘿' }, 2000) // 响应式数据将在两秒后修改为嘿嘿,此时页面应自动响应 </script> </body> </html>
如何debugger
开启sourceMap
打包后的文件由于没有sourceMap所以运行测试用例时,浏览器devtool中都是压缩混淆过的文件,为了更好的阅读,首先需要修改vue的打包配置,使打包结果中包含sourceMap文件
分析:
-
来到
package.json查看打包命令,从这里可以看出vue打包时都干了什么"build": "node scripts/build.js"显然,执行打包命令,相当于执行了
scripts下的build.js,找到这个文件,看看内部做了什么 -
进去文件后,搜索
sourceMap,可以找到:... const args = require('minimist')(process.argv.slice(2)) ... const sourceMap = args.sourcemap || args.s ... sourceMap ? `SOURCE_MAP:true` : `` ...从下往上看,如果
sourceMap为true,那么SOURCE_MAP:true就会生效,sourceMap的值又由args决定,那么可以查查这个minimist是哪里来的 -
minimistnpm中该包的示例如下:$ node example/parse.js -a beep -b boop { _: [], a: 'beep', b: 'boop' }也就是说,这个包的作用是从启动命令中获取一些参数
-
回到第2步,看来
sourceMap的值取决于启动命令中sourceMap或者s的值 -
修改一下启动命令
"build": "node scripts/build.js -s", -
重新打包,在浏览器控制台
sources中查看上述写的测试用例,看看是否能看到源码 -
不出意外,可以在
sources中看到多了一个packages文件夹,打开里面就是源码了
debugger
有了源码之后,一切就简单起来了,由于我们的用例是和reactive相关的,在浏览器控制台找到reactive/reactive.ts,搜索reactive函数,然后在函数内部随便点一行,打个断点,刷新页面就可以看到,代码成功进入debugger中
框架雏形搭建
直接clone(推荐)
接下来初始化一下项目结构,之后将边阅读源码,边实现一个简易版的vue,.直接clonemini-vue,然后将目录文件中已经写好的内容删除即可
- 安装依赖
npm i - 运行打包:
npm run dev
不想clone的同学,可以按照以下步骤自行搭建
项目结构
新建一个空文件夹将来的vue就放在这里,起名为mini-vue
packages
├─ compiler-core // 编译器核心模块
├─ compiler-dom // 浏览器相关编译器模块
├─ reactivity // 响应式
├─ runtime-core // 运行时核心模块
├─ runtime-dom // 浏览器相关运行时模块
├─ shared // 共享工具库
├─ vue // 项目入口,测试用例,打包结果
TS
安装:
npm i --save-dev tslib@2.4.0 typescript@4.7.4
生成配置文件:
npx tsc -init
配置ts,在生成的ts.config.js中添加配置:
// https://www.typescriptlang.org/tsconfig,也可以使用 tsc -init 生成默认的 tsconfig.json 文件进行属性查找
{
// 编辑器配置
"compilerOptions": {
// 根目录
"rootDir": ".",
// 严格模式标志
"strict": true,
// 指定类型脚本如何从给定的模块说明符查找文件。
"moduleResolution": "node",
// https://www.typescriptlang.org/tsconfig#esModuleInterop
"esModuleInterop": true,
// JS 语言版本
"target": "es5",
// 允许未读取局部变量
"noUnusedLocals": false,
// 允许未读取的参数
"noUnusedParameters": false,
// 允许解析 json
"resolveJsonModule": true,
// 支持语法迭代:https://www.typescriptlang.org/tsconfig#downlevelIteration
"downlevelIteration": true,
// 允许使用隐式的 any 类型(这样有助于我们简化 ts 的复杂度,从而更加专注于逻辑本身)
"noImplicitAny": false,
// 模块化
"module": "esnext",
// 转换为 JavaScript 时从 TypeScript 文件中删除所有注释。
"removeComments": false,
// 禁用 sourceMap
"sourceMap": false,
// https://www.typescriptlang.org/tsconfig#lib
"lib": ["esnext", "dom"]
},
// 入口
"include": [
"packages/*/src"
]
}
格式化
安装prettier插件,新建配置文件.prettier.cjs,为了减少项目复杂度,eslint这里没有配置
module.exports = {
printWidth: 120,
tabWidth: 2,
useTabs: false,
singleQuote: true,
semi: true,
trailingComma: 'none',
bracketSpacing: true,
quoteProps: 'as-needed',
proseWrap: 'always', // 超过最大宽度是否换行<always|never|preserve>,默认preserve
arrowParens: 'avoid', // 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
requirePragma: false, //无需顶部注释即可格式化
insertPragma: false, //在已被preitter格式化的文件顶部加上标注
trailingComma: 'none', //尾部逗号设置,es5是尾部逗号兼容es5,none就是没有尾部逗号,all是指所有可能的情况,需要node8和es2017以上的环境。(trailingComma: "<es5|none|all>")
bracketSameLine: false, // 将>多行 HTML(HTML、JSX、Vue、Angular)元素放在最后一行的末尾,而不是单独放在下一行(不适用于自关闭元素)<bool>,默认false
singleAttributePerLine: true // 在 HTML、Vue 和 JSX 中强制执行每行单个属性<bool>,默认false
};
模块构建工具
vue使用rollup作为打包工具,我们也是用rollup作为打包工具,他比webpack产生的冗余代码少,非常适合作为库开发的打包工具
根目录新建rollup配置文件rollup.config.js
安装一下插件:
npm i -D @rollup/plugin-commonjs@22.0.1 @rollup/plugin-node-resolve@13.3.0 @rollup/plugin-typescript@8.3.4
"@rollup/plugin-commonjs": "^22.0.1", 将CommonJs模块转换为ES Module
"@rollup/plugin-node-resolve": "^13.3.0", 路径自动补全
"@rollup/plugin-typescript": "^8.3.4" ts支持
配置文件:
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
// 默认导出一个数组,数组的每一个对象都是一个单独的导出配置,https://www.rollupjs.com/guide/big-list-of-options
export default [
{
input: "packages/vue/src/index.ts", //入口文件
output: [
// 出口文件
{
sourcemap: true, // 开启sourcemap
file: "./packages/vue/dist/vue.js", // 出口文件输出地址
format: "iife", // 出口文件为IIFE函数模式
name: "Myvue", // 全局变量名称,类比Vue
},
],
// 插件
plugins: [
// ts 支持
typescript({
sourceMap: true,
}),
// 模块导入的路径补全
resolve(),
// 将 CommonJS 模块转换为 ES2015
commonjs(),
],
},
];
路径映射
方便进行模块导入导出,给路径加一个映射,将packages文件夹映射到@vue,在ts.config.js中添加配置:
"baseUrl": ".",
"paths": {
// 路径映射
"@vue/*": ["packages/*/src"]
}
添加启动命令
在package.json中:
"build": "rollup -c",
"dev": "rollup -c -w", // 开发模式
如何阅读源码
核心思想
以测试用例为主导,只关注核心逻辑,不关注边缘业务,将有限的精力放在更重要的事情上.
边缘业务指: 各种边界判断,当前debugger没有走的if分支,这些对理解用例所涉及的功能模块的实现没有太多帮助
阅读步骤
-
打包生成源码
执行
npm run build,会在packages/vue/dist下生成文件 -
编写想要查看的模块测试文件,如想查看
reactive的实现,编写reactive.html... <script src="../../dist/vue.global.js"></script> ... <script> const { reactive, effect } = Vue debugger; const data = reactive({ name: '哈哈哈' }) effect(() => { document.querySelector('#app').innerHTML = data.name }) setTimeout(() => { data.name = '嘿嘿' }, 2000) </script> -
运行这个
html,打开浏览器控制台,在source中找到reactive.html,打上断点,刷新,即可一步一步调试.在环境准备一节中,已经开启了sourcemap,所以这里可以直接进入源码
OK,环境准备到这样就可以了,下一篇将从
vue3响应式系统的reactive函数,开启正式的源码之旅~