技术咨询、项目合作、广告投放、简历咨询、技术文档下载 点击这里 联系博主

# Vue3响应式原理

声明:文章大部分内容转载自:Vue3 源码入门,实现简易版 reactivity (opens new window);其中加上了部分个人的理解

# 前言

Vue 3.0 中的 reactive 和 Vue 2.6 中提供的一个全局 API Vue.observable  相同,都是用于让一个对象可响应,首先来对比一下他们之间的差异:

reactive Vue.observable
基于 Proxy  实现 基于 Object.defineProperty  实现
对  代理对象 进行操作 直接操作  源对象
返回一个可响应的  代理对象 返回一个可响应的  源对象

# reactivity 工作流程

在 Vue 3.0 中, reactivity 被独立出来,没有任何依赖,可以用于任何想做响应式数据的地方 先抛开源码中实现的复杂判断,来看一下他的主要工作流程

# 关键方法实现

本文主要实现了其中几个关键的方法

# reactive

  • 创建响应式对象,在 Proxy  中定义 get  及 set  捕获器,对传入的 源对象代理对象 进行拦截处理
  • get  捕获到当前对象的属性也是对象,要进行递归
  • 定义基于 WeakMap  的 reactiveMap  管理代理对象,如果传入的 object  已经有记录,直接返回 此对象代理对象,如果没有,按照正常流程走
/**
 * 处理器对象,定义捕获器
 */
const baseHandlers = {
  set(target, key) {
    Reflect.set(...arguments);
    // 依赖触发
    trigger(target, key);
  },
  get(target, key) {
    // 依赖收集
    track(target, key);
    return typeof target[key] === "object"
      ? reactive(target[key])
      : Reflect.get(...arguments);
  },
};

/**
 * 定义响应式对象,返回proxy代理对象
 * @param {*} object
 */
function reactive(object) {
  if (reactiveMap.has(object)) return reactiveMap.get(object);

  const proxy = new Proxy(
    object,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  );

  reactiveMap.set(object, proxy);
  return proxy;
}

# handles 的类型

在对象类型中,将ObjectArrayMap,Set, WeakMap,WeakSet区分开来了。它们调用的是不同的Proxy Handle

  • baseHandlers.tsObject & Array会调用此文件下的mutableHandlers对象作为Proxy Handle
  • collectionHandlers.tsMap,Set, WeakMap,WeakSet会调用此文件下的mutableCollectionHandlers对象作为Proxy Handle
/**
 * 对象类型判断
 * @lineNumber 41
 */
function targetTypeMap(rawType: string) {
  switch (rawType) {
    case "Object":
    case "Array":
      return TargetType.COMMON;
    case "Map":
    case "Set":
    case "WeakMap":
    case "WeakSet":
      return TargetType.COLLECTION;
    default:
      return TargetType.INVALID;
  }
}

会在new Proxy的根据返回的targetType判断。

const proxy = new Proxy(
  target,
  targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
);

# effect

  • 副作用,创建用于管理 effect  的栈 effectStack ,将 effect  先入栈用于依赖收集,执行一次该 effect ,进入 get  捕获阶段,捕获完毕之后进入 finally  将其在栈中移出

vue 源码: effect.ts (opens new window)

/**
 * 副作用函数
 * @param {*} fn
 */
function effect(fn) {
  try {
    // 将需要执行的effect入栈,用于依赖收集过程中与key的关系对应
    effectStack.push(fn);

    // 执行该effect,进入proxy的get拦截
    return fn();
  } finally {
    // 依赖收集完毕及所有get流程走完,当前effect出栈
    effectStack.pop();
  }
}

# track

  • effect  执行后数据触发 get  捕获器, 在此过程中调用 track  进行依赖收集
  • 定义 targetMap ,以 WeakMap  的方式收集依赖,管理目标对象 target  及其对应的 key
  • 第二层用于管理 key  及其对应的 effect ,上面流程图可以看到数据的结构和层次划分

vue 源码: effect.ts (opens new window)

/**
 * 依赖收集
 * @param {*} target
 * @param {*} key
 */
function track(target, key) {
  // 初始化依赖Map
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }

  // 第二层依赖使用Set存放key对应的effect
  let dep = depsMap.get(key);
  if (!dep) {
    targetMap.get(target).set(key, (dep = new Set()));
  }

  // 取当前栈中的effect存入第二层依赖中;
  const activeEffect = effectStack[effectStack.length - 1];
  activeEffect && dep.add(activeEffect);
}

为什么取effectStack[effectStack.length - 1]就可以保证当前对象一定在effect函数中执行呢?

原因:

  1. 因为代码按照顺序执行,当执行到effect(()=>{})函数的时候,effect 会把函数压入堆栈末尾,并执行对应的函数;
  2. 当执行到 effect 函数中的代码,回去对应的值的时候会触发 proxyget,并进行依赖收集
  3. 此时effectStack末尾的 effect 函数就是依赖于当前对象的

# trigger

  • 修改在 effect  中指定过的内容时会触发 set  捕获器,在此过程中 trigger  负责执行当前 target  下 key  对应的 effect ,完成响应式的过程

Vue3源码: trigger (opens new window)

/**
 * 触发响应,执行effect
 * @param {*} target
 * @param {*} key
 */
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (depsMap) {
    const effects = depsMap.get(key);
    effects && effects.forEach((run) => run());
  }
}

# computed

  • 这里采用简单粗暴的方式,直接返回一个 effect

vue 源码地址 (opens new window)

/**
 * 计算属性
 * @param {*} fn
 */
function computed(fn) {
  return {
    get value() {
      return effect(fn);
    },
  };
}

# 效果展示

# 对象属性响应式,多层嵌套

const object = {
  o: {
    a: 1,
  },
};
const proxy = reactive(object);

effect(() => {
  console.log(`proxy.o.a: ${proxy.o.a}`);
});
  • 首次调用打印一次,重新赋值后再次响应,调用 effect

(opens new window)

# 响应式调色器

  • 配置响应式对象,指定其 rgb 属性,顺便测一下 computed
const object = {
  r: 0,
  g: 0,
  b: 0,
};
const proxy = reactive(object);
const computedObj = computed(() => proxy.r * 2);

effect(() => {
  const { r, g, b } = proxy;
  document.getElementById("r").value = r;
  document.getElementById("b").value = b;
  document.getElementById("g").value = g;
  document.getElementById(
    "color"
  ).style.backgroundColor = `rgb(${r},${g},${b})`;
  document.getElementById("color_text").innerText = `rgb:${r},${g},${b}`;

  const { value } = computedObj;
  document.getElementById(
    "computed_text"
  ).innerText = `computed_text: r*2=${value}`;
});
  • 拖动 rgb 3 个 range 时各自的变化会体现在颜色块上

  • 对象依赖关系 targetMap  结构如下

  • reactiveMap 结构如下

# 总结

  • 通过上述内容可以了解到 Vue 3.0 中的响应式原理
    • reactive  创建响应式对象
    • effect  副作用,调用自身收集依赖,数据变更后重新调用该函数
    • track  依赖收集
    • trigger  触发依赖中对应的 effect
    • computed  计算属性,对应属性值变更调用其 effect
  • 过程中还能更加熟悉一些前置基础知识
    • 代理与反射: Proxy Reflect
    • js 标准内置对象: WeakMap Map Set
    • 语句执行使用技巧: try finally

# 参考资料

声明:文章大部分内容转载自:Vue3 源码入门,实现简易版 reactivity (opens new window);其中加上了部分个人的理解

【未经作者允许禁止转载】 Last Updated: 2/4/2024, 6:06:40 AM