Skip to content

Reactive from Vue 2.0 to 3.0

Reactive in Vue 2.0

思想

  • 监听数据,拦截属性,重新 Object.defineProperty
  • 对嵌套的对象进行递归监听
  • 对新增对象,在 set 之前进行监听
  • 拦截数组,劫持方法并重写

代码

// 重写数组方法
//   1. 函数劫持
//   2. 面向切片编程 AOP
let oldArrayPrototype = Array.prototype;
let proto = Object.create(oldArrayPrototype);
['push', 'pop', 'shift', 'unshift'].forEach(method => {
  proto[method] = function() { // 函数劫持
    updateView(); // AOP
    oldArrayPrototype[method].apply(this, arguments);
  }
});

// data 观察者
function observer(target) {
  if (typeof target !== 'object' || target === null) {
    return target;
  }
  if (Array.isArray(target)) { // (3): 拦截数组,重写数组方法
    // Object.setPrototypeOf(target, proto)
    target.__proto__ = proto;
  }
  for(let key in target) {
    defineReactive(target, key, target[key]);
  }
}

function defineReactive(target, key, value) {
  observer(value); // (1): 嵌套对象
  Object.defineProperty(target, key, {
    get() {
      return value;
    },
    set(newVal) {
      if (newVal !== value) {
        observer(newVal); // (2): 对新增对象
        updateView();
        value = newVal;
      }
    }
  });
}

function updateView() {
  console.log('更新视图');
}

let data = {
  count: 100,
  name: { a: 'hello' }, // 递归 observer
  arr: [1, 2, 3]
};
observer(data);

// data.count = 200;

// (1): 嵌套对象
// data.name.a = 'hi'; // (1)

// (2): 对新增对象
// data.name = { b: 'world' };
// data.name.b = 'new world';

// (3): 新增属性响应式
// 重写数组方法: push, pop, shift, unshift 等
data.arr.push(4);

问题

  1. 默认一上来就对 target 进行递归监听
  2. 数组长度改变无效,data.arr.length = 100 无效
  3. 对于不存在的属性不能拦截

Reactive in Vue 3.0

思想

  • Proxy 取代 Object.defineProperty,性能更好
  • 在 get 触发时,才开始递归监听
  • 屏蔽无效 set, 避免重复渲染
  • 使用 WeakMap 保存原生对象,被代理对象,避免重复代理
  • 使用 WeakMap 保存依赖关系,get 时订阅,set 时发布
  • activeEffectStacks 栈顶始终保存 watchingActiveEffect,执行后立即弹出

代码

function isObject(val) {
  return typeof val === 'object' && val !== null;
}

const toProxy = new WeakMap(); // { 原对象: 代理对象 }
const toRaw = new WeakMap(); // { 代理对象: 原对象 }

// 响应式核心
function reactive(target) {
  return createReactive(target);
}

// 响应式工厂函数
function createReactive(target) {
  if (!isObject(target)) {
    return target;
  }

  // (2.1) 若被代理过,返回代理过的结果
  if (toProxy.has(target)) {
    return toProxy.get(target);
  }

  // (2.2) 若是被代理过的对象,则直接返回自己
  if (toRaw.has(target)) {
    return target;
  }

  const baseHandler = {
    get(target, key, reciever) {
      const result = Reflect.get(target, key, reciever);
      console.log('获取', key);
      // (1) 对于嵌套对象,在 get 时,才递归代理监听

      // 订阅: 收集依赖
      track(target, key);

      return isObject(result) ? reactive(result) : result;
    },
    set(target, key, value, reciever) {
      let hadKey = target.hasOwnProperty(key);
      let oldVal = target[key];
      const result = Reflect.set(target, key, value, reciever);
      // (3) 对于数组操作,会进行两次 set,引起视图重复更新
      if (!hadKey) {
        console.log('新增属性', key, value);
         // 发布: 触发响应 更新视图
         trigger(target, key);
      } else if (value !== oldVal) {
        console.log('有效修改属性', key, value);
        // 发布: 触发响应 更新视图
        trigger(target, key);
      }
      return result;
    },
    deleteProperty(target, key, reciever) {
      console.log('删除', key);
      return Reflect.deleteProperty(target, key, reciever);
    }
  };
  let observed = new Proxy(target, baseHandler);

  toProxy.set(target, observed);
  toRaw.set(observed, target);

  return observed;
}

// -----------------------
const activeEffectStacks = []; // effect 栈, 栈顶为 watchingActiveEffect

const targetsMap = new WeakMap();
// 收集依赖
function track(target, key) {
  const effect = activeEffectStacks[activeEffectStacks.length - 1];
  let depsMap = targetsMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetsMap.set(target, depsMap);
  }
  let deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }
  deps.add(effect);
}

// 触发响应
function trigger(target, key) {
  let depsMap = targetsMap.get(target);
  if (depsMap) {
    deps = depsMap.get(key);
    if (deps) {
      deps.forEach(effect => {
        effect();
      });
    }
  }
}

// 响应式副作用
function watchEffect(fn) {
  let effect = createReactiveWatchEffect(fn);
  effect(); // 默认执行一次
}

// 响应式副作用 工厂函数
function createReactiveWatchEffect(fn) {
  function effect() {
    // 1. 将 effect 存入栈中
    // 2. 执行 fn
    return run(effect, fn);
  }

  return effect;
}

function run(effect, fn) {
  try {
    activeEffectStacks.push(effect);
    fn(); // fn 有可能 throw Error
  } finally {
    activeEffectStacks.pop();
  }
}

测试

// 测试用例
const data = {
  count: 100,
  name: {
    a: 'hello'
  },
  arr: [1, 2, 3]
};
const proxy = reactive(data);

// proxy.z;
// proxy.count = 101;
// delete proxy.count;

// (1) 嵌套对象
// proxy.name.a = 'hi';

// (2) WeakMap
//   (2.1) 对于代理过的对象,不重复代理
// const proxy2 = reactive(data);
// console.log(proxy2 === proxy);
//   (2.2) 屏蔽多层代理
// const proxy3 = reactive(proxy);
// console.log(proxy3 === proxy);

// (3) 对于数组操作,会进行两次 set
//   set 数组下标 index
//   set 数组长度 length
// 对于无效操作,避免重复更新视图
// proxy.arr.push(4);

// (4) effect
watchEffect(function() {
  console.log('watchEffect 执行了:', proxy.count);
});
proxy.count = 200;

问题

  • 兼容性不好,如 IE 11, 要 fallback 到 2.0 的响应式