本文是专栏的第一篇,主要了解一些基本概念,准备源码调试和实现的环境
一些概念
名词概念
编程范式
命令式
面向过程: 以描述怎么做的形式来实现功能
声明式
面向结果: 以描述做什么的形式来实现功能
声明式是建立在命令式之上的,因为一定是经过了某些过程,才能达到某个结果
命令 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
是哪里来的 -
minimist
npm
中该包的示例如下:$ 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
,.直接clone
mini-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
函数,开启正式的源码之旅~