likes
comments
collection
share

关于vue3的Proxy

作者站长头像
站长
· 阅读数 31

关于vue3的Proxy

引入

数据响应式:从一开始使用 Vue 时,对于之前的 jq 开发而言,一个很大的区别就是基本不用手动操作 dom,data 中声明的数据状态改变后会自动重新渲染相关的 dom。因此实现数据响应式有两个重点问题:

  1. 如何知道数据发生了变化?
  2. 如何知道数据变化后哪里需要修改?

对于第一个问题,Vue3 之前使用了 ES5 的一个 APIObject.defineProperty, Vue3 中使用了 ES6 的 Proxy都是对需要侦测的数据进行 变化侦测,添加 getter 和 setter ,这样就可以知道数据何时被读取和修改。

第二个问题,Vue 对于每个数据都收集了与之相关的依赖,这里的依赖其实就是一个对象,保存有该数据的旧值及数据变化后需要执行的函数。每个响应式的数据变化时会遍历通知其对应的每个依赖,依赖收到通知后会判断一下新旧值有没有发生变化,如果变化则执行回调函数响应数据变化(比如修改 dom)。依赖收集与 Vue3与Vue2 类似,在 getter 中收集依赖,setter 中触发依赖

什么是Proxy对象

在MDN上对于Proxy的解释是:

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

首先Proxy的语法是:let p = new Proxy(target, handler);:

  • target是你要代理的对象.它可以是JavaScript中的任何合法对象.如: (数组, 对象, 函数等等);
  • handler是你要自定义操作方法的一个集合;
  • p是一个被代理后的新对象,它拥有target的一切属性和方法.只不过其行为和结果是在handler中自定义的。
let obj = {
  a: 1,
  b: 2,
}

const p = new Proxy(obj, {
  get(target, key, value) {
    if (key === 'c') {
      return '我是自定义的一个结果';
    } else {
      return target[key];
    }
  },

  set(target, key, value) {
    if (value === 4) {
      target[key] = '我是自定义的一个结果';
    } else {
      target[key] = value;
    }
  }
})

console.log(obj.a) // 1
console.log(obj.c) // undefined
console.log(p.a) // 1
console.log(p.c) // 我是自定义的一个结果

obj.name = '李白';
console.log(obj.name); // 李白
obj.age = 4;
console.log(obj.age); // 4

p.name = '李白';
console.log(p.name); // 李白
p.age = 4;
console.log(p.age); // 我是自定义的一个结果
  • 总结:Proxy对象就是可以让你去对JavaScript中的一切合法对象的基本操作进行自定义.然后用你自定义的操作去覆盖其对象的基本操作.也就是当一个对象去执行一个基本操作时,其执行的过程和结果是你自定义的,而不是对象的. * 类似浅拷贝

Proxy的作用:

  • 拦截和监视外部对对象的访问;
  • 降低函数或类的复杂度
  • 复杂操作前对操作进行校验或对所需资源进行管理

Reflect

在MDN上对于Reflect的解释是:

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。(类似Math)

Reflect所定义的静态方法包含了proxy handlers所处理的代理操作,举例:Reflect.get(target, name);效果等同于target[name];

为什么使用Reflect与Proxy搭配?

  • 用函数处理相关操作,例如删除属性:比起delete target[name]Reflect.deleteProperty(target, name)更能保持一致性;
const loggedObj = new Proxy(obj, {
  deleteProperty: function(target, name) {
    // instead of `delete target[name]...
    return Reflect.deleteProperty(target, name);
  }
});
  • 解决this指向:在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。
const target = {
    get foo() {
        return this.bar;
    },
    bar: 3
};
const handler = {
    get(target, propertyKey, receiver) {
        if (propertyKey === 'bar') return 2;
        console.log('Reflect.get ', Reflect.get(target, propertyKey, receiver)); 
        console.log('target[propertyKey] ', target[propertyKey]);
    }
};
const obj = new Proxy(target, handler);
console.log(obj.bar);
// 2
console.log(obj.foo);
// Reflect.get  2
// target[propertyKey]  3

兼容性: 关于vue3的Proxy 关于vue3的Proxy

Vue2和Vue3数据响应式对比

温习:Vue2.x数据响应式原理:定义依赖收集对象dep,在get部分调用dep.depend收集依赖,在set部分调用dep.notify更新依赖。virtualDom在vue中表现为抽象语法树ast,包含子节点,本质就是一个json对象,外面包裹了一层观察者模式,dep.notify只需要触发observe,由observe触发ast进行更新。

关于vue3的Proxy

VUE3.0 的响应式原理

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <script src="./vue.global.js"></script>
</head>
<body>
  <div id="app"></div>

  <script>
    const {createApp, reactive} = Vue
    const App = {
      template: `<h1 @click="updateTitle">{{title.name}}</h1>`,
      setup() {
        const title = reactive({name: 'vue2'});
        const updateTitle = () => {
          title.name = 'vue3';
        }
        return {title, updateTitle};
      }
    }
    createApp(App).mount('#app')
  </script>
</body>
</html>

从上面的代码可以看出,当reactive({name: 'vue2'});是应该触发一次更新视图的,而当变更值proxy.name = 'vue3';的时候需要再触发一次更新视图。vue3.0这里的主要逻辑是通过Vue.effect()这个方法去实现的,effect这个方法会先执行一次,当数据变化的时候会再执行。通过示例可以看到实现 Vue3 这个数据响应式需要有 reactive、effect 这两个函数,下面通过从变化侦测及依赖收集两个方面介绍,简单实现这几个函数。

变化侦测

let sourceProxy = new WeakMap(); // 存放原对象和代理过的对象
let toRaw = new WeakMap(); // 存放被代理过的对象和原对象
function reactive(target) {
  return createReactive(target);
}
function createReactive(target) {
  // 创建响应式对象
  if(!(typeof target === 'object' && target !== null)) {
    return target;
  }
  // 判断target是否被代理过
  if(sourceProxy.get(target)) {
    return sourceProxy.get(target);
  }
  // 判断代理过的里面是否有target, 防止一个对象被多次代理
  if (toRaw.has(target)) {
    return target;
  }
  // 创建观察者
  let baseHandle = {
    get(target, key, receiver) {
      // 获取
      let datas = Reflect.get(target, key, receiver);
      // 订阅
      subscribeTo(target, key); // 当key变化的时候重新执行effect
      //  结果代理(递归)
      return typeof target === 'object' && target !== null ? reactive(datas) : datas;
    },
    set(target, key, value, receiver) {
      // 设置
      let oldValue = target[key];
      let res = Reflect.set(target, key, value, receiver);
      // 此处的判断是屏蔽无意义的修改
      if (!target.hasOwnProperty(key)) {
        // 新增属性
        console.log('新增');
        trigger(target, 'add', key);
      } else if (oldValue !== value) {
        // 修改属性
        console.log('修改');
        trigger(target, 'set', key);
      }
      console.log(res);
      return res;
    }
  }
  let observed = new Proxy(target, baseHandle);
  // 设置weakmap
  sourceProxy.set(target, observed);
  toRaw.set(observed, target);
  return observed;
}

reactive 中使用 Proxy 对目标进行代理,代理的行为是 baseHander ,然后对目标对象及代理后的对象进行缓存,防止多次代理。track 函数收集依赖,trigger 函数触发依赖更新。

依赖收集(发布订阅)

let stacks = [];  // 保存当前待收集的依赖对象
let targetMap = new WeakMap();  // 记录所有数据及其对应依赖的表
// 响应式
function effect(fn) {
  let effect = createReactiveFn(fn); // 创建响应式的函数
  effect(); // 默认执行一次
}
function createReactiveFn(fn) {
  let eFn = function() {
    // 执行fn并且将fn存入栈中
    return carryOut(eFn, fn)
  }
  return eFn;
}
function carryOut(effect, fn) {
  // 执行fn并且将fn存入栈中
  try {
    stacks.push(effect);
    fn();
  } finally {
    stacks.pop(effect);
  }
}
function subscribeTo(target, key) { // 收集依赖
  let effect = stacks[stacks.length - 1];
  if (effect) { //如果有,则创建关联
    let maps = targetMap.get(target);
    if (!maps) {
      targetMap.set(target, maps = new Map);
    }
    let deps = maps.get(key);
    if (!deps) {
      maps.set(key, deps = new Set());
    }
    if(!deps.has(effect)) {
      deps.add(effect);
    }
  }
}
function trigger(target, type, key) { // 触发依赖更新
  let tMap = targetMap.get(target);
  if (tMap) {
    let keyMaps = tMap.get(key);
    // 将key对应的effect执行
    if (keyMaps) {
      keyMaps.forEach((eff) => {
        eff();
      })
    }
  }
}

关于vue3的Proxy

作者:Luin

[译] 尤雨溪:Vue 3.0 计划

ES6中的代理模式-----Proxy

一起來了解 Javascript 中的 Proxy 與 Reflect

vue2.0响应式到vue3.0响应式原理

了解Vue3源码这一篇就够了