什么是响应式
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
当逻辑中有一个变量X,有某些逻辑代码在运行的时候,需要使用变量X
那么我们就认为这些需要使用变量X的代码为变量X的依赖
而当变量X发生改变的时候,可以自动去执行变量X的所有依赖的过程,就被称之为响应式
模拟实现
响应式函数
const user = {
name: 'Klaus',
age: 23
}
const reactiveNameFns = []
const reactiveAgeFns = []
// 定义name的响应式函数
function watchNameEffect(fn) {
reactiveNameFns.push(fn)
}
// 定义age的响应式函数
function watchAgeEffect(fn) {
reactiveAgeFns.push(fn)
}
watchNameEffect(function() {
console.log('name需要响应的代码 - 1')
})
watchNameEffect(function() {
console.log('name需要响应的代码 - 2')
})
function foo() {
console.log('其它业务逻辑,不需要被响应')
}
watchAgeEffect(() => console.log('age需要响应的代码'))
user.name = 'Alex'
// user的name属性发生改变了,需要name需要响应的代码
reactiveNameFns.forEach(fn => fn())
console.log('----------------')
// user的age属性发生改变了,执行age需要响应的代码
reactiveAgeFns.forEach(fn => fn())
可以看到使用响应式函数后,当某一个属性发生改变的时候
对应的需要执行的代码都被正确的执行了
但是单单使用响应式函数是存在很多问题的
- 依赖并不是自动收集的,对应的更新也不是自动更新的,都是需要手动执行的
- 一个需要响应的属性 对应一个响应式函数和一个收集依赖的数组,存在很多的可抽取的重复代码
依赖响应类
const user = {
name: 'Klaus',
age: 23
}
// 定义响应类 --- 重复代码的抽离
class Dep {
constructor() {
this.depFns = []
}
depend(fn) {
this.depFns.push(fn)
}
notify() {
this.depFns.forEach(fn => fn())
}
}
// 一个属性 对应 一个Dep实例
const nameDep = new Dep()
const ageDep = new Dep()
nameDep.depend(() => console.log('name依赖 - 1'))
nameDep.depend(() => console.log('name依赖 - 2'))
nameDep.notify()
ageDep.depend(() => console.log('age依赖'))
ageDep.notify()
收集依赖的数据结构
之前的代码最大的问题是每一个属性会存在一个实例,随着项目越来越大,对应的dep实例也会越来越多
所以我们需要使用一个数据结构来管理所有的dep实例,这就是WeakMap
const user = {
name: 'Klaus',
age: 23
}
// activeReactiveFn 是为了在watchEffect 和 Dep实例的set方法 传递 响应函数
let activeReactiveFn = null
// 收集响应类
class Dep {
constructor() {
// 使用set的原因是
// 如果一个响应函数中使用了同一个属性多次,如果使用的是数组,那么同一个响应函数会被多次添加
// 此时如果执行notify方法,那么多个相同的响应函数会被依次重复执行
// 这是没有意义的,所以使用set来存储响应函数的目的是为了对应响应函数进行去重操作
this.depFns = new Set()
}
depend() {
if (activeReactiveFn) {
this.depFns.add(activeReactiveFn)
}
}
notify() {
this.depFns.forEach(fn => fn())
}
}
// 对所有的dep实例进行统一管理
const deps = new WeakMap()
// 根据对象和对象的属性名 去获取对应的 dep实例
function getDep(target, key) {
let map = deps.get(target)
if (!map) {
map = new Map()
deps.set(target, map)
}
let depends = map.get(key)
if (!depends) {
depends = new Dep()
map.set(key, depends)
}
return depends
}
// 监听对象的属性的set和get
const userProxy = new Proxy(user, {
get(target, key, receiver) {
// 收集依赖
const dependency = getDep(target, key)
dependency.depend()
return Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) {
// 需要先更新值后再去修改依赖
// 不然执行的响应式函数中使用的值依旧是更新前的值
Reflect.set(target, key, newValue, receiver)
const dependency = getDep(target, key)
dependency.notify()
}
})
function watchEffect(fn) {
activeReactiveFn = fn
// 收集依赖的时候,需要先执行一遍响应函数
// 只有这样,对应属性的get方法才会被执行
fn()
activeReactiveFn = null
}
// 只有在收集和使用的时候,全部使用代理后的对象,其属性对应的响应函数才会被正常收集和执行
// 如果使用原对象,其对应的属性的set和get方法并没有被修改,所以无法触发对应的响应式
// 响应依赖函数
watchEffect(() => console.log(`user的name发生了改变 - 1 --- ${userProxy.name}`))
watchEffect(() => console.log(`user的name发生了改变 - 2 --- ${userProxy.name}`))
watchEffect(() => console.log(`user的age发生了改变 ---- ${userProxy.age} --- ${userProxy.age}`))
console.log('-------------------------')
// userProxy.name = 'Alex'
userProxy.age = 18
但是,此时的代码实现依旧是存在弊端的,因为此时我们需要手动的将一个函数转换为对应的代理对象
对此,我们可以将此操作进行一个简单的封装,将对象转换为代理对象的过程封装成一个名为reactive
的函数
reactive函数
let activeReactiveFn = null
class Dep {
constructor() {
this.depFns = new Set()
}
depend() {
if (activeReactiveFn) {
this.depFns.add(activeReactiveFn)
}
}
notify() {
this.depFns.forEach(fn => fn())
}
}
const deps = new WeakMap()
function getDep(target, key) {
let map = deps.get(target)
if (!map) {
map = new Map()
deps.set(target, map)
}
let depends = map.get(key)
if (!depends) {
depends = new Dep()
map.set(key, depends)
}
return depends
}
// 对象 ---> 代理对象
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const dependency = getDep(target, key)
dependency.depend()
return Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
const dependency = getDep(target, key)
dependency.notify()
}
})
}
const user = reactive({
name: 'Klaus',
age: 23
})
function watchEffect(fn) {
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
watchEffect(() => console.log(`user的name发生了改变 - 1 --- ${user.name}`))
watchEffect(() => console.log(`user的name发生了改变 - 2 --- ${user.name}`))
watchEffect(() => console.log(`user的age发生了改变 ---- ${user.age} --- ${user.age}`))
console.log('-------------------------')
// user.name = 'Alex'
user.age = 18
而这就是 Vue3中 watchEffect
函数 和 reactive
函数的简单实现
vue2响应式 vs vue3响应式
vue最早发布于2014年,也就是ES6发布之前, 所以在vue2中,并不是使用proxy进行数据代理的,而是使用了Object.defineProrperty
function reactive(obj) {
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
set(newValue) {
value = newValue
const dependency = getDep(obj, key)
dependency.notify()
},
get() {
const dependency = getDep(obj, key)
dependency.depend()
return value
}
})
})
return obj
}
而Object.defineProrperty
只能对对象的属性的get和set进行拦截,并不能对对象的其它操作,例如添加新属性,删除属性进行监听
所以在vue2定义好data后,如果希望为data赋值新属性的时候,这个属性并不是响应式的,因为此时并没有监听其get和set方法
因此vue2提供了$set
API 来专门解决这个问题
另外,本质上,使用Object.defineProrperty
修改对象的属性的时候,修改的是原数据,这可能导致数据的不可控,不利于后期的维护和调试
因此自ES6提出Proxy后,vue3在重构vue代码的时候,使用proxy对数据进行代理,此时是对整个对象的所有操作都进行了拦截,而不是只能对get和set方法进行拦截操作
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const dependency = getDep(target, key)
dependency.depend()
return Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
const dependency = getDep(target, key)
dependency.notify()
}
})
}