🤖 Vue3响应式系统、watch与computed函数粗略代码实现

49 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情

proxy.mjs

import { activeEffect } from "./effect.mjs";
export var bucket = new WeakMap();

export function track(target, key) {
  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);
  activeEffect.deps.push(deps);
}

export function trigger(target, key) {
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  // 因为执行副作用函数中,会进行清空effects的操作,但副作用函数的执行又会使effects中有新增操作,导致执行Set的forEach循环
  // 类似 const set = new Set([1]);set.forEach(i=>{set.delete(1);set.add(1);console.log('运行中')}) 会循环
  const effectsToRun = new Set(effects);
  effectsToRun &&
    effectsToRun.forEach((effectFn) => {
      // 为了防止 proxy.x ++ 导致无限递归,因为此步骤 = proxy.x = proxy.x +1;
      // 当访问 proxy.x时候,会添加一份副作用函数,proxy.x加一后进行赋值,会触发trigger,然后再次执行这份副作用函数,重复此步骤下去,导致递归爆栈。
      // 解决办法是:当我们执行这个副作用函数时候,此时activeEffect为自身,然后触发trigger,所以只需要在trigger时加一个判断条件。
      if (activeEffect === effectFn) return;
      if (effectFn.options.scheduler) {
        effectFn.options.scheduler(effectFn);
      } else effectFn();
    });
}
export function createProxy(data) {
  const proxy = new Proxy(data, {
    get(target, key) {
      track(target, key);
      return target[key];
    },
    set(target, key, val) {
      target[key] = val;
      trigger(target, key);
      return true;
    },
  });
  return proxy;
}

effect.mjs

export var activeEffect = null;
// 维护一份调用栈,防止收集错副作用函数(一般出现在嵌套的副作用函数之中)。
export var activeEffectStack = [];

const effect = function (fn, options = {}) {
  // 执行后返回对应结果
  const effectFn = () => {
    cleanup(effectFn);
    // 执行此副作用函数时候,将自身推入栈中。
    activeEffect = effectFn;
    activeEffectStack.push(effectFn);
    const res = fn();
    // 执行完后,恢复调用栈。
    activeEffectStack.pop();
    activeEffect = activeEffectStack[activeEffectStack.length - 1];
    return res;
  };
  // 将用户的参数选项、添加至副作用函数
  effectFn.options = options;
  effectFn.deps = [];
  // 如果选项中包含lazy懒加载 第一次不执行
  if (!options.lazy) effectFn();
  // 用户可以手动执行
  return effectFn;
};

// 每次执行副作用函数都清空,与副作用函数相关的依赖,为了防止遗留的依赖被添加进来。
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

export default effect;

computed.js(computed函数实现)

import effect from "./effect.mjs";
import { track, trigger } from "./proxy.mjs";
function computed(getter) {
  let dirty = true;
  let value = null;
  const effectFn = effect(getter, {
    // 开启了懒加载,第一次不执行,只有.value才会进行依赖收集。
    lazy: true,
    scheduler(fn) {
      // 当计算属性依赖的数据变化,同时,obj.value触发,此时dirty为false,手动trigger
      if (!dirty) {
        // 当依赖数据变化,副作用函数执行,此时脏为true(代表下一次不走缓存)。
        dirty = true;
        trigger(obj, "value");
      }
      fn();
    },
  });
  const obj = {
    get value() {
      // 如果为脏,就需要手动执行副作用拿到结果,如果不为脏,就走缓存。(一旦触发了.value 此时dirty就为false)
      if (dirty) {
        value = effectFn();
        dirty = false;
      }
      // 为了防止effect(()=>{console.log(obj.value)})无法收集依赖,手动track。
      track(obj, "value");
      return value;
    },
  };

  return obj;
}

export default computed;

watch.mjs(watch函数实现)

import effect from "./effect.mjs";

function traverse(value, seen = new Set()) {
  // 遍历读取对象,使每个key都有对应的副作用函数
  if (typeof value !== "object" || value === null || seen.has(value)) return;
  seen.add(value);
  for (const k in value) {
    traverse(value[k]);
  }
  return value;
}

export default function watch(source, cb, options) {
  let getter;
  //  过期的副作用,解决竞态问题
  let cleanup;
  function onInvalidate(fn) {
    cleanup = fn;
  }
  // 需要传入旧value和新value
  let oldValue, newValue;
  //   如果传入的source为()=>source这种形式,就记录该key的副作用
  if (typeof source === "function") {
    getter = source;
    // 否则,记录下该对象所有key的副作用
  } else {
    getter = () => traverse(source);
  }
  const job = (fn) => {
    newValue = fn();
    // 执行job时,消除过期内容
    cleanup && cleanup();
    cb(oldValue, newValue, onInvalidate);
    oldValue = JSON.parse(JSON.stringify(newValue));
  };
  //  副作用就是拿到最新的值
  const effectFn = effect(() => getter(), {
    lazy: true,
    // 加入调度器,更新值后传入新值与旧值
    scheduler: job,
  });
  if (options?.immdiate) {
    job(effectFn);
  } else {
    oldValue = effectFn();
  }
}

测试文件

      import effect from "./effect.mjs";
      import computed from "./computed.mjs";
      import { createProxy } from "./proxy.mjs";
      import watch from "./watch.mjs";
      const data = {
        name: "xhj",
        sum: 0,
      };
      const proxyData = createProxy(data);

      // 计算属性
      const sum = computed(() => {
        console.log("computed", proxyData.sum);
        return proxyData.sum;
      });
      // 这边需要使用到计算属性sum,不然不会进行收集依赖
      console.log(sum.value);

      // watch监听
      watch(proxyData, (oldVal, newVal) => {
        console.log("watch", oldVal.sum, newVal.sum);
      });

      proxyData.sum++;

测试结果

image.png

至此,粗略的响应式已实现,欢迎小伙伴们评论区讨论交流~