1. 实现Vue3源码 ——reactive与effect

247 阅读5分钟

vue3与vue2的区别对比

  1. Vue3 响应式是独立的包,叫reactive,基于Proxy实现; Vue2 响应式基于Object.defineProperty;
  2. Vue3中Effect的与Vue2中的Watcher的概念一致。

用例1:

import { effect, reactive } from "@hpstream/reactivity";

const state = reactive({ name: "jw", age: 30 }); 

effect(() => {
  var app = document.querySelector("#app");
  app.innerHTML = state.name + "今年" + state.age + "岁了";
  // console.log(state);
});
setTimeout(() => {
  state.age++;
}, 1000);

需要实现功能如下:

  1. 实现effect函数
  2. 实现reactive函数
  3. 当状态改变是重新执行effect函数

实现effect;

function effect(fn){
  fn();
}

但是如果这么实现的话,我们只能初次渲染,当状态改变是我们无法在进行effect调用了。 于是:

var activeEffect = undefined;
class ReactEffect(){
  public deps = [];
  constructor(public fn) {};
  run(){
    try {
      activeEffect = this;
      return this.fn();
    } catch (error) {
    } finally {
      activeEffect = undefined;
    }   
  }
}

function effect(fn){
   const _effect = new ReactiveEffect(fn);
   _effect.run(); // 默认执行一次;
}

我们创建一个 ReactEffect,方便做收集用;

实现reactive

const isObject = (value) => {
  return typeof value === "object" && value !== null;
};

function reactive(target){
  // 如果targer 不是对象的话,我们就直接返回就行了;
  if(!isObject(target)){
    return targer;
  }
	const proxy = new Proxy(target, {
  get(target, key, receiver) {
    let res = Reflect.get(target, key, receiver);
    if (isObject(res)) {
      // 如果是对象,则进行深度响应式
      return reactive(res);
    }
    return res;
  },
  set(target, key, value, receiver) {
    let oldvalue = target[key];
    let result = Reflect.set(target, key, value, receiver);
    return result;
  },
});
  return proxy;
}

上面是reactive实现的主要代码,但是没有实现与effect进行关联的逻辑,也就是没有实现依赖收集,和修改更新的逻辑。

实现依赖收集:

const proxy = new Proxy(target, {
  get(target, key, receiver) {
    let res = Reflect.get(target, key, receiver);
    track(target, "get", key); // 收集依赖的函数 track;
    if (isObject(res)) {
      // 如果是对象,则进行深度响应式
      return reactive(res);
    }
    return res;
  },
  set(target, key, value, receiver) {
    let oldvalue = target[key];
    let result = Reflect.set(target, key, value, receiver);
    if (oldvalue !== value) {
      // 通知更新页面函数:trigger
      trigger(target, "set", key, value);
    }
    return result;
  },
});

实现track:

const targetMap = new WeakMap(); // 生成弱引用,来标记是否响应式
function track(target, type, key){
  let depsMap = targetMap.get(target)
  if(!depsMap){
    targetMap.set(target,depsMap = new Map())
  }  
  let deps = depsMap.get(key);
  if(!deps){
    deps.set(key,deps = new Set())
  }
  if (!activeEffect) return;
  var sholdEffect = deps.has(activeEffect);// 防止重复添加依赖
  if(!sholdEffct){
    deps.add(sholdEffect); //收集依赖
    activeEffect.deps.push(dep); // 反向收集依赖
    
  }
}

实现trigger:

function trigger(target, type, key, value){
  const depsMap = targetMap.get(target);
  if (!depsMap) return; // 触发的值不在模板中使用
  let deps = depsMap.get(key); // 找到了属性对应的effect
  // deps 里面装的都是effect;
  deps && deps.forEach(effect=>{
     effect.run()
  })
}

上面的代码以最简单的方式现实了依赖收集的实现逻辑,简单的说get的实现收集effect, set 去通知收集的effect实现更新逻辑;

问题

但是上面的代码很脆弱,稍微不注意就出问题了;

问题1:

import { effect, reactive } from "@hpstream/reactivity";

const state = reactive({ name: "jw", age: 30,address:'北京' }); 

effect(() => {
  state.name = 'jw1'
  effect(() => {
     state.age = 31;
  });
  state.address = '上海'// 无法进行依赖收集,因为activeEffect为undefined
});

问题2:

import { effect, reactive } from "@hpstream/reactivity";

const state = reactive({ name: "jw", age: 30,address:'北京' }); 

effect(() => {
   // 由于在effect中触发了;更新逻辑,又会触发更新逻辑,执行当前函数,导致死循环
   state.age = Math.random()
 	 app.innerHTML = state.name + "今年" + state.age + "岁了";
});

问题3:

const state = reactive({ flag: true, name: 'jw', age: 30 })
effect(() => { // 副作用函数 (effect执行渲染了页面)
    console.log('render')
    document.body.innerHTML = state.flag ? state.name : state.age
});
setTimeout(() => {
    state.flag = false;
    setTimeout(() => {
      	// 由于name已经没有使用了,所以应该不更新的,但是还是进行了更新逻辑;
        console.log('修改name,原则上不更新')
        state.name = 'zf'
    }, 1000);
}, 1000)

问题1:产生的原因

由于嵌套使用了effect导致;导致内部effect退出时,activeEffect 丢失,无法找到父effect;

var activeEffect = undefined;
class ReactEffect(){
  constructor(public fn) {};
  run(){
    try {
      activeEffect = this;
      return this.fn();
    } catch (error) {
    } finally {
      // 直接定义成了undefined,导致嵌套,退出的时候,找不到外层的effect了;
      activeEffect = undefined;
    }   
  }
}

function effect(fn){
   const _effect = new ReactiveEffect(fn);
   _effect.run(); // 默认执行一次;
}

// 采用tree的逻辑解决,定义一个父节点

class ReactEffect(){
  public parent = null;
  constructor(public fn) {};
  run(){
    try {
      // 通过递归收集的方式,保证effect 永远是当前的effect;
      this.parent = activeEffect;
      activeEffect = this;
      return this.fn();
    } catch (error) {
    } finally {
      // 直接定义成了undefined,导致嵌套,退出的时候,找不到外层的effect了;
      activeEffect = this.parent;
    }   
  }
}

问题2:产生原因

由于在effect中触发了;修改了响应的值,所以响应的值又会去通知更新,因此导致了死循环

function trigger(target, type, key, value){
  const depsMap = targetMap.get(target);
  if (!depsMap) return; // 触发的值不在模板中使用
  let deps = depsMap.get(key); // 找到了属性对应的effect
  // deps 里面装的都是effect;
  deps && deps.forEach(effect=>{
    	// 如果activeEffect 与更新的effect 是同一个,那么就不用在更新了。
     if(effect !== activeEffect) effect.run()
  })
}

问题3:产生原因

这是应为我们在name已经收集了依赖,所以他能继续响应式,我们在每次触发更新的时候,删除依赖,然后重新才比较合适。

class ReactEffect(){
  constructor(public fn) {};
  run(){
    try {
      activeEffect = this;
      cleanupEffect(this);
      return this.fn();
    } catch (error) {
    } finally {
      // 直接定义成了undefined,导致嵌套,退出的时候,找不到外层的effect了;
      activeEffect = undefined;
    }   
  }
}

// 清楚依赖
function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect; // deps 里面装的是name对应的effect, age对应的effect
  for (let i = 0; i < deps.length; i++) {
    deps[i].delete(effect); // 解除effect,重新依赖收集
  }
  effect.deps.length = 0;
}

但是我们这样子处理会导致页面死循环,需要在分发的那个处理下;

function trigger(target, type: string, key, oldvalue) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return; // 触发的值不在模板中使用
  let effects = depsMap.get(key); // 找到了属性对应的effect

  // 永远在执行之前 先拷贝一份来执行, 不要关联引用
  effects = [...effects]; 生成一个新 set;
  if (effects) {
    // triggerEffects(effects);
    effects.forEach((effect) => {
      if (effect !== activeEffect) effect.run(); // 防止循环
    });
  }
}

关于为啥会产生死循环,是一个set遍历导致的结果;所以需要复制出来一个新的,这里由于代码太过分散了不好理解,我重新模拟了一个案例:

let age, effect;
class Effect {
  constructor(public fn) {}
  deps = [];
  run() {
    const { deps } = this; // deps 里面装的是name对应的effect, age对应的effect
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect); // 解除effect,重新依赖收集
    }
    this.deps.length = 0;
    this.fn();
  }
}

function fn() {
  effect.deps.push(age);
  age.add(effect);
  // this.fn();
}
age = new Set();
effect = new Effect(fn);
effect.run();
// 循环一直不结束
age.forEach((effect) => {
  effect.run(); // 由于run函数删除了age,又重新添加了age所以导致循环不会停止
});

上面的代码手动模拟依赖收集的问题,也会触发循环。

我们复制一份就不会产生死循环了。

// 解决办法
var age1 = new Set(age);
age1.forEach((effect) => {
  // 导致循环停不下来
  effect.run(); // 由于run函数删除了age,又重新添加了age所以导致循环不会停止
});

这是我对于reactive与effect的理解,git 仓库如下:

https://github.com/hpstream/vue-source

感兴趣的可以关注下,后面会继续更新