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
的特殊场景
全局依赖包
在安装全局依赖时,在根目录下执行安装命令时,需要加上-w
,如果是开发环境的依赖,则加上-wD
安装子项目的依赖
1
| pnpm --filter project_1 add vue
|
安装子包的依赖时,需要加上--filter
后面跟上需要安装依赖的子项目名称,注意这里的项目名称需要与子项目下的package.json
的name
对应,而不是文件夹名称
子项目互相依赖
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
文件中的track
与trigger
两个函数共同实现的,比较不同的是 vue3 多了个 effect,副作用函数的概念,副作用函数的作用就是传入一个函数,当依赖更新时,触发函数的执行,本质上是用来实现watch
、computed
和视图更新的底层实现函数。
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
| export let activeEffect
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
| const targetMap = new WeakMap()
export const track = (target, key) => { if (activeEffect === undefined) { return } console.log(`触发 Getter 依赖收集 track -> target:`, target, ` key:${key}`) let depsMap = targetMap.get(target) if (!depsMap) { depsMap = new Map() targetMap.set(target, depsMap) } console.log('全局收集到的effect -> targetMap:', targetMap)
let deps = depsMap.get(key)
if (!deps) { deps = new Set() depsMap.set(key, deps) } 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
|
export const trigger = (target, key) => { const depsMap = targetMap.get(target) if (!depsMap) { 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) } }) } } add(depsMap.get(key))
const run = (effect) => { effect() } effects.forEach(run) }
|