【JavaScript】vue中的响应式原理

发布时间:2024年01月19日

重学JavaScript08----- vue中的响应式原理


前言

请添加图片描述

在搞懂Vue2 - Vue3 响应式原理前,我们首先要认识JavaScript中的Object.definePropertyProxymapweakMapSetReflect这几个知识点。

如果已经明白这些知识点,我们可以直接导航跳到"响应式原理"。

对象访问器属性

我们希望监听对象中的属性被设置或获取的过程时可以使用它们。

vue2中的响应式原理利用了Object.defineProperty访问器属性,它设计的初衷,其实不是为了去监听截止一个对象中所有的属性的。

const obj = {
  name: "蜘蛛侠",
  age: 18
}

Object.keys(obj).forEach(key => {
  let value = obj[key]

  Object.defineProperty(obj, key, {
    get: function() {
      console.log(`监听到obj对象的${key}属性被访问了`)
      return value
    },
    set: function(newValue) {
      console.log(`监听到obj对象的${key}属性被设置值`)
      value = newValue
    }
  })
})

obj.name = "死侍"
obj.age = 30

console.log(obj.name)
console.log(obj.age)

obj.height = 1.88

如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么Object.defineProperty是无能为力的

Proxy代理

由于Object.defineProperty的缺陷,vue3使用了在ES6中,新增的一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的。

也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象Proxy对象);

之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作

const obj = {
  name: "蜘蛛侠",
  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 = "死侍"
objProxy.age = 30

console.log(obj.name)
console.log(obj.age)

响应式原理

响应式的原理主要分4步,封装响应式函数收集依赖对象的依赖管理监听对象的变化

1、封装响应式函数

凡是传入到watchFn的函数,就是需要响应式的

首先通过activeReactiveFn变量暂存并且收集到当前需要收集的响应式函数,它们最后会推入到Depend类类中的reactiveFns数组中(第二步的内容)

然后我们会立刻执行下这个响应式函数

// 暂存并且收集到当前需要收集的响应式函数,最后会推入到depend类中reactiveFns 
let activeReactiveFn = null
// 封装一个响应式的函数
function watchFn(fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}

2、响应式依赖的收集

我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数,这么说太复杂了我们实际的演练一遍。

只要构造了Depend类,那么我们收集的activeReactiveFn变量这时候就要派上用场了,我们通过判断,内存中activeReactiveFn存在了,就在变量叫reactiveFnsSet类型数组内推入这个响应式函数(set的不可重复特性)。

Depend类通过调用depend方法收集响应式函数并且推入到reactiveFns数组中。

再通过调用notify方法触发所有Depend类中收集到的函数。

// 响应式依赖的收集
class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }

  depend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

4、对象的依赖管理

实际开发中我们会有不同的对象需要绑定响应式,另外这些对象也会有不同的属性需要管理

我们用WeakMap对象和Map对象管理响应式的数据依赖

我们会通过WeakMap对象的特性弱引用提升性能,逐级的去获取到map对象存储的depend

// 封装一个获取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
}

5、监听对象的变化

通过 Object.defineProperty的方式(vue2采用的方式);

通过new Proxy的方式(vue3采用的方式);

主要讲一讲Vue3:

封装完reactive函数后

我们用reactive函数去绑定一个对象,当我们发生数据操作,比如get获取属性proxy代理会自动通过getDepend函数会生成一个depend类,然后调用depend.depend()收集所有响应式函数,并且把结果反射出去

再比如set修改属性时,代理会先反射修改的值,再自动通过getDepend函数会生成一个depend类,然后调用depend.notify()把收集到的响应式函数触发一次,以此来达到一个完整的响应式流程。


// vue2监听对象的变化
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
}

//vue3
function reactive(obj) {
  return new Proxy(obj, {
    get: function(target, key, receiver) {
      // 根据target.key获取对应的depend
      const depend = getDepend(target, key)
      // 给depend对象中添加响应函数
      // depend.addDepend(activeReactiveFn)
      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()
    }
  })
}

重点!!完整代码

!!!重点
以Vue3举例,源码是这样的

// 保存当前需要收集的响应式函数
let activeReactiveFn = null

// 封装depend类收集依赖
class Depend {
  constructor() {
    this.reactivefns = new Set()
  }
  depend() {
    if (activeReactiveFn) {
      this.reactivefns.add(activeReactiveFn)
    }
  }
  notify() {
    this.reactivefns.forEach(fn => { fn() })
  }
}

// 封装一个响应式的函数
const watchFn = (fn) => {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}


// 封装一个获取depend函数
let targetMap = new WeakMap()
const getDepend = (target, key) => {
  let map = targetMap.get(target)
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }
  let depend = map.get(key)
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}

// 封装代理数据
const reactive = (obj) => {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const depend = getDepend(target, key)
      depend.depend()
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver)
      const depend = getDepend(target, key)
      depend.notify()
      return true
    }
  })
}
//1、 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
  name: "死侍", // depend对象
  age: 18 // depend对象
})

const infoProxy = reactive({
  address: "广州市",
  height: 1.88
})
//2、 绑定响应式函数
watchFn(() => {
  console.log(infoProxy.address)
})
//3、 修改对象的内容
infoProxy.address = "北京市"

watchFn(() => {
  console.log(objProxy .name)
})

objProxy.name = "蜘蛛侠"

我们一步步剖析原理,现在讲一下我们绑定响应式后的过程

1、我们给对象绑定上响应式,监听对象的属性变量

const objProxy = reactive({
  name: "死侍", // depend对象
  age: 18 // depend对象
})

const infoProxy = reactive({
  address: "广州市",
  height: 1.88
})

2、绑定响应式函数

watchFn(() => {
  console.log(infoProxy.address)
})

我们在响应式函数内传入一个() => {console.log(infoProxy.address)}箭头函数

这时候因为infoProxy已经成功绑定了响应式
请添加图片描述

所以infoProxy.address就会触发proxy代理的get方法
在这里插入图片描述

getDepend方法会传入target{address: "广州市",height: 1.88}),keyaddress )两个参数

由于是第一次传入,它会通过mapweakmap重新生成一个depend类并返回
在这里插入图片描述

并且这个Depend类会自动收集() => {console.log(infoProxy.address)}箭头函数到类自己的reactivefns中,而后反射出来
在这里插入图片描述
Depend类的depend方法收集到了() => {console.log(infoProxy.address)}箭头函数然后存入reactivefns
在这里插入图片描述

反射出代理的对象,我们的控制台就会打印“广州市”
在这里插入图片描述

3、 修改对象的内容

infoProxy.address = "北京市"

我们修改infoProxy.address的内容时,会触发proxyset方法
在这里插入图片描述

它返回一个depend类,由于之前get方法触发时已经收集到了target{address: "广州市",height: 1.88}),keyaddress
所以我们这次set也会传入的target(自身),key(键)

这样就会无比准确的获取到get收集到的那个depend

这个类的reactivefns中保存着() => {console.log(infoProxy.address)}箭头函数

我们set方法中通过depend类中的方法notify再一次触发了这个() => {console.log(infoProxy.address)}箭头函数

控制台就会打印出我们修改属性后的内容"北京市",完成一个响应式闭环~

后续的代码,是同理的就不多解释了

watchFn(() => {
  console.log(objProxy .name)
})

objProxy.name = "蜘蛛侠"
文章来源:https://blog.csdn.net/m0_68324632/article/details/127848043
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。