很多前端 JavaScript 框架,包含但不限于(Angular,React,Vue)都拥有自己的响应式方法,而这些方法其实都是基于监听对象来实现的,这里我们来看看监听对象实现响应式的原理。
Object.defineProperty 监听对象
如果需要监听对象的属性变化,可以使用Object.defineProperty的存储属性描述符的方法来监听。
const obj = {
a: 1,
b: 2,
c: 3
}
// 监听一个属性的变化
Object.defineProperty(obj, 'a', {
set(val) {
console.log('a set', val)
},
get() {
console.log('a get')
return this.a
}
})
<!-- more -->
// 监听所有属性的变化
Object.keys(obj).forEach(key => {
let val = obj[key]
Object.defineProperty(obj, key, {
get() {
console.log(key, 'get')
return val
},
set(newVal) {
console.log(key, 'set', newVal)
val = newVal
},
})
});
console.log(obj.a) // a get 1
obj.a = 2 // a set 2
但通过Object.defineProperty会带来很多问题:
- 如果需要监听对象的增删操作,这个方法就会失效
- 定义属性时,所有的普通属性都会强行变成属性描述符
存储数据描述符设计的初衷并不是为了去监听一个完整的对象。
Proxy的基本使用
在ES6中,新增了Proxy类,用于为对象创建一个代理:
如果我们希望监听一个对象的相关操作,可以先创建一个代理对象(Proxy),然后将相关操作都在代理对象上完成。
const obj = {
a: 1,
b: 2,
c: 3
}
const proxy = new Proxy(obj, {
get(target, key) {
console.log('get', key)
return target[key]
},
set(target, key, val) {
console.log('set', key, val)
target[key] = val
},
has(target, key) {
console.log('has', key)
return key in target
},
deleteProperty(target, key) {
console.log('delete', key)
delete target[key]
}
})
console.log(proxy.a); // get a 1
proxy.a = 2; // set a 2
console.log('b' in proxy); // has b true
delete proxy.c // delete c
Proxy除了可以代理对象外,还能代理函数。
function foo() {
}
const proxy = new Proxy(foo, {
apply(target, thisArg, argArray) {
console.log('apply')
return target.apply(thisArg, argArray)
},
construct(target, argArray) {
console.log('construct')
return new target(...argArray)
},
})
proxy.apply({}, [1, 2]) // apply
new proxy('1','2') // construct
Proxy有很多捕获器方法,具体可以查阅MDN文档proxy。
Reflect基本使用
Reflect也是ES6新增的一个API,它是一个对象,它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法:比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf(); Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty()……
为什么要有Reflect呢?早期ECMA规范中没有考虑到这种对对象本身的操作如何设计会更加规范,所以将这些API放到了Object上面。但是Object作为一个构造函数,这些操作实际上放到它身上并不合适。另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的。所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上。
// 将之前代理转换成Reflect使用
const obj = {
a: 1,
b: 2,
c: 3
}
const proxy = new Proxy(obj, {
get(target, key, receiver) {
console.log('get', key)
return Reflect.get(target, key, receiver)
},
set(target, key, val, receiver) {
console.log('set', key, val)
return Reflect.set(target, key, val, receiver)
},
has(target, key) {
console.log('has', key)
return Reflect.has(target, key)
},
deleteProperty(target, key) {
console.log('delete', key)
return Reflect.deleteProperty(target, key)
}
})
Receiver的作用
receiver的参数,它的作用是什么呢?如果我们的源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this。
function Student(name, age) {
this.name = name
this.age = age
}
function Teacher() {}
const teacher = Reflect.construct(Student, ['Tom', 18], Teacher)
console.log(teacher); // Teacher { name: 'Tom', age: 18 }
console.log(teacher instanceof Teacher) // true
响应式原理
响应式的定义
响应式是指,当一个对象的属性发生变化时,会自动触发对应的方法。
变量响应式
let a = 10
// 需要执行的代码
console.log(a)
console.log(a * 2)
// 当a发生变化时上面代码需要再次执行
a = 20
对象响应式
let obj = {
a: 1,
b: 2,
c: 3
}
console.log(obj.a); // obj.a 改变时需要再次执行
console.log(obj.b); // obj.b 改变时需要再次执行
响应式函数的封装
通过创建一个数组收集变量触发需要执行的方法,然后在每次变量发生变化时,遍历数组,调用所有变量的响应式方法。
let fns = []
function watchFn(fn) {
fns.push(fn)
}
const obj = {
a: 1,
b: 2,
c: 3
}
watchFn(function() {
console.log(obj.a)
})
watchFn(function() {
console.log(obj.b)
}
// 变量改变时触发该函数
fns.forEach(fn => fn())
依赖收集封装
上述的方法在对多个对象做监听的时候,每创建一个对象都需要创建一个数组和方法,这样会比较麻烦。可以通过将该方法封装成一个类去实现:
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => sub.update())
}
}
const obj = {
a: 1,
b: 2,
c: 3
}
const dep = new Dep()
dep.addSub(function() {
console.log(obj.a)
})
dep.addSub(function() {
console.log(obj.b)
})
// 变量改变时触发该函数
dep.notify()
自动监听对象的变化
收集好依赖函数后,对象变化时如何触发相关依赖?我们可以通过proxy来实现:
// 沿用上个例子的代码
const proxy = new Proxy(obj, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, val, receiver) {
Reflect.set(target, key, val, receiver)
dep.notify()
}
})
proxy.a = 10 // 触发dep.notify() -> 10 2
依赖的管理
我们目前是创建了一个Depend对象,用来管理对于obj变化需要监听的响应函数:但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?
在前面关于ES6的文章中我提到过WeakMap,并且在学习WeakMap的时候我讲到了后面通过WeakMap如何管理这种响应式的数据依赖:
// 依赖封装
class Depend {
constructor() {
this.reactiveFns = []
}
addDepend(reactiveFn) {
this.reactiveFns.push(reactiveFn)
}
notify() {
this.reactiveFns.forEach(reactiveFn => reactiveFn())
}
}
// 响应式函数封装
let activeReactFn = null
function watchFn(reactiveFn) {
activeReactFn = reactiveFn
reactiveFn()
activeReactFn = null
}
// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
// 根据target获取map
let map = targetMap.get(target)
if (!map) {
map = new Map()
targetMap.set(target, map)
}
// 根据target获取map中的depend对象
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}
// 对象案例
const obj = {
name: '张三',
age: 18
}
// 监听代理
const objProxy = new Proxy(obj, {
get: function (target, key, receiver) {
// 获取depend对象
const depend = getDepend(target, key)
// 添加响应函数
depend.addDepend(activeReactFn)
// 返回值
return Reflect.get(target, key, receiver)
},
set: function (target, key, val, receiver) {
Reflect.set(target, key, val, receiver)
// 获取depend对象
const depend = getDepend(target, key)
// 响应函数
depend.notify()
}
})
watchFn(function() {
console.log('React Name')
console.log(objProxy.name)
})
watchFn(function() {
console.log('React Age')
console.log(objProxy.age)
})
objProxy.name = '李四' // 只会触发 React Name 李四,不会触发 React Age 18
依赖管理重构
上述的方法可以针对对象的某个变量完成响应式,但是如果响应式函数中变量被调用两次,那么这个函数会被收集两次,可以通过Set来解决这个问题,还可以将get的收集依赖方法放在Dep内部,避免混淆:
// 对上述两个方法进行改进
// 依赖封装
class Depend {
constructor() {
this.reactiveFns = new Set()
}
addDepend() {
if (activeReactiveFn)
this.reactiveFns.add(activeReactiveFn)
}
notify() {
this.reactiveFns.forEach(reactiveFn => reactiveFn())
}
}
// 监听代理
const objProxy = new Proxy(obj, {
get: function (target, key, receiver) {
// 获取depend对象
const depend = getDepend(target, key)
// 添加响应函数
depend.addDepend()
// 返回值
return Reflect.get(target, key, receiver)
},
set: function (target, key, val, receiver) {
Reflect.set(target, key, val, receiver)
// 获取depend对象
const depend = getDepend(target, key)
// 响应函数
depend.notify()
}
})
实现Vue3/Vue2响应式
通过上面的原理,再利用闭包的特性,可以实现Vue的响应式方法:
Vue3是通过Proxy实现响应式:
// vue3 reactive
function reactive(obj) {
return new Proxy(obj, {
get: function (target, key, receiver) {
// 获取depend对象
const depend = getDepend(target, key)
// 添加响应函数
depend.addDepend()
// 返回值
return Reflect.get(target, key, receiver)
},
set: function (target, key, val, receiver) {
Reflect.set(target, key, val, receiver)
// 获取depend对象
const depend = getDepend(target, key)
// 响应函数
depend.notify()
}
})
}
const info = reactive({
name: '张三',
age: 18
})
watchFn(function() {
console.log('React Name')
console.log(info.name)
})
info.name = '李四' // React Name 李四
Vue2是通过Object.defineProperty实现响应式:
// vue2 reactive
function reactive(obj) {
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get: function (target, key, receiver) {
// 获取depend对象
const depend = getDepend(target, key)
// 添加响应函数
depend.addDepend()
// 返回值
return Reflect.get(target, key, receiver)
},
set: function (target, key, val, receiver) {
Reflect.set(target, key, val, receiver)
// 获取depend对象
const depend = getDepend(target, key)
// 响应函数
depend.notify()
}
})
})
return obj
}
const info = reactive({
name: '张三',
age: 18
})
watchFn(function() {
console.log('React Name')
console.log(info.name)
})
info.name = '李四' // React Name 李四