手写vue3-vue2响应式原理 /Proxy和Reflect

245 阅读6分钟

Proxy

Object.defineProperty()

监听对象的操作->执行操作数据 可以用Object.defineProperty()其中的存储属性描述符进行监听

  name: 'xsh',
  age: 18,
};

Object.defineProperty(obj, 'name', {
  get: function () {
    console.log('name被访问了');
  },
  set: function () {
    console.log('name被赋值了');
  },
});

// 监听obj所有key进行监听   
Object.keys(obj).forEach(key => {
  let value = obj[key];
  Object.defineProperty(obj, key, {
    get: function () {
      console.log('name被访问了');
      return value;
    },
    set: function (newValue) {
      console.log('name被赋值了');
      value = newValue;
    },
  });
});

缺点

  1. 设计初衷并不是为了监听对象的所有属性的 而是为了去定义对象中的数据属性描述符的
  2. 如果新增属性 删除属性 他是监听不到的

proxy (类)

如果我们希望监听一个对象的相关操作 我们可以创建一个代理对象(Proxy对象) 之后对该对象的所有操作 都用代理对象来完成 Proxy有13种捕获器

使用Proxy来监听obj对象
如果捕获器不去设置值({}) 修改代理对象也会修改原对象
const objProxy = new Proxy(obj,{
   获取值 
   target:原对象 obj
   key:获取对象的key
   get :function (target,key){
     return target[key]
   }
   设置值 
   set:function(target,key,newValue){
       target[key]=newValue
   }
   监听in的捕获器
   has:function (target,key){
      return key in target
   }
   监听删除捕获器
   deleteProperty(target,key){
      return delect target[key]
   }
   监听获取对象原型
   getPrototypeOf(){}
   监听设置对象原型
   setPrototypeOf(){}
   监听对象能不能扩展
   isExtensible(){}
   preventExtensions(){}
   getOwnPropertyDescriptor(){}
   defineProperty(){}
   监听 Object.getOwnPropertynamesObject.getOwnpropertySymbols()
   onKeys(){}
   用于函数对象
   apply(target,thisArg,argArray){
    return target.apply(thisArg,argArray)
   }
   // 用于new函数对象
   constuct(target,argArray){
    return new target(...argArray)
   }
   
})
objProxy.name = 'abc';
objProxy.age = 12;
console.log('name' in objProxy);
delete objProxy.name;
Object.getPrototypeOf();
Object.setPrototypeOf();
Object.isExtensible();

Reflect(对象) 字面意思是反射 经常和Proxy一起使用

提供了很多操作js对象的方法 类似于js中的Object. 操作对象的方法 意义 1.因为早期ECMA规范中没有考虑到对 对象本身的操作如何设计会更加规范 所以把这些api都放在了Object 但是Object本身就是一个构造函数 这些操作放在它身上有些不合适 另外包括一些类似于 in delect 操作符让js看上去很奇怪

  • Proxy是拦截对象的操作并定义拦截时具体执行什么操作;
  • 而Reflect其实是执行了对象的操作
  • 按我理解,Proxy是改写了对象原有方法;而Reflect像是要抽离原来Object上的方法,它就是是调用Object原有方法的函数,目的是将数据和逻辑代码分离。这样以后可能Obejct就只是数据想要对其操作要用Reflect。

比较Reflect 和 Object developer.mozilla.org/zhCN/docs/W… 他也跟proxy一样有13种捕获器

Reflect;
const objProxy = new Proxy(obj, {
  // 获取值
  // target:原对象 obj
  // key:获取的对象的key
  // receiver 代理对象  会对原对象中的this 从obj指向 到代理对象objProxy
  get: function (target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
  // 设置值   新赋予的值newValue
  set: function (target, key, newValue) {
    // Reflect 他会返回一个boolean 会判断是否设置值成功
    Reflect.set(target, key, newValue, receiver);
  },
  // 监听in捕获器
  has: function (target, key) {
    return key in target;
  },
  // 监听删除捕获器
  deleteProperty(target, key) {
    return delete target[key];
  },
});

响应式(vue3->vue2)

  1. 定义
  2. 响应式函数的封装 reactiveFns 响应式数据有了变化的对应的操作 函数 放在这个数组里面
const reactiveFns=[]
watchFn(fn){}
  1. 封装一个类 主要是用于调用类中方法 来往操作的数组添加函数 和遍历循环这个数组里面的函数
class Depend {
  constructor(){
    this.reactiveFns=[]
  }
  
  addDepend(reactiveFn){
   this.reactiveFns.push(reactiveFn)
  }
  
  notify(){
   this.reactiveFns.forEach(fn=>{
    fn()
   })
  }
}

4.监听对象的变化(使用proxy的set捕捉器)

new proxy(obj,set:depend.notify)

5.依赖收集的数据结构 使用的WeakMap 和Map 使用的WeakMap这样可以把对象的key和依赖函数存储在一起。 使用map的可以具体吧key所对应的依赖函数存储在一起。因为用了WeakMap 所以当不需要的时候 可以直接摧毁

function getDepend(){}

6.正确收集依赖

  • Proxy的get方法中收集对应的函数
  • 创建全局activeReactiveFn变量
  • 在get中找到depend对象,appDepend(全局的activeReactiveFn变量)

7.对Depend进行优化

  • addDepend函数换成depend函数
  • 直接获取到自由变量** 这样可以在监听函数的时候赋值 然后再传给收集依赖的depend类中**
  • 将之前保存的数组编程Set类型 防止传入重复的依赖函数

8.对对象进行响应式操作

  • 封装reactive函数
  • new Proxy() vue3 因为proxy是劫持整个对象 所以当你往对象里面新增key的时候 他也会执行get方法
  • Object.defineProperty() vue2 他是对对象现有的key进行劫持 这就会造成新增key 他没有重写get方法 在vue中使用$set 其实也是在用Object.defineProperty()方法进行原对象的新增key重新执行一边
const info ={
name:'xsh',
age:19
}

1. 把对象在封装的reactive函数里面 在函数里面可以使用Proxy或者Object.defineProperty来进行响应式

const objProxy = reactive(info)
watchFn(() => {
  console.log('当代理对象中的name改变的时候 自动调用/也就是name的依赖');
  console.log(objProxy.name);
});

watchFn(() => {
  console.log('当代理对象中的age改变的时候 自动调用/也就是age的依赖');
  console.log(objProxy.age);
});

将传入的对象进行封装操作 vue3

function reactive(obj){
return new Proxy(obj,{
当获取值的时候 获取到对应的依赖 当你创建了变量 他会先获取值
   get(target,key,receiver){
   获取到该对象所改变的key 所对应的依赖
    const depend=getDepend(target,key)
    depend,depend()
    return Reflect.get(target,key,receiver)
   }
   当改变值的时候触发 触发获取到对应依赖
   set(target,key,newValue,oldValue){
   改变代理对象的值
   Reflect.set(target,key,NewValue,receiver)
   获取到该对象所改变的key所对应的依赖类
    const depend = getDepend(target, key, receiver);
      depend.notify();
   }
})
}

// 对传入对象进行封装操作 Vue2 用的是Object.defineProperty()
function reactive(obj) {
  // {name: "why", age: 18}
  // ES6之前, 使用Object.defineProperty
  Object.keys(obj).forEach(key => {
    let value = obj[key];
    Object.defineProperty(obj, key, {
      get: function () {
        const depend = getDepend(obj, key);
        depend.depend();
        return value;
      },
      set: function (newValue) {
        value = newValue;
        const depend = getDepend(obj, key);
        depend.notify();
      },
    });
  });
  return obj;
}


封装一个depend函数 为了整合对象中的依赖数据格式 主要用的是weakMap和Map 
使用weakMap 他是个弱引用 方便于回收 
const targetWeakMap = new WeakMap()
function getDepend(target,key){
首先获取到传入的对象。获取到它内部key所有的依赖

    let map = targetWeakMap.get(target)

    第一次没有的话 给这个对象创建一个空的依赖进去
    if(!map){
      map= new Map()
      map.set(target,map)
    }

    通过key获取到该key所有的依赖函数
        let depend = map.get(key)
        if(!depend){
            第一次没有的话 给该对象创建一个空的依赖进去

            depend= new Depend()
            map.set(key,depend)

        }

    return depend

}

let activeReactiveFn=null
方便存储收集到的依赖 并且触发 每一个对象都有自己的类
class depend{
 constructor(){
 这块创建依赖数组 用Set不用Arry 因为Set有自动去重 防止加入重复的依赖函数
  this.reactiveFns=new Set()
 }
 
 depend(){
  这块使用一个全局变量 他的用处就是获取每一次的方法依赖
  if(activeReactiveFn){
   this.reactiveFns.add(activeReactiveFn)
  }
 }
 
 循环遍历出发依赖中的函数
 notify(){
  this.reactiveFns.forEach(fn=>{
   fn()
  })
 }
 封装一个响应式函数 这个主要是获取到每一次使用的函数并且出发封装函数中的Set方法
 function watchFn(fn){
  activeReactiveFn=fn
  fn()
  activeReactiveFn=null
 }