vue3-effect的简易实现(2)

136 阅读4分钟

基础 effect

响应式数据的实现思路就是在数据发生变化的时候重新执行数据依赖的上下文,effect的作用就是收集响应式数据依赖的上下文环境,以便在数据发生变化的时候取出来重新执行。

接上一篇的内容, 还是在 packages/reactivity/src 下面新建一个 effect.ts 文件

export let activeEffect = undefined; // 当前正在执行的effect

class ReactiveEffect {
  active = true;

  constructor(public fn) {}

  run() {
    // 不是激活状态
    if (!this.active) {
      return this.fn();
    }

    this.fn();
  }
}

export function effect(fn, options?) {
  const _effect = new ReactiveEffect(fn); // 创建响应式effect
  _effect.run(); // 让响应式effect默认执行
}

之后在 index.ts 中导出该函数

export { reactive } from "./reactive";
export { effect } from "./effect";

在 index.html 中引入该函数

const { reactive, effect } = VueReactivity;
let wbw = {
  name: "wbw",
  age: 7,
  address: { province: "gd", city: "gz" },
  get myName() {
    return this.name;
  },
};
let wbwProxy = reactive(wbw);
// effect函数是支持嵌套的
effect(() => {
  console.log("wbwProxy.age:", wbwProxy.age);
  effect(() => {
    console.log("wbwProxy.name:", wbwProxy.name);
  });
});
// 这个时候改变并不能触发上面的effect重新执行, 因为我们还没有处理依赖关系
setTimeout(() => {
  wbwProxy.age = 2;
}, 5000);

现在解决一下嵌套 effect 的问题, 改造一下 ReactiveEffect

class ReactiveEffect {
  active = true;
  parent = undefined;
  constructor(public fn) {}

  run() {
    // 不是激活状态
    if (!this.active) {
      return this.fn();
    }

    try {
      this.parent = activeEffect; // 当前的effect就是他的父亲
      activeEffect = this; // 设置成正在激活的是当前effect
      return this.fn();
    } finally {
      activeEffect = this.parent; // 执行完毕后还原activeEffect
      this.parent = undefined;
    }
  }
}

依赖收集

class ReactiveEffect {
  active = true;
  deps = []; // 存储dep数组
  parent = undefined;
  constructor(public fn) {}
}

// {对象:{ 属性 :[ dep, dep ]}}
const targetMap = new WeakMap();
export function track(target, type, key) {
  if (activeEffect) {
    // 看看是否已经存在依赖
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
      depsMap.set(key, (dep = new Set()));
    }
    let shouldTrack = !dep.has(activeEffect);
    if (shouldTrack) {
      dep.add(activeEffect);
      activeEffect.deps.push(dep); // 保存dep, 方便后续处理
    }
  }
}

将属性和对应的 effect 维护成映射关系,后续属性变化可以触发对应的 effect 函数重新 run

get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
        return true;
    }
    const res = Reflect.get(target, key, receiver);
    track(target, 'get', key);  // 依赖收集
    return res;
}

触发更新

export function trigger(target, type, key?, newValue?, oldValue?) {
  const depsMap = targetMap.get(target); // 获取对应的映射表
  if (!depsMap) {
    return;
  }
  let effects = depsMap.get(key);
  effects &&
    effects.forEach((effect) => {
      if (effect !== activeEffect) effect.run(); // 防止循环
    });
}

set 函数改造

set(target, key, value, receiver) {
  let oldValue = target[key];
  const result = Reflect.set(target, key, value,receiver);
  if (oldValue !== value) {
    trigger(target, "set", key, value, oldValue);
  }
  return result;
},

分支切换和 clearup

effect(() => {
  // 副作用函数 (effect执行渲染了页面)
  console.log("render");
  document.body.innerHTML = wbwProxy.flag ? wbwProxy.name : wbwProxy.age;
});
setTimeout(() => {
  wbwProxy.flag = false;
  setTimeout(() => {
    console.log("修改name,原则上不更新");
    wbwProxy.name = "bwb";
  }, 1000);
}, 1000);
try {
  this.parent = activeEffect; // 当前的effect就是他的父亲
  activeEffect = this; // 设置成正在激活的是当前effect
  clearupEffect(this); // 运行前先清理再收集
  return this.fn();
} finally {
  activeEffect = this.parent; // 执行完毕后还原activeEffect
  this.parent = undefined;
}
function clearupEffect(effect) {
  let { deps } = effect;
  for (let index = 0; index < deps.length; index++) {
    deps[index].delete(effect);
  }
  deps.length = 0;
}

这里要注意的是:触发时会进行清理操作(清理 effect),在重新进行收集(收集 effect)。在循环过程中会导致死循环。

let effect = () => {};
let s = new Set([effect]);
s.forEach((item) => {
  s.delete(effect);
  s.add(effect);
}); // 这样就导致死循环了

解决的方案, trigger 函数改造

let effects = depsMap.get(key);
if (effects) {
  effects = new Set(effects);
  effects.forEach((effect) => {
    if (effect !== activeEffect) effect.run(); // 防止循环
  });
}

停止effect

如果我们想在某个条件之后停止effect的监听执行,可以这样处理,在 ReactiveEffect 添加一个stop函数

stop() {
    // 改为失活状态, 并且清除所有依赖
    if (this.active) {
      this.active = false;
      clearupEffect(this);
    }
}

改造effect函数

export function effect(fn, options?) {
  const _effect = new ReactiveEffect(fn); // 创建响应式effect
  _effect.run(); // 让响应式effect默认执行

  const runner = _effect.run.bind(_effect); // this确保指向_effect实例, 不然会变成runner
  runner.effect = _effect;
  return runner;
}

在外面就可以这样调用

let wbw = {
  name: "wbw",
  age: 7,
};
let wbwProxy = reactive(wbw);
let runner = effect(() => {
  console.log("render");
  wbwProxy.age;
});
runner.effect.stop();
setTimeout(() => {
  runner();
  setTimeout(() => {
    wbwProxy.age = 1;
  }, 3000);
}, 3000);

调度器

trigger触发时,我们可以自己决定副作用函数执行的时机、次数、及执行方式

export function effect(fn, options) {
  const _effect = new ReactiveEffect(fn, options.scheduler); // 加入scheduler函数
}

class ReactiveEffect {
  constructor(public fn, public scheduler) {} // 添加到属性中
}

// trigger的时候判断是否有传入的scheduler, 有就执行, 没有就还是走run函数
if (effects) {
  effects = new Set(effects);
  effects.forEach((effect) => {
    if (effect !== activeEffect) {
      if (effect.scheduler) {
        effect.scheduler();
      } else {
        effect.run();
      }
    }
  });
}

可适用于以下的场景,不想频繁触发变动的时候

let waiting = false;
let wbw = {
  name: "wbw",
  age: 7,
};
let wbwProxy = reactive(wbw);
let runner = effect(
  () => {
    console.log("render");
    wbwProxy.age;
  },
  {
    scheduler() {
      if (!waiting) {
        waiting = true;
        setTimeout(() => {
          runner();
          waiting = false;
        }, 3000);
      }
    },
  }
);
wbwProxy.age = 1;
wbwProxy.age = 2;
wbwProxy.age = 3;

总结

effect的触发流程

  1. 创建ReactiveEffect的实例的时候会接收一个我们传入的fn
  2. 把实例赋值给全局的activeEffect,然后执行fn函数,这时候里面如果有响应式对象reactive或者是ref,读取属性的时候自动会进入该属性的get函数中触发track函数的依赖收集
  3. 在targetMap的缓存数据中就会存在该对象属性对象的activeEffect
  4. 等到下次对对象的属性进行设置时,触发trigger函数就会取出该属性对应的activeEffect.run方法执行,也就是当初的我们传入的fn执行,就形成了一次更新闭环。

effect执行流程.png