vue3响应式原理

2023-10-26 17:27:49 浏览数 (1)

Vue3改用proxy替代object.defineProperty。因为proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法.

响应式原理

在说响应式原理之前,需要理解一些重要的api

proxy

1.什么是proxy

用于修改某些操作的默认行为,在目标对象之前架设一层“拦截”,可以对外界的访问进行过滤和改写。

2.proxy的一些方法

通过构造函数生成实例,var proxy = new Proxy(target,handler),target表示所要拦截的目标对象,handler参数也是一个对象,用于定制拦截行为.

get
代码语言:javascript复制
var obj = {
  a: "123"
}
const handler = {
  // 当使用代理对象访问其属性时会触发get函数,get方法的两个参数分别是目标对象和所要访问的属性。
  get: function(obj, prop) {
    console.log('prop: ', prop);
    return prop in obj ? obj[prop] : 37
  },
}
const p = new Proxy(obj, handler)
p.a //123
p.b //37

如果访问目标对象不存在的属性,会返回37.如果没有这个拦截函数,访问不存在的属性,只会返回undefinedget方法可以继承 这里需要说一下Object.create() 语法:Object.create(proto[,propertiesObject]) 参数:proto,创建对象的原型,表示要继承的参数.propertiesObject(可选):也是一个对象,用于对新创建的对象进行初始化. 看一段代码

代码语言:javascript复制
var obj = {
  a: "123"
}

var myObj = Object.create(obj,{
  c:{value:1},
  d:{
    get:function(){
      console.log("333333")
      return "3333"
    }
  }
})

输出结果obj在myObj的原型上,第一个参数,在返回对象的原型上,而不是直接属性

代码语言:javascript复制
var obj = {
  a: "123"
}
const handler = {

  //继承
  get:function(obj, propertyKey, receiver){
    console.log('obj: ', obj);
    return obj[propertyKey];
  },

}
const p = new Proxy(obj, handler)
// p.b = "456"
// console.log('p: ', p.a);


let extendValue = Object.create(p);
console.log(extendValue.a) //123

使用get拦截,实现数组读取复数的索引

代码语言:javascript复制
var arr = ["a","b","c"]
const handler = {
  get:function(arr,key,value){
    console.log('arr: ', arr);    //["a","b","c"]
    console.log('key: ', key);     //-1
    console.log('value: ', value);    //proxy{0:"a",1:"b",2:"c"}
    let index = Number(key)
    console.log('index: ', index);    //-1
    if(index < 0){
      key = String(arr.length   index);
    }
    return Reflect.get(arr, key, value);
  }
}
const p = new Proxy(arr,handler)
console.log('p: ', p[-1]);    //c

get方法的第三个参数,总是指向原始的读操作所在的那个对象,一般情况下就是Proxy实例。

代码语言:javascript复制
const proxy = new Proxy({}, {
  get: function(target, key, receiver) {
    return receiver;
  }
});
proxy.getReceiver === proxy // true

如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。

代码语言:javascript复制
const target = Object.defineProperties({}, {
  foo: {
    value: 123,
    writable: false,
    configurable: false
  },
});

const handler = {
  get(target, propKey) {
    return 'abc';
  }
};

const proxy = new Proxy(target, handler);

proxy.foo
// TypeError: Invariant check failed
set

set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。

代码语言:javascript复制
var obj = {
  age:100
}
const handler = {
  //添加或者修改属性值的时候执行
  set :function(obj,prop,value){
    if(prop === "age"){
      if(!Number.isInteger(value)){
        console.log("age不是整数")
      }
      if(value > 200){
        console.log("age不大于200")
      }
    }
  }
}
const p = new Proxy(obj,handler)
p.age = "300"
console.log("p",p.age )

set设置了数据验证的方法,

代码语言:javascript复制
var obj = {
  a:1
}
const handler = {
  set:function(obj,prop,value,receiver){
    obj[prop] = receiver;
    return true;
  }
}
const p = new Proxy(obj,handler)
console.log("p" ,p.a === 1)

p并没有a属性.因此会去到p的原型链去找a属性, 注意 set代理返回一个布尔值,严格模式下,set代理如果没有返回true,就会报错。

代码语言:javascript复制
'use strict';
const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
    // 无论有没有下面这一行,都会报错
    return false;
  }
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
// TypeError: 'set' on proxy: trap returned falsish for property 'foo'
apply

apply方法拦截函数的调用、callapply操作。

代码语言:javascript复制
var a = function(){
  return "我是target"
}
const handler = {
  apply:function(){
    return "我是proxy"
  }
}
const p = new Proxy(a,handler)
console.log('p: ', p());

p作为proxy的实例,当它作为函数调用时(p()),就会被apply方法拦截,返回一个字符串。

has

has()方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。 has()方法可以接受两个参数,分别是目标对象、需查询的属性名。

代码语言:javascript复制
var obj ={ _prop: 'foo', prop: 'foo' };
const handler = {
  has:function(target, key) {
    console.log('target: ', target);
    console.log('key: ', key);
    if(key[0] === '_'){
      return false
    }
    return key in target
  }
}
const p = new Proxy(obj,handler)
console.log('p: ', p);
console.log('p: ', '_prop' in  p); //false

has隐藏某些属性,不被in运算符发现.上面代码中,如果原对象的属性名的第一个字符是下划线,proxy.has()就会返回false,从而不会被in运算符发现。has()拦截对for...in循环不生效。

construct()

拦截new命令 接受三个参数,target:目标对象,args:构造函数的参数数组, newTarget:创造实例对象时,new命令作用的构造函数

代码语言:javascript复制
const handler = {
  construct:function(obj,args){
    console.log('obj: ', obj);
    console.log('args: ', args);
    return {args:args[0]}

  }
}
const p = new Proxy(function(){},handler)
console.log('p: ', p);
console.log(new p(1))

construct()方法返回的必须是一个对象,否则会报错。 另外,由于construct()拦截的是构造函数,所以它的目标对象必须是函数,否则就会报错。 construct()方法中的this指向的是handler,而不是实例对象。

deleteProperty

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

代码语言:javascript复制
var obj = {
  _prop:1
}
console.log(obj[0])
const handler = {
  deleteProperty:function(obj,key){
    invariant(key, 'delete');
    delete target[key];
    return true
  }
}
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}
const p = new Proxy(obj,handler)
console.log('p: ', p);
delete p._prop //```
// Error: Invalid attempt to delete private "_prop" property
defineProperty

拦截了Object.defineProperty()操作。

代码语言:javascript复制
var handler = {
  defineProperty (target, key, descriptor) {
    return false;
  }
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不会生效

上面代码中,defineProperty()方法内部没有任何操作,只返回false,导致添加新属性总是无效。注意,这里的false只是用来提示操作失败,本身并不能阻止添加新属性。

getOwnPropertyDescriptor

拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined

代码语言:javascript复制
var handler = {
  getOwnPropertyDescriptor (target, key) {
    if (key[0] === '_') {
      return;
    }
    return Object.getOwnPropertyDescriptor(target, key);
  }
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }
getPrototypeOf

用来拦截获取对象的原型 主要用来拦截以下操作

代码语言:javascript复制
-   `Object.prototype.__proto__`
-   `Object.prototype.isPrototypeOf()`
-   `Object.getPrototypeOf()`
-   `Reflect.getPrototypeOf()`
-   `instanceof`

getPrototypeOf()方法拦截Object.getPrototypeOf(),返回proto对象。getPrototypeOf()方法的返回值必须是对象或者null,否则报错

isExtensible

拦截Object.isExtensible()(判断一个对象是否可扩展)

代码语言:javascript复制
var p = new Proxy({}, {
  isExtensible: function(target) {
    console.log("called");
    return true;
  }
});

Object.isExtensible(p)
// "called"
// true

调用Object.isExtensible()输出called 只能返回布尔值 该方法有个强限制,返回值必须与目标对象的.isExtensible属性保持一致

ownKeys

ownKeys()方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • for...in循环
代码语言:javascript复制
let target = {
  a: 1,
  b: 2,
  c: 3
};

let handler = {
  ownKeys(target) {
    return ['a'];
  }
};

let proxy = new Proxy(target, handler);

Object.keys(proxy)
// [ 'a' ]

拦截了对于target对象的Object.keys()操作,只返回abc三个属性之中的a属性。 使用Object.keys()方法时,有三类属性会被ownKeys()方法自动过滤,不会返回。

  • 目标对象上不存在的属性
  • 属性名为 Symbol 值
  • 不可遍历(enumerable)的属性
代码语言:javascript复制
let target = {
  a: 1,
  b: 2,
  c: 3,
  [Symbol.for('secret')]: '4',
};

Object.defineProperty(target, 'key', {
  enumerable: false,
  configurable: true,
  writable: true,
  value: 'static'
});

let handler = {
  ownKeys(target) {
    return ['a', 'd', Symbol.for('secret'), 'key'];
  }
};

let proxy = new Proxy(target, handler);

Object.keys(proxy)
console.log('Object.keys(proxy): ', Object.keys(proxy));
// ['a']
preventExtensions

preventExtensions()方法拦截Object.preventExtensions()(让一个对象变的不可扩展,也就是永远不能再添加新的属性。)。该方法必须返回一个布尔值,否则会被自动转为布尔值。

setPrototypeOf

用来拦截Object.setPrototypeOf()(为现有对象设置原型,返回一个新对象)方法。

代码语言:javascript复制
var handler = {
  setPrototypeOf (target, proto) {
    throw new Error('Changing the prototype is forbidden');
  }
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden

只要修改target的原型对象,就会报错。

set和map数据结构

set

基础概念

类似数组,没有重复的值,成员的值是唯一的.

代码语言:javascript复制
const s = new Set()
console.log('s: ', s);
[1,1,3,4,4,4,4,46].forEach(x => s.add(x));
console.log('s: ', s);
for(let i of s){
  console.log('i: ', i); //1,3,4,46
}
代码语言:javascript复制
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

set可用于数组去重和字符串里面的字符去重

set实例的属性和方法

Set 结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。 Array.from方法可以将 Set 结构转为数组。
代码语言:javascript复制
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);
遍历操作

Set 结构的实例有四个遍历方法,可以用于遍历成员。

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员

WeakSet

不重复值的集合,与set区别, 成员只能是对象; 对象都是弱引用.如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存, weakset接收的参数只能是对象,注意:[[1, 2], [3, 4]]是对象类型,如果不是对象会报错 WeakSet 结构有以下三个方法。

  • WeakSet.prototype.add(value) :向 WeakSet 实例添加一个新成员。
  • WeakSet.prototype.delete(value) :清除 WeakSet 实例的指定成员。
  • WeakSet.prototype.has(value) :返回一个布尔值,表示某个值是否在 WeakSet 实例之中。 WeakSet没有size属性,且不可遍历

Map

类似对象,是键值对集合,但各种类型的值都可以当做键。只有对同一个对象的引用,map结构才将其视为同一个键。简单类型的话如果严格相等则被视为同一个键.

map实例的属性和方法

**size 属性:**返回 Map 结构的成员总数。 Map.prototype.set(key, value):set方法设置键名key对应的键值为value Map.prototype.get(key):get方法读取key对应的键值,如果找不到key,返回undefinedMap.prototype.has(key):has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。 Map.prototype.delete(key):delete方法删除某个键,返回true。如果删除失败,返回falseMap.prototype.clear():clear方法清除所有成员,没有返回值。

遍历方法

Map 结构原生提供三个遍历器生成函数和一个遍历方法。

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。

WeakMap

键值对的集合,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。不计入垃圾回收机制 不计入垃圾回收机制,没有遍历操作,也没有size属性。没有Get() set() has() delete()

Reflect

 从reflect对象上可以拿到语言内部的方法 方法和proxy一一对应。

vue3响应式原理的实现

通过proxy代理我们所需要的对象,reactive()创建一个响应式对象或数组,查看源码关于reactive()方法

代码语言:javascript复制
//判断是否为object
function isObject(val) {
  return val !== null && typeof val === 'object'
}

function reactive(target) {
  console.log('target: ', target);
  return createReactiveObject(target)
}

创建响应式对象,此方法是响应式的核心方法,该方法判断是否可读,后返回createReactiveObject()方法

为什么要用Reflect

在proxy代理情况下,如果目标对象下的属性有函数类型且内部使用this,在使用代理对象访问属性(函数类型)的时候,此时this指向proxy代理对象。 解决办法 通过 Reflect.get获取值后,判断值的类型,如果是函数,通过bind改变函数this指向,否则直接返回。

代码语言:javascript复制
user = new Proxy(user, { get(target, prop, receiver) { 
    let value = Reflect.get(...arguments);
    return typeof value == 'function' ? value.bind(target) : value; } 
});
代码语言:javascript复制
function createReactiveObject(target) {
  if(!isObject(target)){
    return target
  }
  const handler = {
    get:function(target,key,receiver){
      //proxy   reflect 反射 此种写法等同于return target[key]
      console.log("获取")
      let result = Reflect.get(target,key,receiver)
      return result
    },
    set:function(target,key,value,receiver){
      // target[key] = value //如果设置没成功,可能报错,所有采用和此种写法等价的方法
      let res = Reflect.set(target,key,value,receiver)
      console.log("修改")
      return res
    },
    deleteProperty:function(target,key){
      console.log("删除")
      let res =  Reflect.deleteProperty(target,key)
      return res
    }
  }
  const p = new Proxy(target,handler)
  console.log('p: ', p);
  return p
}
const proxy = reactive({
  a:1
})
proxy.a
proxy.a = 123
console.log('proxy.a: ', proxy.a);
// delete proxy.a

此方法创建响应式对象,返回proxy代理.实现对象的读取和修改(Reflect优点:不会报错,会有返回值,会替代object上的方法)

深层代理

以上的方法可实现第一层代理,但是对象难免是多层,这时我们需要修改get方法,在获取代理时利用递归修改返回值。这样的好处是只有访问内部属性才会进行深层响应式,减少性能消耗。

代码语言:javascript复制
const handler = {
    get:function(target,key,receiver){
      //proxy   reflect 反射 此种写法等同于return target[key]
      console.log("获取")
      let result = Reflect.get(target,key,receiver)
      return isObject(result) ? reactive(result) : result //是个递归
    },
  }
  
proxy.a.b = 456
console.log("proxy",proxy.a.b) // 456

此方法在get获取目标对象时,对目标对象进行判断,如果目标对象是对象,则通过proxy对其进行再一次代理.

解决重复代理的问题

在我们进行代理时,如果这个对象代理过了,就不要再new了,通过WeakMap(弱引用对象,一旦弱引用对象未被使用,会被垃圾回收机制回收)来解决,toRaw方法返回原对象

代码语言:javascript复制
let toProxy = new WeakMap() //弱引用映射表,放置的是原对象和代理过的对象
let toRaw = new WeakMap() //被代理过的对象,原对象

const p = new Proxy(target,handler)
toProxy.set(target,p)


let proxy = toProxy.get(target)
console.log('proxy: ', proxy);

if(proxy){
    return proxy //如果取到值,说明被代理过了
}
if(toRaw.has(target)){ //防止一个对象被多次代理
    console.log('toRaw.has(target): ', toRaw.has(target));
    return target
}
  
  
const p = new Proxy(target,handler)
toProxy.set(target,p)
toRaw.set(p,target)
return p

let obj = {
  a:{b:1}
}
const proxy = reactive(obj)
reactive(obj)
reactive(obj)
reactive(obj)

关于数组的问题

代码语言:javascript复制
let arr = [1, 2, 3]
const proxy = reactive(arr)
proxy.push(4)

此时会修改数组的值和长度

此时我们需要判断代理对象是修改属性还是添加长度

代码语言:javascript复制
//判断当前对象是否有此属性
function hasOwn(target, key) {
  return target.hasOwnProperty(key);
}


set: function (target, key, value, receiver) {
  // target[key] = value //如果设置没成功,可能报错,所有采用和此种写法等价的方法
  let hadKey = hasOwn(target,key)
  let oldValue = target[key]
  let res = Reflect.set(target, key, value, receiver)
  if (!hadKey) {
    console.log("新增属性") //需要数组的长度
  } else if (oldValue !== value) { //表示属性更改过了
    console.log("修改属性") //为了屏蔽无意义的修改
  }
  console.log("修改")
  return res
},

解构丢失响应式

将响应式对象属性赋值或解构(注意:解构出来是基本数据类型时,失去响应式,解构出来是引用数据类型时,不会失去响应式)到本地,或将该属性传入一个函数时,会失去响应式。

完整代码

代码语言:javascript复制
let toProxy = new WeakMap() //弱引用映射表,放置的是原对象和代理过的对象
let toRaw = new WeakMap() //被代理过的对象,原对象

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

//判断当前对象是否由此属性
function hasOwn(target, key) {
  return target.hasOwnProperty(key);
}

function reactive(target) {
  return createReactiveObject(target)
}
function createReactiveObject(target) {
  if (!isObject(target)) {
    return target
  }
  let proxy = toProxy.get(target)
  if (proxy) {
    return proxy //如果取到值,说明被代理过了
  }
  if (toRaw.has(target)) { //防止一个对象被多次代理
    console.log('toRaw.has(target): ', toRaw.has(target));
    return target
  }
  const handler = {
    get: function (target, key, receiver) {
      //proxy   reflect 反射 此种写法等同于return target[key]
      console.log("获取")
      let result = Reflect.get(target, key, receiver)
      return isObject(result) ? reactive(result) : result
    },
    set: function (target, key, value, receiver) {
      // target[key] = value //如果设置没成功,可能报错,所有采用和此种写法等价的方法
      let hadKey = hasOwn(target,key)
      let oldValue = target[key]
      console.log('oldValue: ', oldValue);
      let res = Reflect.set(target, key, value, receiver)
      if (!hadKey) {
        console.log("新增属性")
      } else if (oldValue !== value) { //表示属性更改过了
        console.log("修改属性") //为了屏蔽无意义的修改
      }
      console.log("修改")
      return res
    },
    deleteProperty: function (target, key) {
      console.log("删除")
      let res = Reflect.deleteProperty(target, key)
      return res
    }
  }
  const p = new Proxy(target, handler)
  toProxy.set(target, p)
  toRaw.set(p, target)
  return p
}
// let obj = {
//   a:{b:1}
// }
// const proxy = reactive(obj)
// reactive(obj)
// reactive(obj)
// reactive(obj)

// proxy.a
// proxy.a = 123
// proxy.a.b = 456
// console.log("proxy",proxy.a.b)
// delete proxy.a

let arr = [1, 2, 3]
const proxy = reactive(arr)
proxy.push(4)
// proxy.length = 100

但不得不提,reactive具有局限性,仅对对象类型有用(对象,数组,set,map这样的集合类型),而对string,number,boolean这样的原始类型无效.

用ref定义响应式变量

reactive的响应式不能作用于所有值类型,因此,vue提供了ref来允许定义所有值类型的响应式.

0 人点赞