前言
我们在编写Vue2,Vue3代码的时候,经常会在data中定义某些数据,然后在template用到的时候,可能会在多处用到这些数据,通过对这些数据的操作,可以达到改变视图的作用,即所谓数据驱动视图。
我们可以通过Mustache 语法,让data可以在页面上显示,随着data的变化,视图中也会随之改变。
那么,这种响应式操作在Vue2、Vue3中是怎么实现的呢?
Vue2响应式操作
响应式函数的封装
在进行响应式操作前,我们需要简单大致封装一个响应式函数,参数接收的是函数,凡是传入到响应式函数的函数,就是需要响应式的,其他默认定义的函数是不需要响应式的。
我们需要用一个数组将他们收集起来,(现在暂时使用函数,最好的办法是放入Set中,下文会讲),代码如下:
// 封装一个响应式的函数
let reactiveFns = []
function watchFn(fn) {
reactiveFns.push(fn)
}
等到我们需要执行这些函数的时候(什么时候需要执行是后话,先简单提一下),可以遍历这个数组然后执行:
reactiveFns.forEach(fn => {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->
fn()
})
Depend类的封装
我们需要封装一个Depend类,这个类的作用是:这个类用于管理某一个对象的某一个属性的所有响应式函数。一个对象里面可能会有多个属性并且有他们对应的值,我们可能用到了这个对象里面多个属性,所以我们要给这里的每个用到的属性建立一个属于自己的类,用来管理对这个属性有依赖的所有函数。
所以我们得想办法拿到刚才在响应式函数里面传进去的函数,这里我们可以用activeReactiveFn暂时保存刚才传进去的函数。
所以我们对响应式函数的封装进行重构一下,如下:
// 保存当前需要收集的响应式函数
let activeReactiveFn = null
// 封装一个响应式的函数
function watchFn(fn) {
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
因为某个属性可能会用多个函数进行依赖,所有在这个类的内部我们会定义一个Set, reactiveFns = new Set(),定义成Set而不是数组是因为Set数据结构没有重复的数据,从而防止了重复的操作。
这里定义了一个depend方法可以将activeReactiveFn在有值的情况下,放入reactiveFns中,notify函数就是将这些收集了的函数进行执行。
class Depend {
constructor() {
this.reactiveFns = new Set()
}
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn)
}
}
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
监听对象的变化
在Vue2中使用的监听对象的变化使用的方法是:使用Object.defineProperty。
我们可以封装一个reactive函数,参数传入一个对象,函数内部对这个对象进行监听,遍历这个对象,获取所有的属性和属性值,对每个属性使用Object.defineProperty,在Object.defineProperty第三个参数中,get和set方法中,在set方法中,修改值为新的值之后,之前提到,每个属性都要有属于自己的Depend对象,那么如何获取这个对象呢?
那这里还有个问题,有不同的对象,对象里面又有多个属性,那么这该如何解决呢?
可以定义一个WeakMap将各个对象保存成Map形式,然后在每个单一对象里面,我们可以用Map形式保存属性的Depend类,如图所示:
那么如何根据对象名,属性名获取depend呢?可以在getDepend函数实现,参数传入对象名,属性名
,代码如下:
// 封装一个获取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)
}
// 根据key获取depend对象
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}
获取到属性特定的depend后,回到原来的话题,那么在set方法中,修改值为新的值之后,获取到属性特定的depend后,要调用depend里面的notify方法,使对这个属性有依赖的所有函数执行,也就是对数据进行更新。
在get方法中,在返回属性值之前,要先获取到属性特定的depend后,调用depend里面的depend方法,将对此属性依赖的函数保存下来。
代码如下:
function reactive(obj) {
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get: function() {
const depend = getDepend(obj, key)
depend.depend()
return value
},
set: function(newValue) {
value = newValue
const depend = getDepend(obj, key)
depend.notify()
}
})
})
return obj
}
至此,Vue2的响应式操作就已经实现了
所有代码以及测试代码如下:
// 保存当前需要收集的响应式函数
let activeReactiveFn = null
class Depend {
constructor() {
this.reactiveFns = new Set()
}
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn)
}
}
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
// 封装一个响应式的函数
function watchFn(fn) {
activeReactiveFn = fn
fn()
activeReactiveFn = 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)
}
// 根据key获取depend对象
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}
function reactive(obj) {
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get: function() {
const depend = getDepend(obj, key)
depend.depend()
return value
},
set: function(newValue) {
value = newValue
const depend = getDepend(obj, key)
depend.notify()
}
})
})
return obj
}
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
name: "cy", // depend对象
age: 18 // depend对象
})
const infoProxy = reactive({
address: "安徽省",
height: 1.88
})
watchFn(() => {
console.log(infoProxy.address)
})
infoProxy.address = "北京市"
const foo = reactive({
name: "foo"
})
watchFn(() => {
console.log(foo.name)
})
foo.name = "aaa"
foo.name = "bbb"
// 安徽省
// 北京市
// foo
// aaa
// bbb
Vue3响应式操作
Proxy、Reflect
Proxy:
在Vue2中,使用Object.defineProperty来监听对象的变化,但是这样做有什么缺点呢?
首先,Object.defineProperty设计的初衷,不是为了去监听截止一个对象中所有的属性的。
我们在定义某些属性的时候,初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。
其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么Object.defineProperty是无能为力的。
所以我们要知道,存储数据描述符设计的初衷并不是为了去监听一个完整的对象
在ES6中,新增了一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的:
也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象);
之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作;
如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap):
set函数有四个参数:
target:目标对象(侦听的对象);
property:将被设置的属性key;
value:新属性值;
receiver:调用的代理对象;
get函数有三个参数:
target:目标对象(侦听的对象);
property:被获取的属性key;
receiver:调用的代理对象
实例代码如下;
const obj = {
name: "cy",
age: 18
}
const objProxy = new Proxy(obj, {
// 获取值时的捕获器
get: function(target, key) {
console.log(`监听到对象的${key}属性被访问了`, target)
return target[key]
},
// 设置值时的捕获器
set: function(target, key, newValue) {
console.log(`监听到对象的${key}属性被设置值`, target)
target[key] = newValue
}
})
console.log(objProxy.name)
console.log(objProxy.age)
objProxy.name = "kobe"
objProxy.age = 30
console.log(obj.name)
console.log(obj.age)
// 监听到对象的name属性被访问了 { name: 'cy', age: 18 }
// cy
// 监听到对象的age属性被访问了 { name: 'cy', age: 18 }
// 18
// 监听到对象的name属性被设置值 { name: 'cy', age: 18 }
// 监听到对象的age属性被设置值 { name: 'kobe', age: 18 }
// kobe
// 30
Reflect:
Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射。
那么这个Reflect有什么用呢?
它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法;
比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf();
比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty() ;
如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?
这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面;
但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;
另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;
所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上;
Reflect中常见的方法:
那么我们可以将之前Proxy案例中对原对象的操作,都修改为Reflect来操作;
我们发现在使用getter、setter的时候有一个receiver的参数,它的作用是什么呢?
如果我们的源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this
Vue3响应式
Vue3响应式使用的是Proxy,我们需要在Vue2的reactive函数里面进行一些改变:
function reactive(obj) {
return new Proxy(obj, {
get: function(target, key, receiver) {
// 根据target.key获取对应的depend
const depend = getDepend(target, key)
// 给depend对象中添加响应函数
depend.depend()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// depend.notify()
const depend = getDepend(target, key)
depend.notify()
}
})
}
其他方面的代码同Vue2基本没啥变化,Vue3的响应式操作就已经实现了
所有代码以及测试代码如下:
// 保存当前需要收集的响应式函数
let activeReactiveFn = null
class Depend {
constructor() {
this.reactiveFns = new Set()
}
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn)
}
}
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
// 封装一个响应式的函数
function watchFn(fn) {
activeReactiveFn = fn
fn()
activeReactiveFn = 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)
}
// 根据key获取depend对象
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}
function reactive(obj) {
return new Proxy(obj, {
get: function(target, key, receiver) {
// 根据target.key获取对应的depend
const depend = getDepend(target, key)
// 给depend对象中添加响应函数
depend.depend()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// depend.notify()
const depend = getDepend(target, key)
depend.notify()
}
})
}
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
name: "cy", // depend对象
age: 18 // depend对象
})
const infoProxy = reactive({
address: "安徽省",
height: 1.88
})
watchFn(() => {
console.log(infoProxy.address)
})
infoProxy.address = "北京市"
const foo = reactive({
name: "foo"
})
watchFn(() => {
console.log(foo.name)
})
foo.name = "bar"
// 安徽省
// 北京市
// foo
// bar
以上就是一篇搞懂Vue2、Vue3响应式源码的原理的详细内容,更多关于Vue2、Vue3响应式源码的原理的资料请关注编程网其它相关文章!