基本的响应式数据实现

120 阅读4分钟

1. 响应式数据与副作用函数

const obj = {
  text: "hello world",
};
function effect() {
  document.body.innerText = obj.text;
}
effect();

obj.text = "hello vue";

期望:当改变obj.text的时候,再次执行副作用函数effect

2. 响应式数据的基本实现

观察分析:

  • 当执行effect函数时,会触发obj.textget操作
  • 当修改obj.text的值时,会触发obj.textset操作

思考:如何实现obj.text的set操作时候再次执行effect

如何拦截obj.txt的set操作,然后触发effect?

所以,关键的地方在于对象的读取和设置操作拦截

  • 在Vue2中,使用的是Object.defineProperty()
  • 在Vue3中,使用的是Proxy

通过Proxy拦截set操作,触发effect

const obj = {
  text: "hello world",
};
const newObj = new Proxy(obj, {
  // 在set操作中,赋值,然后调用effect函数
  set(target, key, value) {
    target[key] = value;
    effect();
  },
});

function effect() {
  document.body.innerText = newObj.text;
}
effect();
newObj.text = "hello Vue3";
//页面数据发生变化:hello world --> hello Vue3

3. 设计一个完善的相应系统

如果副作用函数名称不叫作effect,如何动态收集?

封装一个收集副作用的函数,收集的同时也调用副作用函数

let activeFn = undefined;
// 在调用effect时候,将参数fn赋值给activeFn,
// 在set函数中可以直接调用activeFn,而不用关心传入函数的具体的名字
function effect(fn) {
  activeFn = fn;
  fn();
}

const obj = {
  text: "hello world",
};
const newObj = new Proxy(obj, {
  // 在set操作中,赋值,然后调用effect函数
  set(target, key, value) {
    target[key] = value;
    activeFn();
    return true;
  },
});

function effect0() {
  console.log("effect0", "----", newObj.text);
}

effect(effect0);
newObj.text = "hello Vue3";

如果有多个副作用函数,那么需要将副作用函数收集,在触发的时候,依次调用。

使用Set数据结构可以去重

注意:这样看似乎可以在effect注册副作用函数时候收集,也可以在对象的get操作中进行收集

let activeFn = undefined;
const bucket = new Set();
function effect(fn) {
  activeFn = fn;
  fn();
}
const obj = {
  text: "hello world",
};
const newObj = new Proxy(obj, {
  get(target, key){
    // 将副作用函数存储到set中
    if(activeFn){
      bucket.add(activeFn)
    }
    return target[key]
  },
  // 在set操作中,赋值,然后调用effect函数
  set(target, key, value) {
    target[key] = value;
    bucket.forEach((fn) => {
      fn();
    });
    return true;
  },
});

function effect0() {
  console.log("effect0", "----", newObj.text);
}

effect(effect0);
newObj.text = "hello Vue3";

如果有两个响应式数据,那么该如何在修改某个响应式数据的时候触发对应的副作用函数?

使用Map数据结构,以原始对象为key,副作用函数为value进行存储

进阶:使用WeakMap数据结构,当原始对象没有引用时,则在WeakMap清除这个原始对象,不阻碍垃圾回收。

如果一个响应式数据中有多个属性,那么该如何在修改某个属性的时候触发对应的副作用函数?

使用Map数据结构,以原始对象的keykey,副作用函数为value进行存储

所以必须在get操作中收集副作用函数!

上面两种情况合在一起,绘制下面这幅图,清楚表示副作用函数的存储

let activeFn = undefined;
const bucket = new WeakMap();

const obj = {
  text: "hello world",
};

function effect(fn) {
  activeFn = fn;
  fn();
}

const newObj = new Proxy(obj, {
  get(target, key) {
    // 如果没有注册的副作用函数,直接返回值
    if (!activeFn) return target[key];
    // 取出原始对象对应的副作用函数(对象所有的key对应的所有的副作用函数的集合)
    let depMap = bucket.get(target);
    // 如果没有原始对象对应的map则创建并于整个数据结构进行关联
    if (!depMap) {
      depMap = new Map();
      bucket.set(target, depMap);
    }
    // 取出对象key对应的副作用函数的集合
    let deps = depMap.get(key);
    if (!deps) {
      deps = new Set();
      depMap.set(key, deps);
    }
    deps.add(activeFn);
    return target[key];
  },
  // 在set操作中,赋值,然后调用effect函数
  set(target, key, value) {
    target[key] = value;
    
    const depMap = bucket.get(target);
    if (!depMap) return true;
    
    const effects = depMap.get(key);
    if (!effects) return true;
    effects.forEach((fn) => {
      fn();
    });
    return true;
  },
});

function effect0() {
  console.log("effect0", "----", newObj.text);
}

effect(effect0);
newObj.text = "hello Vue3";

代码优化:

  • 将在get中收集依赖的部分封装成一个函数,track
  • 将在set中触发依赖的部分封装成一个函数,trigger
let activeFn = undefined;
const bucket = new WeakMap();

const obj = {
  text: "hello world",
};

function effect(fn) {
  activeFn = fn;
  fn();
}

// 在get中收集依赖
function track(target, key) {
  // 如果没有注册的副作用函数,直接返回值
  if (!activeFn) return target[key];
  // 取出原始对象对应的副作用函数(对象所有的key对应的所有的副作用函数的集合)
  let depMap = bucket.get(target);
  // 如果没有原始对象对应的map则创建并于整个数据结构进行关联
  if (!depMap) {
    depMap = new Map();
    bucket.set(target, depMap);
  }
  // 取出对象key对应的副作用函数的集合
  let deps = depMap.get(key);
  if (!deps) {
    deps = new Set();
    depMap.set(key, deps);
  }
  deps.add(activeFn);
}

// 在set中触发依赖
function trigger(target, key) {
  const depMap = bucket.get(target);
  if (!depMap) return;

  const effects = depMap.get(key);
  if (!effects) return;
  effects.forEach((fn) => {
    fn();
  });
}

const newObj = new Proxy(obj, {
  get(target, key) {
    track(target, key);
    return target[key];
  },
  // 在set操作中,赋值,然后调用effect函数
  set(target, key, value) {
    target[key] = value;
    trigger(target, key);
    return true;
  },
});

function effect0() {
  console.log("effect0", "----", newObj.text);
}

effect(effect0);
newObj.text = "hello Vue3";