方式二:通过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[map]放到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[map]放到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://bbs.csdn.net/topics/618166371)**
万事开头难,但是程序员这一条路坚持几年后发展空间还是非常大的,一切重在坚持。
为了帮助大家更好更高效的准备面试,特别整理了《前端工程师面试手册》电子稿文件。
![](https://img-blog.csdnimg.cn/img_convert/621960a57eb42479e02d6d64c0c81891.png)
![](https://img-blog.csdnimg.cn/img_convert/5230c48fd0fcb265f3401a21603bda2b.png)
**前端面试题汇总**
![](https://img-blog.csdnimg.cn/img_convert/42728594459506983a38ca2b86545fc6.png)
**JavaScript**
![](https://img-blog.csdnimg.cn/img_convert/7796de226b373d068d8f5bef31e668ce.png)
**性能**
![](https://img-blog.csdnimg.cn/img_convert/d7f6750332c78eb27cc606540cdce3b4.png)
**linux**
![](https://img-blog.csdnimg.cn/img_convert/ed368cc25284edda453a4c6cb49916ef.png)
**前端资料汇总**
![](https://img-blog.csdnimg.cn/img_convert/6e0ba223f65e063db5b1b4b6aa26129a.png)
前端工程师岗位缺口一直很大,符合岗位要求的人越来越少,所以学习前端的小伙伴要注意了,一定要把技能学到扎实,做有含金量的项目,这样在找工作的时候无论遇到什么情况,问题都不会大。