Vue3.0 响应式初探

277 阅读6分钟

引言

vue3.0 base版终于发布了,最近了解下相关功能的介绍以及API文档,其中响应式从原来的Object.defineProperty()改为Proxy代理。

什么是Proxy

什么是Proxy代理,简而言之就是在原来的对象上加上一层拦截,当外界对对象进行访问是,必须经过这个层对象。因此提供了一层机制,可以对外界对象的访问进行过滤和修改。

  • get(target, propKey, receiver): 拦截对象的读取。
  • set(target, propKey, receiver): 拦截对象的设置。
  • has(target, propKey): 拦截propKey in proxy, 返回一个布尔值。
  • deleteProperty(target, propKey): 拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target): 拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

用法

var person = {
  name: "张三"
};

var proxy = new Proxy(person, {
  get: function(target, propKey) {
    if (propKey in target) {
      return target[propKey];
    } else {
      throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
    }
  }
});

proxy.name // "张三"
proxy.age // 抛出一个错误

以上引用于阮一峰ECMAScript 6 入门

Proxy和Object.defineProperty对比

Proxy优点

  • Proxy 可以原生支持数组,而Object.defineProperty不支持数组操作
  • Proxy 可以监控到删除新增等属性, 而Object.defineProperty只能监控到get,set
  • Proxy有多大13中拦截方法,而Object.defineProperty不具备

Proxy缺点

  • Proxy有兼容问题,对于IE11不支持,Object.defineProperty没有兼容问题

Vue3.0 响应式实现

  • reactive 主要用于创建proxy代理
  • effect 生成响应式

1. 通过reactive创建代理对象

reactive函数

  • 参数 target 要代理的对象
  • 返回值:被代理的对象
function reactive(targe) {
  // 如果不为对象,则不需要代理
  if(!isObject(target)) {
    return;
  }
  return createReactiveObject(target);
}

// 真正实现代理的函数
function createReactiveObject(target) {
  const baseHandle = {
    get(target, key, receive) {
      console.log('get', key)
      const res = Reflect.get(target, key, receive);
      return isObject(res) ? reactive(res) : res;
    },
    set(target, key, value, receive){
      const hasKey = hasOwn(target, key); // 判断属性是否有
      const oldValue = target[key];
      const res = Reflect.set(target, key, value, receive);
      return res; // 必须有返回值,如果没有返回值,当代理数组的时候会报错
    },
    deleteProperty(target, key){
      const res = Reflect.deleteProperty(target, key);
      return res;
    } 
  }

  const observer = new Proxy(target, baseHandle);
  toProxy.set(target, observer);
  toRaw.set(observer, target);
  return observer;
}

// demo
const person = {name: 'dragon', age: 20};
const proxy = reactive(person);
proxy.age = 21;
console.log(proxy.age) //21

此时对象已被代理,但是还有一些问题,当一个对象多次代理时, 会产生多个对象:

const person = {name: 'dragon', age: 20};
const proxy = reactive(person);
const proxy1 = reactive(person);
const proxy2 = reactive(proxy1);
// 对象被多次代理,以及可以对代理对象进行二次代理

修改方案,可以做一个缓存,保存下已被代理的对象和代理的对象的对应关系:

+ const toProxy = new WeakMap(); // 放的是愿对象(key) 和 代理对象(proxy)
+ const toRaw = new WeakMap(); // 放的是代理对象(proxy) 和  愿对象(key)

// 修改reactive函数
function reactive(target) {
  if(!isObject(target)) {
    return;
  }

  // 防止被多次代理
 + const observer = toProxy.get(target);
 + if(observer) {
 +  return observer;
 + }

  // 防止多层代理
  + if(toRaw.has(target)) {
  +  return target;
  + }

  return createReactiveObject(target);
}

2. 通过effect函数实现收集

// 当proxy.name 修改后触发回掉,且第一调用时会首先调用一次回调
effect(() => {console.log(proxy.name)})

effect实现

activeEffectStacks = []; // 回调函数堆栈

// 响应式。副作用
function effect(fn) {
  const effect = createReactiveEffect(fn);
  effect();
}

// 创建响应式的effect
function createReactiveEffect(fn) {
  const effect = function() {
    return run(effect, fn); // 让fn执行,并让effect 存入栈中
  }
  return effect;
}


function run(effect, fn) { //运行fn, 并让effect存入栈中
  try{
    activeEffectStacks.push(effect);
    fn(); // fun 执行的时候会调用取值
  }finally {
    activeEffectStacks.pop();
  }
}

修改baseHandle中的get和set方法,进行收集

// 修改createReactiveObject
function createReactiveObject(target) {
  const baseHandle = {
    get(target, key, receive) {
      console.log('get', key)
      const res = Reflect.get(target, key, receive);
      // 判断当前返回值是否为对象,如果是对象,则重新代理
      + track(target, key); // 收集依赖
      return isObject(res) ? reactive(res) : res;
    },
    set(target, key, value, receive){
      const hasKey = hasOwn(target, key); // 判断属性是否有
      const oldValue = target[key];
      const res = Reflect.set(target, key, value, receive);
     	+ if(!hasKey) { // 新增属性
      +  trigger(target, 'add', key);
      + } else if(oldValue !== value) { // 更新
      +  trigger(target, 'set', key);
      + }
      
      return res; // 必须有返回值,如果没有返回值,当代理数组的时候会报错
    },
    deleteProperty(target, key){
      const res = Reflect.deleteProperty(target, key);
      return res;
    } 
  }

  const observer = new Proxy(target, baseHandle);
  toProxy.set(target, observer);
  toRaw.set(observer, target);
  return observer;
}

此过程中添加了track和trigger方法,其中track是进行收集的,而trigger方法是进行调用的。

let targetsMap = new WeakMap();
function track(target, key) { // 如果这个target中的key变化了我就执行方法
  const effect = activeEffectStacks[activeEffectStacks.length - 1];
  if(effect) {
    let depsMap = targetsMap.get(target);
    if(!depsMap) {
      targetsMap.set(target, (depsMap = new Map()));
    }
    let deps = depsMap.get(key);
    if(!deps) {
      depsMap.set(key, (deps = new Set()));
    }
    if(!deps.has(effect)) {
      console.log(effect);
      deps.add(effect);
    }
  }
}

function trigger(target, type, key) {
  let depsMap = targetsMap.get(target);
  if(depsMap) {
    const deps = depsMap.get(key);
    if(deps) {
      deps.forEach(effect => {
        effect();
      });
    }
  }
}

收集的整体过程

  1. 调用effect方法,在effect方法中调用createReactiveEffect()生成用于真正进行回调函数effect,同时调用effect;
  2. 调用effect时,会触发run方法,此时,把回调函数effect加入到栈中,同时在run中,调用fn
  3. 调用fn时会触发,get方法,在get中通个track进行收集,把结果缓存到targetsMap
  4. 调用set时,在trigger触发响应

完整代码

const toProxy = new WeakMap(); // 放的是愿对象(key) 和 代理对象(proxy)
const toRaw = new WeakMap(); // 放的是代理对象(proxy) 和  愿对象(key)

// 判断是否为对象
function isObject(target) {
  return typeof target === 'object' && target !== null;
}

// 判断是否包含key
function hasOwn(target, key) {
  return target.hasOwnProperty(key);
}

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

  // 防止被多次代理
  const observer = toProxy.get(target);
  if(observer) {
    return observer;
  }

  // 防止多层代理
  if(toRaw.has(target)) {
    return target;
  }

  return createReactiveObject(target);
}

// 创建响应式对象
/**
 * Refelct 会有返回值,不会报错,可以遍历 symbol 
 */
function createReactiveObject(target) {
  const baseHandle = {
    get(target, key, receive) {
      console.log('get', key)
      const res = Reflect.get(target, key, receive);
      // 判断当前返回值是否为对象,如果是对象,则重新代理
      track(target, key); // 收集依赖
      return isObject(res) ? reactive(res) : res;
    },
    set(target, key, value, receive){
      const hasKey = hasOwn(target, key); // 判断属性是否有
      const oldValue = target[key];
      const res = Reflect.set(target, key, value, receive);
      if(!hasKey) { // 新增属性
        trigger(target, 'add', key);
      } else if(oldValue !== value) { // 更新
        trigger(target, 'set', key);
      }
      
      return res; // 必须有返回值,如果没有返回值,当代理数组的时候会报错
    },
    deleteProperty(target, key){
      const res = Reflect.deleteProperty(target, key);
      return res;
    } 
  }

  const observer = new Proxy(target, baseHandle);
  toProxy.set(target, observer);
  toRaw.set(observer, target);
  return observer;
}

let activeEffectStacks = [];
let targetsMap = new WeakMap();
function track(target, key) { // 如果这个target中的key变化了我就执行方法
  const effect = activeEffectStacks[activeEffectStacks.length - 1];
  if(effect) {
    let depsMap = targetsMap.get(target);
    if(!depsMap) {
      targetsMap.set(target, (depsMap = new Map()));
    }
    let deps = depsMap.get(key);
    if(!deps) {
      depsMap.set(key, (deps = new Set()));
    }
    if(!deps.has(effect)) {
      console.log(effect);
      deps.add(effect);
    }
  }
}

function trigger(target, type, key) {
  let depsMap = targetsMap.get(target);
  if(depsMap) {
    const deps = depsMap.get(key);
    if(deps) {
      deps.forEach(effect => {
        effect();
      });
    }
  }
}

// 响应式。副作用
function effect(fn) {
  const effect = createReactiveEffect(fn);
  effect();
}
// 创建响应式的effect
function createReactiveEffect(fn) {
  const effect = function() {
    return run(effect, fn); // 让fn执行,并让effect 存入栈中
  }
  return effect;
}

function run(effect, fn) { //运行fn, 并让effect存入栈中
  try{
    activeEffectStacks.push(effect);
    fn(); // fun 执行的时候会调用取值
  }finally {
    activeEffectStacks.pop();
  }
}

// 依赖收集,发布订阅

const proxy = reactive({name: {n: 1}});
effect(() => {
  console.log(proxy.name.n)
})
proxy.name.n = 3;