理解&实现(二):Vue3 响应式原理

1,289 阅读3分钟

Vue3 使用 Proxy 对象重写响应式系统。相比 Vue2有以下不同:

  • 多层属性嵌套,在访问属性过程中才会处理下一级属性(响应式性能提升)
  • 默认监听动态添加的属性
  • 默认监听属性的删除操作
  • 默认监听数组索引和length属性
  • 可以作为单独的模块使用

响应式系统关键API:

  • reactive: 响应式处理对象或数组
  • ref:响应式处理基本类型数据
  • toRefs:将proxy代理的对象的所有属性值,都变成响应式数据

reactive

  • 参数只能是对象或数组 (简单类似使用ref)
  • 修改属性,响应式。重新赋值,不是响应式,需要再次使用 reactive 处理
  • 得到的响应式对象不能解构(解构用toRefs)
/**
 * 响应式处理对象或数组
 * 1. 判断参数,如果不是对象或数组直接返回。
 * 2. proxy 代理 target。
 *    get 收集依赖,如果属性值是对象,需要递归处理。返回处理后的属性值。
 *    set 属性值变化,触发更新。如果新属性值是对象,需递归处理。返回boolean。
 *    deleteProperty 对象属性存在,成功删除后,触发更新。返回boolean。
 * @param { object, array } target
 * @return { proxy }
 */

const isObject = (val) => val !== null && typeof val === "object";
const isArray = (val) =>
  Object.prototype.toString.call(val) === "[object Array]";
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);

const convert = (target) => (isObject(target) ? reactive(target) : target);

export function reactive(target) {
  if (!isObject(target)) {
    return target;
  }

  const handler = {
    get(target, key, receiver) {
      // 收集依赖
      track(target, key);
      const result = Reflect.get(target, key, receiver);
      return convert(result);
    },
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver);
      if (value === oldValue) return true;

      const result = Reflect.set(target, key, convert(value), receiver);
      // 触发更新
      trigger(target, key);
      return result;
    },
    deleteProperty(target, key, receiver) {
      const hasKey = hasOwn(target, key);
      const result = Reflect.deleteProperty(target, key, receiver);
      if (hasKey && result) {
        // 触发更新
        trigger(target, key);
        console.log("delete", key);
      }
      return result;
    },
  };

  return new Proxy(target, handler);
}

/**
 * watchEffect 底层调用的方法
 * 在callback() 访问响应式对象属性,收集依赖
 * @param { function } callback
 */
let activeEffect = null;
export function effect(callback) {
  activeEffect = callback;
  callback();
  activeEffect = null;
}

/**
 * 收集依赖
 * @param {object, array} target
 * @param {string} key
 */
let targetMap = new WeakMap();
export function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
  dep.add(activeEffect);
}

/**
 * 触发更新
 * 先根据target、key找到回调集合,遍历执行。
 * @param {object, array} target
 * @param {string} key
 */
export function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (!dep) return;
  dep.forEach((cb) => {
    cb();
  });
}

ref

  • 参数为基础类型,内部创建具有value属性的对象,value属性具有getter和setter。
  • 参数为对象,就相当于reactive(obj) 返回代理对象
  • 返回的对象,重新赋值成对象,也是响应式的。
/**
 * 对原始数据类型进行响应式处理
 * 1. 如果是带有__v_isRef的对象,直接返回
 * 2. 使用convert对raw进行递归的reactive处理,得到value
 * 3. 返回的对象,__v_isRef = true,value属性 获取会收集依赖,设置会触发更新。
 * @param {string, boolean, number} raw
 * @return {object}
 */
export function ref(raw) {
  if (isObject(raw) && row.__v_isRef) return;

  let value = convert(raw);

  const r = {
    __v_isRef: true,
    get value() {
      track(r, "value");
      return value;
    },
    set value(newValue) {
      if (newValue !== value) {
        value = convert(newValue);
        trigger(r, "value");
      }
    },
  };

  return r;
}

toRefs

  • 参数为 proxy代理的对象
  • 将proxy代理的对象的所有属性值,都变成响应式数据。可解构。
/**
 * 将响应式对象所有属性,变成ref
 * @param {*} proxy
 */
export function toRefs(proxy) {
  const result = isArray(proxy) ? [] : {};
  for (let key in proxy) {
    result[key] = toRef(proxy, key);
  }
  return result;
}

/**
 * 将 响应式对象 指定的属性,变成ref
 * 1. 返回的对象解构与ref返回的类似
 * 2. 无需收集依赖和触发更新,是因为 proxy 本来是响应式的
 * @param {proxy} proxy proxy实例
 * @param {string} key
 */
export function toRef(proxy, key) {
  return {
    __v_isRef: true,
    get value() {
      return proxy[key];
    },
    set value(newValue) {
      proxy[key] = newValue;
    },
  };
}