Proxy

监听对象的操作

如果我们希望监听一个对象中的属性被设置或获取的过程

在之前,我们可以通过 Object.defineProperty 的存储属性描述符(get set)来监听到

const obj = {
name: 'xxx',
age: 18
}

Object.keys(obj).forEach(key => {
let value = obj[key]

Object.defineProperty(obj, key, {
get: function() {
console.log(`监听到obj对象的${key}属性被访问了`);
return value
},
set: function(newValue) {
console.log(`监听到obj对象的${key}属性被设置值`);
value = newValue
}
})
})

obj.name = 'asd'
console.log(obj.name);

但是如果我们想要监听更加丰富的操作,比如说新增属性、删除属性,那么这个方法是无能为力的

而且我们要知道,这个方法的存储描述符设计的初衷也不是为了监听一个完整的对象的

Proxy的基本使用

这也是我们监听对象操作的第二种方式

在 ES6 中,新增了Proxy,用于帮助我们创建一个代理

如果我们希望监听一个对象的相关操作,我们可以先根据这个对象创建一个代理对象(Proxy对象)。

然后对该对象的所有操作,我们都通过代理对象完成,因为这个代理对象可以帮我们监听我们对原对象进行的操作

怎么来使用这个Proxy代理呢?

const obj = {
name: 'xxx',
age: 18
}

const objProxy = new Proxy(obj, {
// 获取值时的捕获器
// target:原对象, key:属性
get: function(target, key) {
console.log(`监听到对象的${key}属性被访问了`, target);
return target[key]
},
// 设置值的捕获器
set: function(target, key, newValue) {
console.log(`监听到对象的${key}属性被设置值`, target);
target[key] = newValue
},
// 监听in的捕获器
has: function(target, key) {
console.log(`监听到对象的${key}属性in操作`, target);
return key in target
},
// 监听delete的捕获器
deleteProperty: function(target, key) {
console.log(`监听到对象的${key}属性delete操作`, target);
delete target[key]
}
})

console.log(objProxy.name);
// 监听到对象的name属性被访问了 {name: 'xxx', age: 18}
// xxx

objProxy.name = 'kkk'
// 监听到对象的name属性被设置值 {name: 'xxx', age: 18}

console.log(obj.name); // kkk

console.log("name" in objProxy); // true
delete objProxy.name

为什么我们对代理对象做出的改变,原对象也是会变的?

因为我们设置值的时候,就是直接对原对象设置的,target[key] = newValue

Proxy所有捕获器

有13个捕获器

image.png image.png

Proxy 对函数的监听:

function foo() {}

const fooProxy = new Proxy(foo, {
apply: function(target, thisArg, arrArray) {
console.log("对foo函数进行了apply调用");
return target.apply(thisArg, arrArray)
},

construct: function(target, arrArray, newTarget) {
console.log("对foo函数进行了new调用");
return new target(...arrArray)
}
})

fooProxy.apply({}, ['aaa','bbb'])
new fooProxy("ddd", "fff")

Reflect

简单介绍

  • Reflect是什么?

    ES6 新增的一个API,它是一个对象,字面意思是反射

  • 有什么用呢?

    主要是提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法

    比如:Reflect.getPrototype(target) 类似于 Object.getPrototype(target)

  • 那有Object可以做这些操作,为什么还要新增 Reflect 对象呢?

    因为在早期的 ECMA 规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放在 Object 上

    但是Object作为一个构造函数,这些操作放到它身上并不合适

    另外还包含一些类似于 in、delete操作符,Reflect让原本的命令式变成函数式 .has ()等

    所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上

  • Object和Reflect对象之间的API关系

    MDN https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect/Comparing_Reflect_and_Object_methods

Reflect常见方法

跟Proxy一一对应的,也是13个

image.png

Reflect 和 Proxy 一起使用

我们可以对前面Proxy案例中,对原对象的操作都修改为Reflect来操作

const obj = {
name: 'xxx',
age: 18
}

const objProxy = new Proxy(obj, {
get(target, key, receiver) {
console.log("get---------")
// return target[key]
// 不直接修改原对象了
return Reflect.get(target, key)
},
set(target, key, newValue, receiver) {
console.log("set---------")
// target[key] = newValue
// 可以拿到设置成功与否的结果,一个布尔值
const result = Reflect.set(target, key, newValue)

// 然后就可以根据结果做一些操作
if (result) {
} else {}
}
})

objProxy.name = 'kkk'
console.log(objProxy.name); // kkk

Receiver参数的作用

访问器还有一个参数 receiver

作用:如果我们的源对象有setter,getter的访问器属性,那么可以通过receiver来改变里面的this

const obj = {
_name: "xxx",
age: 18,
get name() {
return this._name // 改变这里的this
},
set name(newValue) {
this._name = newValue
}
}

const objProxy = new Proxy(obj, {
get(target, key, receiver) {
// receiver是创建出来的代理对象
console.log(receiver === objProxy);
console.log('get方法被访问', key, receiver);
return Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) {
let res = Reflect.set(target, key, newValue, receiver)
console.log('set成功', res);
}

})


objProxy.name = 'kkk'
console.log(objProxy.name);

改变this,让它指向代理对象 又对代理对象有操作,所以捕获了两次

image.png

Reflect中的construct

很少很少用到叭,ryfES6文档:

Reflect.construct(target, args)

等同于new target(...args),这提供了一种不使用new,来调用构造函数的方法。

应用场景,直接看例子

function Student(name, age) {
this.name = name
this.age = age
}

function Teacher() {}

// 执行Student函数中的内容,但是创建出来的对象是Teacher对象
const teacher = Reflect.construct(Student, ["xxx", 18], Teacher)

console.log(teacher);
// Teacher {name: 'xxx', age: 18}
console.log(teacher.__proto__ === Teacher.prototype);
// true

响应式原理

什么是响应式

先看一段代码:

let m = 100

// 一段代码
console.log(m);
console.log(m * 2);
console.log(m + 100);

m = 200

当 m 发生改变的时候,上面依赖m的一段代码自动重新执行

这种可以自动响应数据变量的代码机制,我们就称之为响应式

实现响应式

响应式函数设计与封装

首先,需要重新执行的代码可能有很多行,因此我们可以将这些代码放到一个函数中

当数据发生变化的时候,我们让这个函数执行

// 对象的响应式
const obj = {
name: 'xxx',
age: 18
}

// 依赖obj的代码
function objFn() {
console.log('obj变了');
}

obj.name = 'kkk'
// 当数据发生变化的时候,函数再次执行
objFn()

这是我们简单的思想,如果我们有多个响应式函数需要再次执行呢?

那么我们可以封装一个函数,把这些响应式函数都收集起来

// 封装一个响应式的函数
// 把响应式函数收集到数组中 // 改
let reactiveFns = []
function watchFn(fn) {
reactiveFns.push(fn)
}

// 对象的响应式
const obj = {
name: 'xxx',
age: 18
}

// 我们把需要响应式的函数传入watchFn
watchFn(function() { // 改
console.log('obj变了');
})

watchFn(function() {
console.log('name变了');
})

obj.name = 'kkk'
// 当数据发生变化的时候,执行收集的响应式函数
reactiveFns.forEach(fn => { // 改
fn()
})

响应式依赖的收集

目前我们收集的依赖全都放到一个数组来保存,这存在很大的问题。

实际开发中我们需要监听很多对象的响应式,这些对象需要监听的属性也不止一个,它们都会有对应的响应式函数

所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数

class Depend { 		// 改
constructor() {
this.reactiveFns = []
}

addDepend(reactiveFn) {
this.reactiveFns.push(reactiveFn)
}

// 通知执行依赖
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}

const depend = new Depend() // 改
function watchFn(fn) {
depend.addDepend(fn)
}

// 对象的响应式
const obj = {
name: 'xxx',
age: 18
}

watchFn(function() {
console.log('obj的name变了', obj.name);
})

watchFn(function() {
console.log('name变了', obj.name);
})

obj.name = 'kkk'
// 当数据发生变化的时候,执行收集的响应式函数
depend.notify() // 改

自动监听对象变化

现在我们有一个问题,数据发生改变的时候,我们是手动去调用notify()让依赖函数执行的

我们需要自动监听对象变化

注意,我们创建了代理对象之后,对原对象的操作都通过代理对象完成了

class Depend {
constructor() {
this.reactiveFns = []
}
addDepend(reactiveFn) {
this.reactiveFns.push(reactiveFn)
}
// 通知执行依赖
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}

const depend = new Depend()
function watchFn(fn) {
depend.addDepend(fn)
}

// 对象的响应式
const obj = {
name: 'xxx',
age: 18
}

// 自动监听对象的属性变化
const objProxy = new Proxy(obj, { // 改
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
depend.notify() // 在这里执行
}
})

watchFn(function() {
console.log('obj的name变了', objProxy.name);
})

watchFn(function() {
console.log('name变了', objProxy.name);
})

// 对代理对象操作
objProxy.name = 'kkk' // 改

Vue3 用的Proxy,Vue2用Object.defineProperty() 监听

依赖收集的管理

我们可以看出来,上面只创建了一个depend对象来管理obj.name变化需要监听的响应式函数

如果我们有多个对象,需要监听不同的属性,那么我们可以怎么样来管理不同对象的不同依赖关系呢?

依赖收集的数据结构,这里必须要理清楚这种结构的设计,我们用到了 Map,WeakMap

image.png

伪代码:结构大概就是这样子

const obj1Map = new Map()
obj1Map.set("name", "name的所有depend")
obj1Map.set("age", "age的所有depend")

const obj2Map = new Map()
obj2Map.set("height", "height的所有depend")

let targetMap = new WeakMap()

targetMap(obj1, obj1Map)
targetMap(obj2, obj2Map)

// 当数据变化
obj1.name = ''

// 拿到obj1的name属性的所有依赖
// 根据对象拿对应的map,再根据属性拿依赖函数
const depend = targetMap.get(obj).get("name")

具体代码实现:

先创建一个weakMap,并封装一个获取depend的函数

class Depend {
constructor() {
this.reactiveFns = []
}

addDepend(reactiveFn) {
this.reactiveFns.push(reactiveFn)
}

// 通知执行依赖
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}

// 封装一个响应式的函数
const depend = new Depend()
function watchFn(fn) {
depend.addDepend(fn)
}

// 封装一个获取depend的函数 // 改
const targetMap = new WeakMap()
function getDepends(target, key) {
// 根据对象获取map
let map = targetMap.get(target)
// 如果没有获取到map,说明这个对象是第一次添加依赖
// 我们要给这个对象新建一个map
if (!map) {
map = new Map()
targetMap.set( target, map)
}

// 根据key获取依赖
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}

// 对象的响应式
const obj = {
name: 'xxx',
age: 18
}

// 自动监听对象的属性变化
const objProxy = new Proxy(obj, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)

// 捕获获取值的操作,获取依赖,然后执行
const depend = getDepends(target, key) // 改
depend.notify() // 在这里执行
}
})

watchFn(function() {
console.log('obj的name变了', objProxy.name);
})

watchFn(function() {
console.log('name变了', objProxy.name);
})

// 对代理对象操作
objProxy.name = 'kkk'

现在我们执行会发现,根本没有实现响应式,为什么?

因为我们在watchFn添加依赖函数的时候,不管三七二十一,全都添加到一个depend里面了

image.png

所以当我们获取依赖的时候,根本无法根据对象,key来正确的获取对应key的依赖函数

  • 那么正确的依赖应该在哪里收集呢?

    应该在我们调用Proxy的get捕获器的时候。当我们第一次获取属性值的时候,我们就应该对这个key收集它自己的依赖

  • 另外,当我们在get里面添加依赖的时候出现一个问题,我们怎么拿到这个响应函数呢?

    定义一个全局的变量 activeReactiveFn

class Depend {
constructor() {
this.reactiveFns = []
}

addDepend(reactiveFn) {
this.reactiveFns.push(reactiveFn)
}

// 通知执行依赖
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}

// 封装一个响应式的函数
let activeReactiveFn = null
function watchFn(fn) { // 改!!
activeReactiveFn = fn
fn() // 用原数据先执行一次函数, 收集依赖
activeReactiveFn = null
}

// 封装一个获取depend的函数
const targetMap = new WeakMap()
function getDepends(target, key) {
// 根据对象获取map
let map = targetMap.get(target)
// 如果没有获取到map,说明这个对象是第一次添加依赖
// 我们要给这个对象新建一个map
if (!map) {
map = new Map()
targetMap.set( target, map)
}

// 根据key获取依赖
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}

// 对象的响应式
let obj = {
name: 'xxx',
age: 18
}

// 自动监听对象的属性变化
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
// 根据target,key获取对应的depend // 改!
const depend = getDepends(target, key)
// 给depend对象中添加响应式函数
depend.addDepend(activeReactiveFn)
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)

// 捕获获取值的操作,获取依赖,然后执行
const depend = getDepends(target, key)
depend.notify() // 在这里执行
}
})

watchFn(function() {
console.log('obj的name', objProxy.name);
})

watchFn(function() {
console.log('name', objProxy.name);
})

watchFn(function() {
console.log('age', objProxy.age);
})

console.log('----------修改前---');

objProxy.name = 'kkk'
// objProxy.age = 20
image.png

一个注意的地方:在watchFn()函数里面,我们会先调用一次传入的函数,为什么呢?

因为这个函数如果有对某个属性有get的操作的话,我们就能捕获到,而我们在get里面进行收集依赖的操作,就可以收集到这个属性的依赖了

对 Depend 重构

现在依然存在问题:

  • 如果函数中用到了两次key,那么我们这个函数就会被收集两次,即重复收集了依赖,后面我们执行depend里面的函数的时候,就会重复执行这个函数

    watchFn(function() {
    console.log('name', objProxy.name);
    console.log('name22', objProxy.name);
    })

    console.log('----------修改前---');
    objProxy.name = 'kkk'
    image.png

​ 解决方法:使用Set来保存依赖函数(Set元素不重复)

  • 还有一个可以优化的地方:我们并不希望将添加 activeReactiveFn 方法放在get里面,因为这是属于 Depend 类的行为

修改后:

// 保存当前需要收集的响应式函数
let activeReactiveFn = null // 改!
class Depend {
constructor() {
this.reactiveFns = new Set() // 改!
}
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn) // 改!
}
}
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}

// 封装一个响应式的函数
function watchFn(fn) {
activeReactiveFn = fn
fn() // 用原数据先执行一次函数, 收集依赖
activeReactiveFn = null
}

// 封装一个获取depend的函数
const targetMap = new WeakMap()
function getDepends(target, key) {
let map = targetMap.get(target)
if (!map) {
map = new Map()
targetMap.set( target, map)
}

// 根据key获取依赖
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}

// 对象的响应式
let obj = {
name: 'xxx',
age: 18
}

// 自动监听对象的属性变化
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
// 根据target,key获取对应的depend
const depend = getDepends(target, key)
// 给depend对象中添加响应式函数
depend.depend() // 改!
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
const depend = getDepends(target, key)
depend.notify() // 在这里执行
}
})

watchFn(function() {
console.log('name', objProxy.name);
console.log('name22', objProxy.name);
})
console.log('----------修改前---');
objProxy.name = 'kkk'
image.png

对象的响应式操作

最后一个问题,前面我们都是对一个对象(obj)实现响应式,那么如果是多个对象呢?

所以我们要创建一个函数reactive,针对所有的对象都可以变成响应式对象

也就是实现多个对象的响应式

// 保存当前需要收集的响应式函数
let activeReactiveFn = null

class Depend {
constructor() {
this.reactiveFns = new Set()
}

depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn)
}
}

notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}

// 封装一个响应式的函数
function watchFn(fn) {
activeReactiveFn = fn
fn() // 用原数据先执行一次函数, 收集依赖
activeReactiveFn = null
}

// 封装一个获取depend的函数
const targetMap = new WeakMap()
function getDepends(target, key) {
let map = targetMap.get(target)
if (!map) {
map = new Map()
targetMap.set( target, map)
}

// 根据key获取依赖
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}

// 接收一个对象,返回一个响应式对象 // 改!!!
function reactive(obj) {
return new Proxy(obj, {
get: function(target, key, receiver) {
// 根据target,key获取对应的depend
const depend = getDepends(target, key)
// 给depend对象中添加响应式函数
depend.depend() // 改!
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
const depend = getDepends(target, key)
depend.notify() // 在这里执行
}
})
}


// 把需要响应式的对象传到reactive函数
let obj = reactive({ // 改!!
name: 'xxx',
age: 18
})

let info = reactive({ // 改!!
height: 1.88
})

watchFn(function() {
// 注意这里 因为reactive返回的就是代理对象,我们用obj接收,obj就是响应式的了,所以直接对obj操作
console.log('name', obj.name);
console.log('name22', obj.name);
})

watchFn(function() {
console.log('height', info.height);
})

console.log('--------------修改前---');

info.height = 1.78
obj.name = 'kkk'
image.png

到此!我们的响应式就实现完成了

Vue2响应式原理

前面所实现的响应式的代码,其实是Vue3中的响应式原理

Vue3主要是通过Proxy来监听数据的变化以及收集相关 的依赖的

而Vue2是通过Object.defineProerty 的方式来实现对象属性的监听

function reactive(obj) {
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get() {
const depend = getDepends(obj, key)
depend.depend()
return value
},
set(newValue) {
const depend = getDepends(obj, key)
value = newValue
depend.notify()
}
})
})
return obj
}

在传入对象的时候,遍历所有的key,并且通过属性存储描述符来监听属性的获取和修改,

其他的逻辑和前面Vue3的响应式实现是一致的