前置知识
什么是响应式?
响应式就是当我一个数据发生变动了,我需要做一些额外的事情。这个就是数据响应式。好比如说以下代码。
let a = 1;
let b = 2;
let c = a + b;
现在有一个要求“当a或者b变动时,需要动态更新c的值”,但是在原生js中没办法做到这一点。
所以为了办到这一点我们需要封装一个函数来计算c的值。
let c;
function update() {
c = a + b;
}
到现在,就可以引出两个术语了:
- 副作用(Effect):
update函数改变了程序里的状态,我们把这个现象称为副作用。 - 依赖(Dependency):
update函数里的a和b被视为这个副作用的依赖。也可以说这个副作用函数是这两个变量的订阅者。
Reactive api
如何把一个数据变成响应式?
在原生js中,我们一个函数没有办法动态监控一个变量的值是否发生改变。以函数的视角就是这样子:”我是一个函数,你这个变量是谁啊,不认识你。我在运行之前我都不知道会发生什么,运行之后我也不会记得你“。
虽然我们没有办法监控变量的变化,但是可以监控对象属性的变化,通过getter/setter的方式可以在属性被访问和被赋值时可以被拦截下来做一些其他事情。在ES6推出以后拥有了一种效率更高的方式来监听数据的变动,通过Proxy()返回一个对象代理,可以通过配置,对对象的操作进行监控拦截,从而可以达到数据响应式的功能。
把一个对象变成响应式,vue3中主要使用两种方式reactive和ref。使用reactive方式默认会使用Proxy代理的方式来实现响应式,而ref就还是使用的getter/setter的方式。
如果使用的时options api那么默认还是使用的
getter/setter的模式
reactive
reactive会返回一个对象代理拦截get和 set
// 伪代码解释
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
...
},
set(target, key, value) {
...
}
})
}
ref
ref会把值封装到一个实例对象中进行返回
// 伪代码解释
class RefImpl {
constructor(value) {
this._value = value;
}
get value() {
return this._value;
}
set value(newVal) {
this._value = newVal;
}
}
function ref(value) {
return new RefImpl(value);
}
当使用上面者两个api之后就可以将一个数据变成响应式了,这个时候当数据发生变动时,我们就可以有机会做一些其他事情。
dep
当数据被访问时需要干什么?当数据被修改时需要干什么?
这个到时候dep就闪亮登场了,它可以有如下的功能:
- 数据被访问时,进行依赖收集
- 数据被修改时,通知所有订阅者
当get时,会为这个响应式数据就生成一个对应的dep,里面存储着所有的订阅者。当set时会从这个dep中通知所有的订阅者。就是给每个对象里面的每个属性都加上一个dep用来存储依赖。
由于vue3中定义响应式系统有两种方式,收集依赖的方式都是一样的,但是存储依赖的地方有差别。
reactive deps
通过reactive可以得到一个对象代理。
当访问对象代理的某个属性时,会进行一次依赖的收集,给这个属性映射一个dep用来存储订阅者。通过track函数将副作用对象收集进dep。dep是一个Set() 数据结构。
在访问属性时会调用track函数,该函数主要用于收集依赖。在vue内部会创建一个全局变量targetMap,里面存储了每个响应式对象的每个属性对应的dep,每一个dep又对应着自己的所有订阅者。track函数主要往这个全局对象里面记录东西。
当数据发生变动的时候会调用trigger方法去找到对应的deps,去通知所有的副作用对象。
track函数伪代码:
/* 对于存储响应式对象与副作用操作,大概存储如下结构
{
target1: {
key1: [],
key2: []
},
target2: {
key1: [],
key2: []
}
}
*/
const targetMap = new WeakMap();
// 正在运行的副作用函数,后续会做解释
let activeEffect;
function track(target, type, key) {
// 1. 根据target获取存储在全局变量中targetMap中的deps,deps是一个Map数据类型,获取不到则创建一个
let depsMap = targetMap(target);
if(!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 2. 根据key获取deps中的一个dep,dep是一个Set数据类型,获取不到则创建一个
let dep = depsMap.get(key);
if(!dep) {
dep = new Set();
depMap.set(key, dep);
}
// 3. 将当前正在运行的副作用函数,添加进dep
dep.add(activeEffect);
}
trigger函数伪代码:
function trigger(target, type, key, newValue, oldValue, oldTarget) {
// 1. 获取到当前对象对应的map
const depsMap = targetMap.get(target);
// 2. 获取对应的deps
const deps = depsMap.get(key);
// 3. 循环执行所有副作用任务
for(const effect of deps) {
effect.run();
}
}
ref deps
通过ref()会得到一个对象{value: 0},我们通过.value就可以访问到这个响应式数据。
其实这个对象中,除了value还有其他的属性,其中一个就是dep,这个dep也是用来存储副自己所有的订阅者。
因为现在没有人用到这个数据,所以dep为undefined。
现在如果有人访问我这个响应式数据的时候,就会将依赖收集进这个对象的dep属性中去,通过调用trackRefValue方法。
数据发生变动时,也从该对象中的dep中通知所有的订阅者。
trackRefValue方法伪代码。当访问ref对象的get时,会将this传入该函数
function trackRefValue(refObj) {
if(!refObj.dep) {
refObj.dep = new Set();
}
refObj.dep.add(activeEffect);
}
triggerRefValue方法伪代码
function triggerRefValue(ref, newVal) {
const dep = ref2.dep;
if(dep) {
for(const effect of deps) {
effect.run();
}
}
}
reactiveEffect
track函数时如何追踪到依赖的?
或者换一个问法:一个属性怎么知道时哪个函数使用到了自己?
如下代码:
const countRef = ref(0);
computed(() => {
return count * 2;
})
这个countRef怎么知道是computed里边的回调函数使用到了自己呢?
vue使用了一个很巧妙的方法,那就是先定义一个全局变量activeEffect,在执行这个回调的时候不是直接执行,而是把这个回调函数封装成一个副作用任务对象。
当触发这个副作用对象时,先将全局变量activeEffect = this,就是把自己赋值给activeEffect,这个时候响应式数据里边的track方法就可以通过检查activeEffect来进行依赖收集。
通过reactiveEffect类来创建副作用任务对象。
当任务被触发时,会调用该对象的 .run() 方法
reactiveEffect伪代码:
let activeEffect;
class ReactiveEffect {
constructor(fn) {
this.fn = fn;
}
run() {
activeEffect = this;
return this.fn();
}
}
scheduler
种种原因没有深入,各位大佬可以帮忙解答