Vue3响应式

275 阅读4分钟

vue3响应式 通过测试用例出发, 明白我们要做什么,再着手写代码。

let work = { time: 1 };
console.log(work.time); // 1// 1. 现在想通过一个变量来存储我距离下班还剩下几个小时
let dummy = `距离下班还剩下${8 - work.time}小时`;
console.log(dummy); // 距离下班还剩下7小时// 2. 又工作了一个小时,我们想要 上一个变量 再告诉我们还剩下几个小时下班
work.time++;
dummy = `距离下班还剩下${8 - work.time}小时`; // 3. 需要再次执行语句才能更新dummy的数值

现在可以明确第一个测试需求 : 想要变量 dummy 随着另一个变量 work.time 来改变

结合上面的代码情境也就是 ,

work.time改变时候 , dummy = 距离下班还剩下${8 - work.time}小时 , 这句话要再被执行。

我们把要执行的函数包裹在 effect函数中,每次修改work.time就执行effect函数 , 也就引出了 vue3 中的effect函数

effect

// 1. 最简单地来做 , 每次work.time 一改变 , 手动去触发 effect函数就可以实现上面的需求了
let work = { time: 1 };
let dummy;
function effect() {
  dummy = `距离下班还剩下${8 - work.time}小时`;
  console.log(dummy);
}
work.time++;
effect();  // 距离下班还剩下6小时

进一步 , 要考虑的是如何能够在work.time 改变的时候, 自动地触发 effect 函数 , 替代手动地触发

引入proxy代理来监听 work 对象源的数据变化与访问 ( mdn proxy 教程) , 通过 reactive 包裹 work 对象 , 来监听对该对象的操作。

reactive

function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      console.log('我被访问了')
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      // 1. 我要想办法触发对应的 effect 函数
      console.log('我被改变了,我要做点什么')
      return true;
    },
  });
}
​
const work = reactive( {time : 1} )
work.time++

每次修改 work 对象的属性 , 我们都会访问 proxy 的 set方法 , 就可以在这里去触发effect函数

但是我们只需要 改变work.time 时候,才触发我们要的函数 ,而set函数会在修改任意属性时候都触发,那么如何把effect函数和 work.time(或者其他属性) 进行相应的绑定 , 变成我们要完成的需求。

targetMap

在第一次执行effect函数时候,一定会访问到 proxy 的get方法(dummy = work.time 访问了get方法), 所以可以在get中先存储对应的effect函数, 通过以下的数据结构来绑定 work -> time -> effect

targetMap -- (work对象) --> depMap -- (work对象的time属性) --> dep

如上的存储关系可以通过 work.time 在 targetMap-->depMap -->dep中拿到和 work.time 相关的所有effect函数

那么逻辑就变成了 第一次定义effect函数时候 , 要去触发 proxy的get方法,通过对象属性的区分 把 effect函数放到对应的数据结构中, 在下次该对象的属性修改时候,触发set方法, 再通过对象属性的区分从数据结构中把effect函数拿出来执行, 也就完成了 effect 函数随着变量的改变而重新被执行了

const targetMap = new Map();
export function track(target, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
​
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
    // 通过 target 和 key 拿到了存储对应effect的 dep集合
    // dep (Set) : [()=>{dummy = work.time +1;} , ()=>{dummy = work.time *2 ;} ...]
}

整理集合起来,要实现的代码功能与拆分如下

全流程

// 1. 定义响应式对象work , 每次访问或者修改work的属性都会触发 get / set
let work = reactive({time: 1});
let dummy;
// 2. 把fn传入effect函数 , 跳转到3
effect(()=>{
  // 4. 访问work.time 访问get方法,跳转5
  // 8. 访问get结束 , 拿到work.time的值 , 同时执行完fn,跳转9
  dummy = `距离下班还剩下${8 - work.time}小时`;
  console.log(dummy);
})
​
// 10. (work.time = work.time+1)
// 改变work.time ,先访问get ,在track中发现affectEffect为null,跳出 , 继续访问set , 跳转11
work.time++; // 13. fn重新被执行,输出 距离下班还剩下6小时
# effect.tslet activeEffect;
function effect(fn) {
  // 3. 把fn保存到全局变量activeEffect, 然后执行 fn() , 跳转4
  activeEffect = fn;
  fn();
  // 9. fn执行结束,把全局activeEffect清空(防止effect函数外访问track),effect执行结束 ,跳转10
  activeEffect = null;
}
const targetMap = new Map();
function track(target, key) {
  if (!activeEffect) return;
  // 6. 通过target和key以及全局的activeEffect,把响应函数fn放入到对应的数据结构中,跳转7
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
​
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
​
  dep.add(activeEffect);
}
​
function trigger(target, key, value) {
  // 12. 通过target和key,从数据结构中拿到所有之前存过的dep依赖,执行他们(中间会再触发get,同样通过activeEffect拦截不进入track),跳转13
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
​
  let dep = depsMap.get(key);
  if (!dep) return;
  for (const fn of dep) {
    fn();
  }
}
# reactive.tsfunction reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      // 5. 触发track函数,传入 响应式对象和响应式对象target的属性key ,跳转6
      track(target, key);
      // 7. 返回 target[key]的值 ,跳转8
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      Reflect.set(target, key, value);
      // 11. 触发trigger函数,传入 响应式对象和响应式对象target的属性key ,跳转12
      trigger(target, key, value);
      return true;
    },
  });
}

总结

  1. 定义reactive响应式对象,添加get \ set 劫持
  2. 用effect函数包裹要被重复执行的函数,默认执行一次
  3. 触发get同时把该重复执行的函数放入对应的依赖中
  4. 当响应源改变时,遍历执行对应依赖中的函数

emm这是我第一次写文章,发现写文章的思路和写代码的逻辑还是有许多区别的。想要用文字把代码的执行顺序表达清楚比我想得还是难些(打断点看思路会更清晰些),没啥经验,再接再厉吧。仓库地址 mini-vue