为什么前端框架Vue能够做到响应式?当依赖数据发生变化时,会对页面进行自动更新,其原理还是在于对响应式数据的获取和设置进行了监听,一旦监听到数据发生变化,依赖该数据的函数就会重新执行,达到更新的效果。那么我们如果想监听对象中的属性被设置和获取的过程,可以怎么做呢?
在ES6之前,如果想监听对象属性的获取和设置,可以借助Object.defineProperty方法的存取属性描述符来实现,具体怎么用呢?我们来看一下。
const obj = {
name: 'curry',
age: 30
}
// 1.拿到obj所有的key
const keys = Object.keys(obj)
// 2.遍历obj所有的key,并设置存取属性描述符
keys.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 = 'kobe' // obj对象的name属性被设置啦!
obj.age = 24 // obj对象的age属性被设置啦!
// 访问:
console.log(obj.name) // obj对象的name属性被访问啦!
console.log(obj.age) // obj对象的age属性被访问啦!
在Vue2.x中响应式原理实现的核心就是使用的Object.defineProperty,而在Vue3.x中响应式原理的核心被换成了Proxy,为什么要这样做呢?主要是Object.defineProperty用来监听对象属性变化,有以下缺点:
在ES6中,新增了一个Proxy类,翻译为代理,它可用于帮助我们创建一个代理对象,之后我们可以在这个代理对象上进行许多的操作。
如果希望监听一个对象的相关操作,当Object.defineProperty不能满足我们的需求时,那么可以使用Proxy创建一个代理对象,在代理对象上,我们可以监听对原对象进行了哪些操作。下面将上面的例子用Proxy来实现,看看效果。
基本语法:const p = new Proxy(target, handler)
const obj = {
name: 'curry',
age: 30
}
// 创建obj的代理对象
const objProxy = new Proxy(obj, {
// 获取对象属性值的捕获器
get: function(target, key) {
console.log(`obj对象的${key}属性被访问啦!`)
return target[key]
},
// 设置对象属性值的捕获器
set: function(target, key, newValue) {
console.log(`obj对象的${key}属性被设置啦!`)
target[key] = newValue
}
})
// 之后的操作都是拿代理对象objProxy
// 设置:
objProxy.name = 'kobe' // obj对象的name属性被设置啦!
objProxy.age = 24 // obj对象的age属性被设置啦!
// 访问:
console.log(objProxy.name) // obj对象的name属性被访问啦!
console.log(objProxy.age) // obj对象的age属性被访问啦!
// 可以发现原对象obj同时发生了改变
console.log(obj) // { name: 'kobe', age: 24 }
在上面的例子中,其实已经使用到了set和get捕获器,而set和get捕获器是最为常用的捕获器,下面具体来看看这两个捕获器吧。
(1)set捕获器
set函数可接收四个参数:
(2)get捕获器
get函数可接收三个参数:
上面所讲的都是对对象属性的操作进行监听,其实Proxy提供了更为强大的功能,可以帮助我们监听函数的调用方式。
function fn(x, y) {
return x + y
}
const fnProxy = new Proxy(fn, {
/*
target: 目标函数(fn)
thisArg: 指定的this对象,也就是被调用时的上下文对象({ name: 'curry' })
argumentsList: 被调用时传递的参数列表([1, 2])
*/
apply: function(target, thisArg, argumentsList) {
console.log('fn函数使用apply进行了调用')
return target.apply(thisArg, argumentsList)
},
/*
target: 目标函数(fn)
argumentsList: 被调用时传递的参数列表
newTarget: 最初被调用的构造函数(fnProxy)
*/
construct: function(target, argumentsList, newTarget) {
console.log('fn函数使用new进行了调用')
return new target(...argumentsList)
}
})
fnProxy.apply({ name: 'curry' }, [1, 2]) // fn函数使用apply进行了调用
new fnProxy() // fn函数使用new进行了调用
除了上面提到的4种捕获器,Proxy还给我们提供了其它9种捕获器,一共是13个捕获器,下面对这13个捕获器进行简单总结,下面表格的捕获器分别对应对象上的一些操作方法。
捕获器handler |
捕获对象 |
get() |
属性读取操作 |
set() |
属性设置操作 |
has() |
in操作符 |
deleteProperty() |
delete操作符 |
apply() |
函数调用操作 |
construct() |
new操作符 |
getPrototypeOf() |
Object.getPrototypeOf() |
setPrototypeOf() |
Object.setPrototypeOf() |
isExtensible() |
Object.isExtensible() |
preventExtensions() |
Object.perventExtensions() |
getOwnPropertyDescriptor() |
Object.getOwnPropertyDescriptor() |
defineProperty() |
Object.defineProperty() |
ownKeys() |
Object.getOwnPropertySymbols() |
Proxy捕获器具体用法可查阅MDN:https://developer.mozilla.org/zh-CN/docs/Web/JAVAScript/Reference/Global_Objects/Proxy
在ES6中,还新增了一个API为Reflect,翻译为反射,为一个内置对象,一般用于搭配Proxy进行使用。
可能会有人疑惑,为什么在这里提到Reflect,它具体有什么作用呢?怎么搭配Proxy进行使用呢?
在上述Proxy中,操作对象的方法都可以换成对应的Reflect上的方法,基本使用如下:
const obj = {
name: 'curry',
age: 30
}
// 创建obj的代理对象
const objProxy = new Proxy(obj, {
// 获取对象属性值的捕获器
get: function(target, key) {
console.log(`obj对象的${key}属性被访问啦!`)
return Reflect.get(target, key)
},
// 设置对象属性值的捕获器
set: function(target, key, newValue) {
console.log(`obj对象的${key}属性被设置啦!`)
Reflect.set(target, key, newValue)
},
// 删除对象属性的捕获器
deleteProperty: function(target, key) {
console.log(`obj对象的${key}属性被删除啦!`)
Reflect.deleteProperty(target, key)
}
})
// 设置:
objProxy.name = 'kobe' // obj对象的name属性被设置啦!
objProxy.age = 24 // obj对象的age属性被设置啦!
// 访问:
console.log(objProxy.name) // obj对象的name属性被访问啦!
console.log(objProxy.age) // obj对象的age属性被访问啦!
// 删除:
delete objProxy.name // obj对象的name属性被删除啦!
对比Object,我们来看一下Reflect上常见的操作对象的方法(静态方法):
Reflect方法 |
类似于 |
get(target, propertyKey [, receiver]) |
获取对象某个属性值,target[name] |
set(target, propertyKey, value [, receiver]) |
将值分配给属性的函数,返回一个boolean |
has(target, propertyKey) |
判断一个对象是否存在某个属性,和in运算符功能相同 |
deleteProperty(target, propertyKey) |
delete操作符,相当于执行delete target[name] |
apply(target, thisArgument, argumentsList) |
对一个函数进行调用操作,可以传入一个数组作为调用参数,Function.prototype.apply() |
construct(target, argumentsList [, newTarget]) |
对构造函数进行new操作,new target(...args) |
getPrototypeOf(target) |
Object.getPrototype() |
setPrototypeOf(target, prototype) |
设置对象原型的函数,返回一个boolean |
isExtensible(target) |
Object.isExtensible() |
preventExtensions(target) |
Object.preventExtensions(),返回一个boolean |
getOwnPropertyDescriptor(target, propertyKey) |
|
defineProperty(target, propertyKey, attributes) |
Object.defineProperty(),设置成功返回true |
ownKeys(target) |
返回一个包含所有自身属性(不包含继承属性)的数组,类似于Object.keys(),但是不会受enumerable影响 |
具体Reflect和Object对象之间的关系和使用方法,可以参考MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
construct方法有什么作用呢?具体的应用场景是什么?这里提一个需求,就明白construct方法的作用了。
需求:创建Person和Student两个构造函数,最终的实例对象执行的是Person中的代码,带上实例对象的类型是Student。
construct可接收的参数:
function Person(name, age) {
this.name = name
this.age = age
}
function Student() {}
const stu = Reflect.construct(Person, ['curry', 30], Student)
console.log(stu)
console.log(stu.__proto__ === Student.prototype)
打印结果:实例对象的类型为Student,并且实例对象原型指向Student构造函数的原型。
Reflect的construct方法就可以用于类继承的实现,可在babel工具中查看ES6转ES5后的代码,就是使用的Reflect的construct方法:
在介绍Proxy的set和get捕获器的时候,其中有个参数叫receiver,具体什么是调用的代理对象呢?它的作用是什么?
如果原对象(需要被代理的对象)它有自己的getter和setter服务器属性时,那么就可以通过receiver来改变里面的this。
// 假设obj的age为私有属性,需要通过getter和setter来访问和设置
const obj = {
name: 'curry',
_age: 30,
get age() {
return this._age
},
set age(newValue) {
this._age = newValue
}
}
const objProxy = new Proxy(obj, {
get: function(target, key, reveiver) {
console.log(`obj对象的${key}属性被访问啦!`)
return Reflect.get(target, key)
},
set: function(target, key, newValue, reveiver) {
console.log(`obj对象的${key}属性被设置啦!`)
Reflect.set(target, key, newValue)
}
})
// 设置:
objProxy.name = 'kobe'
objProxy.age = 24
// 访问:
console.log(objProxy.name)
console.log(objProxy.age)
在没有使用receiver的情况下的打印结果为:name和age属性都被访问一次和设置一次。
但是由于原对象obj中对age进行了拦截操作,我们看一下age具体的访问步骤:
// 假设obj的age为私有属性,需要通过getter和setter来访问和设置
const obj = {
name: 'curry',
_age: 30,
get age() {
return this._age
},
set age(newValue) {
this._age = newValue
}
}
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
console.log(`obj对象的${key}属性被访问啦!`)
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
console.log(`obj对象的${key}属性被设置啦!`)
Reflect.set(target, key, newValue, receiver)
}
})
// 设置:
objProxy.name = 'kobe'
objProxy.age = 24
// 访问:
console.log(objProxy.name)
console.log(objProxy.age)
再来看一下打印结果:
也可以打印receiver,在浏览器中进行查看,其实就是这里的objProxy:
当某个变量值发生变化时,会自动去执行某一些代码。如下代码,当变量num发生变化时,对num有所依赖的代码可以自动执行。
let num = 30
console.log(num) // 当num方式变化时,这段代码能自动执行
console.log(num * 30) // 当num方式变化时,这段代码能自动执行
num = 1
在响应式中,需要执行的代码可能不止一行,而且也不可能一行行去执行,所以可以将这些代码放到一个函数中,当数据发生变化,自动去执行某一个函数。但是在开发中有那么多函数,怎么判断哪些函数需要响应式?哪些又不需要呢?
const obj = {
name: 'curry',
age: 30
}
// 定义一个存放响应式函数的数组
const reactiveFns = []
// 封装一个用于收集响应式函数的函数
function watchFn(fn) {
reactiveFns.push(fn)
}
watchFn(function() {
let newName = obj.name
console.log(newName)
console.log('1:' + obj.name)
})
watchFn(function() {
console.log('2:' + obj.name)
})
obj.name = 'kobe'
// 当obj中的属性值发送变化时,遍历执行那些收集的响应式函数
reactiveFns.forEach(fn => {
fn()
})
上面实现的收集响应式函数,目前是存放到一个数组中来保存的,而且只是对name属性的的依赖进行了收集,如果age属性也需要收集,不可能都存放到一个数组里面,而且属性值改变后,还需要通过手动去遍历调用,显而易见是很麻烦的,下面做一些优化。
class Depend {
constructor() {
// 用于存放响应式函数
this.reactiveFns = []
}
// 用户添加响应式函数
addDependFn(fn) {
this.reactiveFns.push(fn)
}
// 用于执行响应式函数
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
const obj = {
name: 'curry',
age: 30
}
const dep = new Depend()
// 在watchFn中使用dep的addDependFn来收集
function watchFn(fn) {
dep.addDependFn(fn)
}
watchFn(function() {
let newName = obj.name
console.log(newName)
console.log('1:' + obj.name)
})
watchFn(function() {
console.log('2:' + obj.name)
})
obj.name = 'kobe'
// name属性发生改变,直接调用notify
dep.notify()
在修改对象属性值后,还是需要手动去调用其notify函数来通知响应式函数执行,其实可以做到自动监听对象属性的变化,来自动调用notify函数,这个想必就很容易了,在前面做了那么多功课,就是为了这里,不管是用Object.defineProperty还是Proxy都可以实现对象的监听,这里我使用功能更加强大的Proxy,并结合Reflect来实现。
class Depend {
constructor() {
// 用于存放响应式函数
this.reactiveFns = []
}
// 用户添加响应式函数
addDependFn(fn) {
this.reactiveFns.push(fn)
}
// 用于执行响应式函数
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
const obj = {
name: 'curry',
age: 30
}
const dep = new Depend()
// 在watchFn中使用dep的addDependFn来收集
function watchFn(fn) {
dep.addDependFn(fn)
}
// 创建一个Proxy
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// 当set捕获器捕获到属性变化时,自动去调用notify
dep.notify()
}
})
watchFn(function() {
let newName = objProxy.name
console.log(newName)
console.log('1:' + objProxy.name)
})
watchFn(function() {
console.log('2:' + objProxy.name)
})
objProxy.name = 'kobe'
objProxy.name = 'klay'
objProxy.name = 'james'
注意:后面使用到的obj对象,需都换成代理对象objProxy,这样储能监听到属性值是否被设置了。
打印结果:name属性修改了三次,对应依赖函数就执行了三次。
在上面实现响应式过程中,都是基于一个对象的一个属性,如果有多个对象,这多个对象中有不同或者相同的属性呢?我们应该这样去单独管理不同对象中每个属性所对应的依赖呢?应该要做到当某一个对象中的某一个属性发生变化时,只去执行对这个对象中这个属性有依赖的函数,下面就来讲一下怎样进行数据存储,能够达到我们的期望。
在ES16中,给我们新提供了两个新特性,分别是Map和WeakMap,这两个类都可以用于存放数据,类似于对象,存放的是键值对,但是Map和WeakMap的key可以存放对象,而且WeakMap对对象的引用是弱引用。如果对这两个类不太熟悉,可以去看看上一篇文章:ES6-ES12简单知识点总结
如果有以下obj1和obj2两个对象,来看一下它们大致的存储形式:
const obj1 = { name: 'curry', age: 30 }
const obj2 = { name: 'kobe', age: 24 }
已经确定了怎么存储了,下面就来实现一下吧。
// 1.创建一个WeakMap存储结构,存放对象
const objWeakMap = new WeakMap()
// 2.封装一个获取dep的函数
function getDepend(obj, key) {
// 2.1.根据对象,获取对应的map
let map = objWeakMap.get(obj)
// 如果是第一次获取这个map,那么需要先创建一个map
if (!map) {
map = new Map()
// 将map存到objWeakMap中对应key上
objWeakMap.set(obj, map)
}
// 2.2.根据对象的属性,获取对应的dep
let dep = map.get(key)
// 如果是第一次获取这个dep,那么需要先创建一个dep
if (!dep) {
dep = new Depend()
// 将dep存到map中对应的key上
map.set(key, dep)
}
// 2.3最终将dep返回出去
return dep
}
在Proxy的捕获器中获取对应的dep:
// 创建一个Proxy
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// 根据当前对象target和设置的key,去获取对应的dep
const dep = getDepend(target, key)
console.log(dep)
// 当set捕获器捕获到属性变化时,自动去调用notify
dep.notify()
}
})
可以发现上面打印的结果中的响应式函数数组全部为空,是因为在前面收集响应式函数是通过watchFn来收集的,而在getDepend中并没有去收集对应的响应式函数,所以返回的dep对象里面的数组全部就为空了。如果对响应式函数,还需要通过自己一个个去收集,是不太容易的,所以可以监听响应式函数中依赖了哪一个对象属性,让Proxy的get捕获器去收集就行了。
// 定义一个全局变量,存放当前需要收集的响应式函数
let currentReactiveFn = null
function watchFn(fn) {
currentReactiveFn = fn
// 先调用一次函数,提醒Proxy的get捕获器需要收集响应式函数了
fn()
// 收集完成将currentReactiveFn重置
currentReactiveFn = null
}
Proxy中get捕获器具体需要执行的操作:
// 创建一个Proxy
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
const dep = getDepend(target, key)
// 拿到全局的currentReactiveFn进行添加
dep.addDependFn(currentReactiveFn)
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// 根据当前对象target和设置的key,去获取对应的dep
const dep = getDepend(target, key)
console.log(dep)
// 当set捕获器捕获到属性变化时,自动去调用notify
dep.notify()
}
})
下面测试一下看看效果:
watchFn(function() {
console.log('1:我依赖了name属性')
console.log(objProxy.name)
})
watchFn(function() {
console.log('2:我依赖了name属性')
console.log(objProxy.name)
})
watchFn(function() {
console.log('1:我依赖了age属性')
console.log(objProxy.age)
})
watchFn(function() {
console.log('2:我依赖了age属性')
console.log(objProxy.age)
})
console.log('----------以上为初始化执行,以下为修改后执行-------------')
objProxy.name = 'kobe'
objProxy.age = 24
截止到上面,大部分响应式原理已经实现了,但是还存在一些小问题需要优化。
// 将currentReactiveFn放到Depend之前,方便其拿到
let currentReactiveFn = null
class Depend {
constructor() {
// 用于存放响应式函数
this.reactiveFns = new Set()
}
// 用户添加响应式函数
addDependFn() {
// 先判断一下currentReactiveFn是否有值
if (currentReactiveFn) {
this.reactiveFns.add(currentReactiveFn)
}
}
// 用于执行响应式函数
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
Proxy中就不用去收集响应式函数了,直接调用addDependFn即可:
// 创建一个Proxy
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
const dep = getDepend(target, key)
// 直接调用addDepend方法,让它去收集
dep.addDependFn()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// 根据当前对象target和设置的key,去获取对应的dep
const dep = getDepend(target, key)
// 当set捕获器捕获到属性变化时,自动去调用notify
dep.notify()
}
})
前面都只讲了一个对象实现响应式的实现,如果有多个对象需要实现可响应式呢?将Proxy封装一下,外面套一层函数即可,调用该函数,返回该对象的代理对象。
function reactive(obj) {
return new Proxy(obj, {
get: function(target, key, receiver) {
const dep = getDepend(target, key)
// 直接调用addDepend方法,让它去收集
dep.addDependFn()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// 根据当前对象target和设置的key,去获取对应的dep
const dep = getDepend(target, key)
// 当set捕获器捕获到属性变化时,自动去调用notify
dep.notify()
}
})
}
看一下具体使用效果:
const obj1 = { name: 'curry', age: 30 }
const obj2 = { weight: '130', height: '180' }
const obj1Proxy = reactive(obj1)
const obj2Proxy = reactive(obj2)
watchFn(function() {
console.log('我依赖了obj1的name属性')
console.log(obj1Proxy.name)
})
watchFn(function() {
console.log('我依赖了age属性')
console.log(obj1Proxy.age)
})
watchFn(function() {
console.log('我依赖了obj2的weight属性')
console.log(obj2Proxy.weight)
})
watchFn(function() {
console.log('我依赖了obj2的height属性')
console.log(obj2Proxy.height)
})
console.log('----------以上为初始化执行,以下为修改后执行-------------')
obj1Proxy.name = 'kobe'
obj1Proxy.age = 24
obj2Proxy.weight = 100
obj2Proxy.height = 165
通过上面9步完成了最终响应式原理的实现,下面对其进行整理一下:
总结:以上通过Proxy来监听对象操作的实现响应式的方法就是Vue3响应式原理了。
Vue3响应式原理已经实现了,那么Vue2只需要将Proxy换成Object.defineProperty就可以了。
function reactive(obj) {
// 1.拿到obj所有的key
const keys = Object.keys(obj)
// 2.遍历所有的keys,添加存取属性描述符
keys.forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get: function() {
const dep = getDepend(obj, key)
// 直接调用addDepend方法,让它去收集
dep.addDependFn()
return value
},
set: function(newValue) {
value = newValue
// 根据当前对象设置的key,去获取对应的dep
const dep = getDepend(obj, key)
// 监听到属性变化时,自动去调用notify
dep.notify()
}
})
})
// 3.将obj返回
return obj
}
本文来自
https://www.cnblogs.com/MomentYY/p/16065162.html