响应式原理
🍤认识响应式逻辑
我们先来看一下响应式意味着什么?我们来看一段代码:
num有一个初始化的值,有一段代码使用了这个值;
那么在num有一个新的值时,我们希望这段代码可以自动重新执行;
let num = 50
console.log(num + 50)
console.log(num * num)
num = 150
上面的这样一种可以自动响应数据变量的代码机制,我们就称之为是响应式的。
那么我们再来看一下对象的响应式
// 对象的响应式
const obj = {
name: "chenyq",
age: 18
}
// 当name属性变化时, 需要重新执行下面这段代码
console.log(obj.name)
console.log(obj.age)
// 当age变化时才需要执行这段代码, name变化不需要重新执行
console.log(obj.age + 100)
// 修改obj的name属性
obj.name = "kaisa"
首先,执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中:
那么我们的问题就变成了,当数据发生变化时,自动去执行某一个函数;
// 对象的响应式
const obj = {
name: "chenyq",
age: 18
}
// 将两段代码分别封装成两个函数, 当数据变化需要重新执行时, 只需重新调用函数
function foo() {
console.log(obj.name)
console.log(obj.age)
}
function bar() {
console.log(obj.age + 100)
}
// 修改obj的name属性
obj.name = "kaisa"
但是有一个问题:在开发中我们是有很多的函数的,我们如何区分一个函数需要响应式,还是不需要响应式呢?
很明显,下面的函数中 foo 需要在obj的name发生变化时,重新执行,做出相应;
bar函数是一个完全独立于obj的函数,它不需要执行任何响应式的操作;
function foo() {
let newName = obj.name
console.log(obj.name)
}
function bar() {
const result = 20 + 30
console.log(result)
}
🍤响应式依赖收集
我们如何区分一个函数是否需要响应式呢?
这个时候我们封装一个新的函数watchFn用来收集name属性变化时, 需要响应式的函数;
凡是传入到watchFn的函数,就是需要响应式的, 再由watchFn函数将他们存放在一个数组中;
其他默认定义的函数都是不需要响应式的;
const obj = {
name: "chenyq",
age: 18
}
// 将依赖对象的函数, 统一收集到一个数组中
const reactiveFns = []
// 设计一个专门收集响应式函数的函数
function watchFn(fn) {
reactiveFns.push(fn)
// 函数传进来时, 会自动执行一次
fn()
}
// 调用函数, 将函数收集到数组中
watchFn(function foo() {
console.log("foo:", obj.name)
console.log("foo:", obj.age)
})
watchFn(function bar() {
console.log("bar:", "hello " + obj.name)
console.log("bar:", obj.age + 10)
})
// 修改obj属性
obj.name = "kaisa"
// 修改obj属性, 将收集响应式函数的数组遍历并且全部调用
reactiveFns.forEach( fn => fn())
🍤响应式依赖管理
目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:
我们在实际开发中需要监听很多对象的响应式;
这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数;
我们不可能在全局维护一大堆的数组来保存这些响应函数;
所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数:
相当于替代了原来的简单 reactiveFns 的数组;
- 定义的类如下:
// 使用一个类代替数组来进行依赖管理
class Depend {
constructor() {
// 定义存放响应式函数的数组
this.reactiveFns = []
}
// 定义实例方法, 用于收集需要响应式的函数
addDepend(fn) {
if (fn) {
this.reactiveFns.push(fn)
}
}
// 定义方法, 用于数据改变时, 执行数组中的响应式的函数
notify() {
this.reactiveFns.forEach(fn => fn())
}
}
- 使用类管理依赖
const obj = {
name: "chenyq",
age: 18
}
// 创建一个实例
const dep = new Depend()
function watchFn(fn) {
// 使用实例方法, 将响应式的函数添加到类中
dep.addDepend(fn)
// 函数传进来时, 会自动执行一次
fn()
}
// 调用函数, 将函数收集到数类中
watchFn(function foo() {
console.log("foo:", obj.name)
console.log("foo:", obj.age)
})
watchFn(function bar() {
console.log("bar:", "hello " + obj.name)
console.log("bar:", obj.age + 10)
})
// 修改obj属性
obj.name = "kaisa"
// 当name数据改变时, 仅需调用类中的notify方法即可
dep.notify()
🍤监听属性的变化
目前存在的一个问题就是: 我们当某个属性发生变化, 我们想要它响应式, 就需要手动的调用notify方法, 如果没有调用, 那么数据就不会更新.
这样操作是非常繁琐的, 我们希望实现自动监听刷新数据的效果
那么我们接下来就可以通过之前学习的方式来监听对象的变量:
方式一:通过 Object.defineProperty的方式(vue2采用的方式);
方式二:通过new Proxy的方式(vue3采用的方式);
我们这里先以Object.defineProperty的方式来监听:
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
set: function(newValue) {
value = newValue
dep.notify()
},
get: function() {
return value
}
})
})
监听后就不需要再手动调用, 当属性发生变化时, 会自动调用notify方法, 实现数据更新
// 修改obj属性
console.log("------------name属性发生改变------------")
obj.name = "kaisa"
console.log("------------age属性发生改变------------")
obj.age = 38
🍤自动的收集依赖(核心难点)
目前我们是通过watchFn不管三七二十一的, 将函数添加到类中, 当属性改变时重新执行添加到类中的函数
如果向类中添加两个函数, 但是如果其中一个依赖name, 另一个没有依赖name属性, 这样的添加方法是有问题的, 没有依赖name属性的函数, 我们应该不去向类中添加
我们目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数:
但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;
我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?
在前面ES6新特性中我讲解过WeakMap,并且在讲WeakMap的时候我讲到了后面通过WeakMap如何管理这种响应式的数据依赖(我们会按照如下数据结构对响应式输入依赖进行管理):
dep对象数据结构的管理(最难理解)
每一个对象的每一个属性都会对应一个dep对象
同一个对象的多个属性的dep对象是存放一个map对象中
多个对象分别对应的map对象, 又会被存放到一个objMap的对象中
依赖收集: 当执行get函数, 自动的添加fn函数; 当执行set函数, 自动执行Depend对象的notify方法
我们可以写一个getDepend函数专门来管理这种依赖关系:
这样我们调用getDepend函数, 一定会返回一个depend对象, 并且在get和set方法中, 可以根据obj和key拿到正确的depend对象
// 封装一个函数: 负责通过obj的key获取对应的Depend对象
const objMap = new WeakMap()
function getDepend(obj, key) {
// 1.根据obj对象, 找到obj对应的map对象
let map = objMap.get(obj)
// 当map对象不存在时, 创建一个map对象, 再将obj放到objMap中
if (!map) {
map = new Map()
objMap.set(obj, map)
}
// 2.根据key, 找到map对应的depend对象
let dep = map.get(key)
// dep没有值时, 创建一个depend对象, 存入对应的map对象中
if (!dep) {
dep = new Depend()
map.set(key, dep)
}
return dep
}
接下来我们就需要正确的将依赖收集起来, 我们之前收集依赖的地方是在 watchFn 中:
但是之前这种收集依赖的方式我们根本不知道是哪一个key的哪一个depend需要收集依赖, 只能针对一个单独的depend对象来添加你的依赖对象;
那么正确的应该是在哪里收集呢?应该在我们调用了get捕获器时, 因为如果一个函数中使用了某个对象的key,那么它应该被收集依赖, 而又当一个函数中使用了某个对象的key, 那么就会执行该对象的get方法, 我们可以在get捕获器中, 将正确的依赖收集进来;
// 封装函数: 用于收集依赖
// 定义一个变量, 临时保存传入的fn函数, 方便在get中添加到depend对象
let reactiveFn = null
function watchFn(fn) {
reactiveFn = fn
// 函数传进来时,会 自动执行一次
fn()
reactiveFn = null
}
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get: function() {
// 获取正确的dep, 将函数添加进去
const dep = getDepend(obj, key)
dep.addDepend(reactiveFn)
return value
}
})
})
当属性改变时, 会执行set方法, 我们在set方法中可以拿到当前dep对象, 并执行当前dep对象的notify方法
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
set: function(newValue) {
value = newValue
// 获取obj.key对应的depend对象
const dep = getDepend(obj, key)
dep.notify()
},
get: function() {
// 找到正确的dep, 将函数添加进去
const dep = getDepend(obj, key)
dep.addDepend(reactiveFn)
return value
}
})
})
按照上诉步骤, 就完成了自动收集依赖的实现, 完整代码如下
// 使用一个类代替数组来进行依赖管理
class Depend {
constructor() {
// 定义存放响应式函数的数组
this.reactiveFns = []
}
// 定义实例方法, 用于收集需要响应式的函数
addDepend(fn) {
if (fn) {
this.reactiveFns.push(fn)
}
}
// 定义方法, 用于数据改变时, 执行数组中的响应式的函数
notify() {
this.reactiveFns.forEach(fn => fn())
}
}
const obj = {
name: "chenyq",
age: 18
}
// 封装函数: 用于收集依赖
// 定义一个变量, 临时保存传入的fn函数, 方便在get中添加到depend对象
let reactiveFn = null
function watchFn(fn) {
reactiveFn = fn
// 函数传进来时,会 自动执行一次
fn()
reactiveFn = null
}
// 封装一个函数: 负责通过obj的key获取对应的Depend对象
const objMap = new WeakMap()
function getDepend(obj, key) {
// 1.根据obj对象, 找到obj对应的map对象
let map = objMap.get(obj)
// 当map对象不存在时, 创建一个map对象, 再将obj放到objMap中
if (!map) {
map = new Map()
objMap.set(obj, map)
}
// 2.根据key, 找到map对应的depend对象
let dep = map.get(key)
// dep没有值时, 创建一个depend对象, 存入对应的map对象中
if (!dep) {
dep = new Depend()
map.set(key, dep)
}
return dep
}
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
set: function(newValue) {
value = newValue
// 获取obj.key对应的depend对象
const dep = getDepend(obj, key)
dep.notify()
},
get: function() {
// 获取正确的dep, 将函数添加进去
const dep = getDepend(obj, key)
dep.addDepend(reactiveFn)
return value
}
})
})
// 调用函数, 将函数收集到数类中
watchFn(function foo() {
console.log("foo:", obj.name)
console.log("foo:", obj.age)
})
watchFn(function bar() {
console.log("bar:", obj.age + 10)
})
// 测试:
// 修改obj属性
console.log("------------name属性发生改变------------")
obj.name = "kaisa"
console.log("------------age属性发生改变------------")
obj.age = 38
🍤对Depend重构
自动收集依赖已经实现, 但是这里有两个小问题:
问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次;
问题二:我们并不希望将添加reactiveFn放到get中,以为它是属于Dep的行为;
所以我们需要对Depend类进行重构:
解决问题一的方法:不使用数组,而是使用Set;
如下代码, 例如当函数中有使用两次或多次name属性时, name属性也会被多次添加到dep.reactiveFns数组中, 导致函数被多次执行
// 调用函数, 将函数收集到数类中
watchFn(function foo() {
console.log("foo:", obj.name)
console.log("foo:", obj.name)
console.log("foo:", obj.name)
console.log("foo:", obj.age)
})
watchFn(function bar() {
console.log("bar:", obj.age + 10)
})
// 测试:
// 修改obj属性
console.log("------------name属性发生改变------------")
obj.name = "kaisa"
解决方式: 使用Set结构
class Depend {
constructor() {
// 定义存放响应式函数换成Set
this.reactiveFns = new Set()
}
addDepend(fn) {
if (fn) {
this.reactiveFns.add(fn)
}
}
notify() {
this.reactiveFns.forEach(fn => fn())
}
}
解决问题二的方法:添加一个新的方法,用于收集依赖;
class Depend {
constructor() {
this.reactiveFns = new Set()
}
addDepend(fn) {
if (fn) {
this.reactiveFns.add(fn)
}
}
// 添加一个新的方法,用于收集依赖
depend() {
if (reactiveFn) {
this.reactiveFns.add(reactiveFn)
}
}
notify() {
this.reactiveFns.forEach(fn => fn())
}
}
get: function() {
const dep = getDepend(obj, key)
// 调用depend方法
dep.depend()
return value
}
🍤创建响应式对象
我们目前的响应式是针对于obj一个对象的,我们可以创建出来一个函数,针对所有的对象都可以变成响应式对象:
function reactive(obj) {
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
set: function(newValue) {
value = newValue
// 获取obj.key对应的depend对象
const dep = getDepend(obj, key)
dep.notify()
},
get: function() {
// 获取正确的dep, 将函数添加进去
const dep = getDepend(obj, key)
// dep.addDepend(reactiveFn)
dep.depend()
return value
}
})
})
return obj
}
当我们想要响应式时, 包裹一层reactive即可
const obj = reactive({
name: "chenyq",
age: 18,
address: "成都市"
})
watchFn(function foo() {
console.log("foo:", obj.name)
console.log("foo:", obj.age)
})
const user = reactive({
userName: 'aaabbbccc',
passWorld: "abc123"
})
watchFn(function bar() {
console.log("bar:", user.userName)
console.log("bar:", user.passWorld)
})
// 测试:
obj.name = "kaisa"
user.passWorld = "123456"
🍤Vue3响应式原理
我们前面所实现的响应式的代码,其实就是Vue2中的响应式原理:
Vue3主要是通过Proxy来监听数据的变化以及收集相关 的依赖的;
Vue2中通过Object.defineProerty 的方式来实现对象属性的监听;
Vue2和Vue3的原理思路是一样的, 我们只需要将reactive函数中的Object.defineProerty重构成Proxy来监听数据, 就是Vue3的响应式原理
function reactive(obj) {
const objProxy = new Proxy(obj, {
set: function(target, key, newValue) {
// 设置新值
Reflect.set(target, key, newValue)
const dep = getDepend(target, key)
dep.notify()
},
get: function(target, key) {
const dep = getDepend(target, key)
dep.depend()
return Reflect.get(target, key)
}
})
return objProxy
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/120066.html