文章目录
- 【Vue3源码】第一章 effect和reactive
- 前言
- 1、实现effect函数
- 2、封装track函数(依赖收集)
- 3、封装reactive函数
- 4、封装trigger函数(依赖触发)
- 5、单元测试
【Vue3源码】第一章 effect和reactive
前言
今天就正式开始Vue3源码学习了,那么很多初学者(包括我)在看vue源码时都会非常迷茫不知从何下手,所以我们在学习源码时应该反其道而行之,剔除掉源码中包括Tree-Shaking、TypeScript类型约束、特性开关、错误处理等操作,只了解其核心原理,大大提高学习效率减少学习成本!
如果你还不了解Vue源码设计“Tree-Shaking、TypeScript类型约束、特性开关、错误处理”?
可以购买一本《Vue.js设计与实现》,在第 2 章 “框架设计的核心要素” 有详细介绍了这些操作,并且有举例子解释了为什么要这么设计。
好了!正式开始学习,想要了解vue3的源码,我们可以先从reactivity文件夹入手。
那么reactivity文件夹是什么呢??
Reactivity:里面写的是vue最最重要的功能那就是“vue被人津津乐道并且顶顶大名的响应式源码“。
今天我们就简单的从零开始实现一遍effect函数(影响,效果),reactive函数(代理或者劫持)。
在reactive函数中还包括两个功能依赖收集和依赖触发,他们分别是track函数(依赖收集)和trigger函数(依赖触发),在今天的文章中我都会一一实现一遍。
1、实现effect函数
effect可以说是响应式系统的核心,所以我们学习vue3源码时推荐从effect入手。
如果你还没有搭建好jest单元测试环境,可以查看我的上一篇文章
- 首先我们新建一个effect.ts文件并且封装一个effect函数
//用来暂存effect传递的参数fn
let activeEffect;
export const effect = (fn) => {
activeEffect = fn
fn()
};
为什么我们还要在函数定义一个activeEffect
块级作用域变量呢?
因为activeEffect
要帮我们暂存用户传进来的fn参数,fn的类型是一个函数,那么fn暂存到了activeEffect
变量后为了不让下次传递的参数覆盖掉了我们的之前的fn,Vue就该把它存到一个“仓库”里保存着,方便后续管理,这个过程叫依赖收集。
我们再优化一下代码方便后续管理。
class ReactiveEffect {
private _fn;
constructor(fn) {
this._fn = fn;
}
run() {
activeEffect = this;
this._fn();
}
}
export const effect = (fn) => {
const _effect = new ReactiveEffect(fn);
_effect.run();
};
那么怎么才能做到依赖收集呢?往下看⬇️
2、封装track函数(依赖收集)
在effect.ts文件中封装track函数
为了方便理解,我把targetMap
抽象的比做一个仓库方便大家理解,作为一个仓库,自然就得有仓库的管理规则,货物进来总不能不分类就乱七八糟直接存进去吧?
所以我们就根据传递进来的参数,target(一个对象),key(该对象中的key)作为仓库的类别,一一细化分类这个仓库里包含的货物。
//targetMap就是一个收集effect传递过来的参数fn的“总仓库”
const targetMap = new WeakMap();
export function track(target, key) {
//根据target一级分类
let depsMap = targetMap.get(target);
//如果这个货物是第一次存,我们就新建这个分类
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
//根据key二级分类
let dep = depsMap.get(key);
//如果这个货物是第一次存,我们就新建这个二级分类
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
//最后把收集到的activeEffect存入这个细化的分类里
dep.add(activeEffect);
}
所以targetMap
作为一个总仓库,它通过传入的target和key进行分类保存我们的activeEffect。
我画一个drawio图,看一下就可以明白里面的逻辑。
那么我们的track函数接收的target和key参数又是怎么来的???往下看⬇️
3、封装reactive函数
我们都知道,vue3使用的是Proxy
代理对象实现了数据响应式,Proxy(MDN对Proxy的介绍)代理多达13种捕获器它们可以完美的监听到任何方式的数据改变,完美的解决了的vue2使用Object.defineProperty
时监听不到数组下标缺点。
不过说到缺点:真是非常的尴尬。。。不管Vue2的劫持还是Vue3的代理对有深层嵌套的对象还是要用递归去处理并且返回多层的响应式对象。
我们今天的文章只处理没有嵌套关系的对象,不深入多层嵌套对象的递归处理,下次再处理这个逻辑。
极简版reactive函数很简单,return反射的结果之前进行依赖收集即可,传入的target正是代理对象,key是正在使用对象的key。
新建一个reactive文件
import { track, trigger } from "./effect"
export const reactive = (raw) => {
return new Proxy(raw,{
get(target,key,receiver) {
const res = Reflect.get(target,key,receiver)
//do something 收集依赖
track(target,key)
return res
},
set(target,key,value,receiver) {
const res = Reflect.set(target,key,value,receiver)
// do something 触发依赖
trigger(target,key)
return res
},
})
}
4、封装trigger函数(依赖触发)
上一步封装好了reavtive函数的还差最后一个trigger函数我们还没有封装。
其实依赖触发也很简单,通过传入的target和key(货物的分类类别),我们就可以从总仓库里取出收集到的对应依赖,并且再出触发它!
在effect.ts文件中封装trigger
export function trigger(target, key) {
let depsMap = targetMap.get(target);
let dep = depsMap.get(key);
for (let effect of dep) {
effect.run();
}
}
5、单元测试
我们新建在test文件夹下新建一个 effect.spec.ts文件
然后就可以打上断点跟着断点走一遍Vue响应式的执行帮助我们理解Vue的逻辑。
我测试的代码如下:
import { effect } from "../effect";
import { reactive } from "../reactive";
describe("effect", () => {
it("happy path", () => {
const user = reactive({
age: 10,
name:'www',
newObj:{
objAge:11
}
});
let nextAge;
let age2
effect(() => {
nextAge =user.age + 1;
});
//无法代理深层嵌套的对象
effect(() => {
age2 = user.newObj.objAge;
});
expect(nextAge).toBe(11);
user.age++;
expect(nextAge).toBe(12);
user.age = 99
expect(nextAge).toBe(100)
expect(age2).toBe(11)
//对于深层嵌套的对象由于没有封装递归的逻辑所以监听不到
user.newObj.objAge ++
//理论上来说age2应该跟着user.newobj.objeAge响应式变成12,而结果却没有变化
expect(age2).toBe(11)
});
});