vue3响应式实现原理(1)

511 阅读5分钟

vue的响应式实现过程:

  1. 通过proxy返回代理后的对象,在proxy中有get set可以拦截对数据的获取和修改
  2. 当在get中拦截,代表要执行与数据相关的操作。使用effect函数将副作用函数收集到一个桶里
  3. 当在set中拦截,代表要重新执行与数据对应的副作用函数(从桶中取)

通过effect函数又可以实现调度器,计算属性,watch

proxy的代理又会介绍

  1. 如何代理对象
  2. 如何代理数组
  3. 如何代理set map
  4. 如何代理简单数据类型(ref toRef toRefs) 举个简单的例子,你在页面上渲染了一个值,过了一秒修改这个值,你希望他做什么呢,也就是执行副作用 render将页面重新渲染一遍。你第一次进入页面,就是effect函数给你注册副作用函数的时机。这个时候副作用函数会自动执行一遍,这就是为什么第一次进页面没有做什么,只是在注册副作用函数,就能渲染出来的原因

proxy effect bucket

先解释一下这几个东西,proxy是用来监听对象的get set 操作,effect是注册副作用函数的函数,bucket是存放副作用的容器,即在副作用函数与被操作的目标字段之间建立明确的联系。这个副作用函数是用来做什么的?举个例子 :在页面上我渲染了一个值,之后我定时一秒后修改他的值,所以页面应该更新是吧。更新应该执行副作用 render(newVode,oldVode,container)即重新渲染页面。

image.png

这是一个比较基础的实现

const data = {
  name: "July",
  age: "22",
};
//开始代理
const proxyData = new Proxy(data, {
  get(target, key) {
    //收集依赖
    if (!activeEffect) return target[key];
    let deps = bucket.get(target);
    if (!deps) bucket.set(target, (deps = new Map()));
    let depsSet = deps.get(key);
    if (!depsSet) deps.set(key, (depsSet = new Set()));
    depsSet.add(activeEffect);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    //触发副作用
    let effects = bucket?.get(target)?.get(key);
    effects &&
      effects.forEach((fn) => {
        fn();
      });
  },
});

let activeEffect; //全局变量-存储被注册的副作用函数
let bucket = new WeakMap(); //副作用容器
function effect(fn) {
  //注册副作用函数的函数
  activeEffect = fn;
  fn(); //执行了才会被收集
}
effect(() => {
  console.log(proxyData.age); //修改的是代理对象的值,不是原始值
});
proxyData.age = 18;

去除不必要的副作用函数

在上面我们只能添加上对应的副作用函数,但是如果数据改变,可能之前的副作用函数就应该舍弃,例如:name修改后age对应的副作用函数集合就不应该包含这个副作用

effect(() => {
  console.log(proxyData.name==="July"?proxyData.age:'not'); 
});
proxyData.name='Anna';
proxyData.age=100;//这一次不应该触发上面的副作用

思路:当触发副作用函数前,应该断开他在桶中所有对应的连接。因为可以消除不需要建立的连接,触发后又会重新建立连接,这个新建立的连接才是正确的。实现方法:给副作用添加一个属性deps,他是一个数组,数据项是一个个set集合(包含当前副作用函数的依赖集合)。

//修改effect函数
function effect(fn) {
//把fn在封装一层
    const effectFn = () => {
        activeEffect = effectFn;//在fn执行之前执行
        fn()
    }
    effectFn.deps=[]//添加要记录的属性
    effectFn(); 
}

//修改get
  get(target, key) {
    if (!activeEffect) return target[key];
    let deps = bucket.get(target);
    if (!deps) bucket.set(target, (deps = new Map()));
    let depsSet = deps.get(key);
    if (!depsSet) deps.set(key, (depsSet = new Set()));
    activeEffect.deps.push(depsSet);//新增,将依赖存进去
    depsSet.add(activeEffect);
    return target[key];
  },
  
 //修改set
    set(target, key, newVal) {
    target[key] = newVal;
    let effects = bucket?.get(target)?.get(key);
    const effectsToRun = new Set(effects);//不这么写会一直死循环
    effects &&
      effectsToRun.forEach((effectFn) => {
        let deps = effectFn.deps;
        deps.forEach((item) => {
          item.delete(effectFn);//新增,去除所有依赖
        });
        effectFn.deps = [];//新增,重置
        effectFn();
      });
  },

嵌套effect

必须实现嵌套的副作用,比如组件里嵌套了一个子组件,在下面这种情况下,修改name,却会触发子副作用函数。因为执行到子副作用函数activeEffect的值改变了,执行完子副作用函数,activeEffect的值变不回去父函数,于是跟name绑定的是子函数

effect(() => {
    effect(() => { console.log(proxyData.age) })
    console.log(proxyData.name)
});
proxyData.name = "Anna";//打印年龄

解决方法:在effect函数将activeEffect入栈。收集依赖时不直接add activeEffect,而是add新加栈的栈顶,add后出栈。

// effect 栈
const effectStack = [] // 新增

  get(target, key) {
    if (!activeEffect) return target[key];
    let deps = bucket.get(target);
    if (!deps) bucket.set(target, (deps = new Map()));
    let depsSet = deps.get(key);
    if (!depsSet) deps.set(key, (depsSet = new Set()));
    activeEffect.deps.push(depsSet);
    if (effectStack.length !== 0) {//新增
      depsSet.add(effectStack[effectStack.length - 1]);
      effectStack.pop(activeEffect);
    }
    return target[key];
  },
  
function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn;
    effectStack.push(activeEffect);//新增
    fn();
  };
  effectFn.deps = [];
  effectFn();
}

副作用函数内同时触发get set

在下面这种情况下 Maximum call stack size exceeded

首先读取 obj.foo 的值,这会触发 track 操作,将当前副 作用函数收集到“桶”中,接着将其加 1 后再赋值给 obj.foo,此时会 触发 trigger 操作,即把“桶”中的副作用函数取出并执行。但问题是 该副作用函数正在执行中,还没有执行完毕,就要开始下一次的执 行。这样会导致无限递归地调用自己,于是就产生了栈溢出。

effect(() => {
  console.log(proxyData.age++);
});

解决办法很简单

  set(target, key, newVal) {
    target[key] = newVal;
    let effects = bucket?.get(target)?.get(key);
    const effectsToRun = new Set(effects);

    effects &&
      effectsToRun.forEach((effectFn) => {
        let deps = effectFn.deps;
        deps.forEach((item) => {
          item.delete(effectFn);
        });
        effectFn.deps.length = 0;
        activeEffect!==effectFn&&effectFn(); //新增
      });
  },
  
  function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn;
    effectStack.push(activeEffect);//新增
    fn();
    activeEffect=''//新增,否则触发与activeEffect相同的副作用会直接跳过
    //例如
    //最后执行proxyData.age++;就不会触发副作用了
  };
  effectFn.deps = [];
  effectFn();
}