1. 1. Vue3 项目结构
    1. 1.1. pnpm管理monorepo仓库
      1. 1.1.1. 全局依赖包
      2. 1.1.2. 安装子项目的依赖
      3. 1.1.3. 子项目互相依赖
  2. 2. 响应式的实现原理
    1. 2.1. Reactive
    2. 2.2. track
    3. 2.3. trigger
Vue3源码学习——响应式原理(01)

Vue3 项目结构

Vue3 采用的是monorepo的方式进行项目代码管理。其目录结构大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
.
├── package.json
└── packages/ # 这里将存放所有子 repo 目录
├── project_1/
│ ├── index.js
│ ├── node_modules/
│ └── package.json
├── project_2/
│ ├── index.js
│ ├── node_module/
│ └── package.json
...

使用monorepo的优势在于:

  • 代码重用将变得容易:由于所有的项目代码都集中于一个代码仓库,很容易抽离出各个项目共用的业务组件或工具,在代码内引用;

  • 依赖管理将变得简单:由于项目之间的引用路径内化在同一个仓库之中,容易追踪当某个项目的代码修改后,会影响到其他哪些项目。通过使用 lerna 一些工具,可以做到版本依赖管理和版本号自动升级;

  • 统一构建和测试:使用统一的构建配置和流程,减少配置和维护的工作量。此外,可以在整个 Monorepo 中执行统一的测试流程,确保所有项目质量和稳定性。

  • 便于协作和开发:在一个代码仓库中,更容易地浏览、搜索和理解整个项目的代码,便于团队成员之间的协作。Monorepo 还可以促进跨项目的合作和知识共享,提高团队的整体效率和协同能力。

  • 更少的内存:多个项目引用相同的依赖,只需要安装一份依赖即可,减少重复安装节省内存空间

pnpm管理monorepo仓库

实现monorepo仓库管理的方式有很多,这里以pnpm为例,我们需要在项目的根目录下创建pnpm-workspace.yaml文件,告诉 pnpm 包管理目录是 packages

1
2
packages:
- 'packages/*'

依赖的安装与移除的操作与 npm类似,但有一些针对monorepo的特殊场景

全局依赖包
1
pnpm add rollup -w

在安装全局依赖时,在根目录下执行安装命令时,需要加上-w,如果是开发环境的依赖,则加上-wD

安装子项目的依赖
1
pnpm --filter project_1 add vue

安装子包的依赖时,需要加上--filter 后面跟上需要安装依赖的子项目名称,注意这里的项目名称需要与子项目下的package.jsonname对应,而不是文件夹名称

子项目互相依赖
1
pnpm --filter project_1 add project_2 --workspace

假设project_1有依赖project_2所导出的方法,需要在project_1中安装依赖project_2,则命令需要增加--workspace

响应式的实现原理

Reactive

首先,我们知道,vue3 实现响应的底层原理就是使用了 ES6 的Proxy,当我们使用 vue3 的 API reactive时,传入的对象会变成一个Proxy对象,因此我们可以简单的写出reactive实现的一个简单框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export const reactive = (target) => {
const proxy = new Proxy(target, {
get: (target, key, receiver) => {
const result = Reflect.get(target, key, receiver)
// 依赖收集
track(target, key)
// 递归处理对象
if (isObject(result)) {
return reactive(result)
}
return result
},
set: (target, key, newValue, receiver) => {
const result = Reflect.set(target, key, newValue, receiver)
// 派发更新
trigger(target, key)
return result
}
})
return proxy
}

vue3 与 vue2 的响应式更新的原理是相同的,都需要有依赖收集与派发更新的步骤,这两个工作就是由effect.ts文件中的tracktrigger两个函数共同实现的,比较不同的是 vue3 多了个 effect,副作用函数的概念,副作用函数的作用就是传入一个函数,当依赖更新时,触发函数的执行,本质上是用来实现watchcomputed和视图更新的底层实现函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/** 当前被执行的effect */
export let activeEffect
/** effect的执行栈,作用是在嵌套执行effect函数时能正确的获取当前执行的effect函数 */
const effectStack = []

export const effect = (fn) => {
const effect = createReactiveEffect(fn)
effect()
return effect
}

function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
if (!effectStack.includes(effect)) {
try {
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
}
return effect
}

track

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/** 使用WeakMap 数据结构来收集effect */
const targetMap = new WeakMap()
/**
* 依赖收集
* @param {*} target
* @param {*} key
*/
export const track = (target, key) => {
if (activeEffect === undefined) {
return
}
console.log(`触发 Getter 依赖收集 track -> target:`, target, ` key:${key}`)
// 1、获取全局存储的 effect
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
// 存储到全局变量 targetMap
targetMap.set(target, depsMap)
}
console.log('全局收集到的effect -> targetMap:', targetMap)

let deps = depsMap.get(key)

if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
// 将当前执行的effect存入映射,方便之后取值
deps.add(activeEffect)
}

这里存储全局的effect用到的是WeakMap,它的键值必须是对象或者非全局注册的Symbol。存储全局effect用到的键值是我们reactive执行后返回的Proxy对象,当Proxy对象作为WeakMap的键值时,只要它被被垃圾回收后,WeakMap中也会相应删除对应的键值,而不像Map一样会因为引用关系而一直不被回收。

trigger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 依赖触发
* @param {*} target
* @param {*} key
*/
export const trigger = (target, key) => {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
console.log(`触发 Setter 依赖触发 trigger -> target:`, target, ` key:${key}`)
const effects = new Set()
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach((effect) => {
if (effect !== activeEffect) {
effects.add(effect)
}
})
}
}
// 获取对应key的deps,它是个Set对象
add(depsMap.get(key))

const run = (effect) => {
effect()
}
// 遍历执行所有依赖相关的effect函数
effects.forEach(run)
}