响应式数据的基本实现

  1. vue2中使用Object.defineProperty实现

  2. vue3中使用Proxy实现

  3. 简单原理

    1. 在方法中读取了响应式数据,则触发了数据的get方法,进而将该方法放入桶中
    2. 当更新响应式数据时,从桶中取出所有的方法,逐个执行
    3. 缺陷:如何自动收集方法放入桶中

    当访问响应式数据时

    当访问响应式数据时

当设置响应式数据时

当设置响应式数据时

设计一个完善的响应式系统

上述图例没有将桶与可变数据进行关联,将所有副作用函数都放入了一个桶中,会导致每个属性变化都会导致桶中函数执行

所以需要设计对应关系

函数储存桶的数据结构

函数储存桶的数据结构

  1. WeakMap是一个储存桶的集合,其中key为响应式对象,也就是监听的对象,value为一个Map数据结构的储存桶
  2. Map储存桶中,key为响应式属性的key,value为最终储存关联函数的Set结构

WeakMap、Map、Set之间的关系

这里有一个例子

const map = new Map();
const weakmap = new WeakMap();
(function(){
     const foo = {foo: 1};
     const bar = {bar: 2};
     map.set(foo, 1);
     weakmap.set(bar, 2);
 })()

其中,当闭包函数执行完之后,bar变量将被回收,而foo变量则会继续保存。这是因为WeekMap区别于Map数据,WeekMap对key的持有是弱引用,所以不会影响垃圾回收机制对没有引用的数据进行回收,而foo因为作为key被map引用着,所以不会被回收。

完整的依赖收集代码

// 存储副作用函数的桶
const bucket = new WeakMap()
const obj = new Proxy(data, {
  // 拦截读取操作 
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中 
    track(target, key);
    // 返回属性值 
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用函数从桶里取出并执行
    trigger(target, key);
  }
});

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
  // 没有 activeEffect,直接 return。activeEffect为当前正在执行的函数(如渲染函数)
  if (!activeEffect) return;
  let depsMap = bucket.get(target);
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  deps.add(activeEffect);
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  effects && effects.forEach(fn => fn());
}

分支切换与cleanup

考虑下面的代码

const data = { ok: true, text: 'hello world' } 
const obj = new Proxy(data, { /* ... */ }) 

effect(function effectFn() { 
    document.body.innerText = obj.ok ? obj.text : 'not' 
})