title: Proxy+Reflect
categories: Javascript
tag:

  • 响应式原理
    date: 2021-11-29 21:10:34

监听对象的操作

我们先来看一个需求:有一个对象,我们希望监听这个对象中的属性被设置或获取的过程

  • 通过我们前面所学的知识,能不能做到这一点呢?
  • 其实是可以的,我们可以通过之前的属性描述符中的存储属性描述符来做到;

监听对象的操作

  • 这段代码就利用了前面讲过的 Object.defineProperty 的存储属性描述符来对属性的操作进行监听。
  1. const obj = {
  2. name: 'why',
  3. age: 18
  4. }
  5. Object.keys(obj).forEach((key) => {
  6. let value = obj[key]
  7. Object.defineProperty(obj, key, {
  8. set: function (newValue) {
  9. console.log(`监听到${key}被设置了`)
  10. value = newValue
  11. },
  12. get: function () {
  13. console.log(`监听到${key}被访问了`)
  14. return value
  15. }
  16. })
  17. })
  18. obj.name = 'dhh'
  19. console.log(obj.name)
  20. // Object.defineProperty(obj, 'name', {
  21. // set: function () {
  22. // console.log('监听到被设置了')
  23. // },
  24. // get: function () {
  25. // console.log('监听到被访问了')
  26. // }
  27. // })

但是这样做有什么缺点呢?

  1. 首先,Object.defineProperty 设计的初衷,不是为了去监听截止一个对象中所有的属性的。
  • 我们在定义某些属性的时候,初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。
  1. 其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么 Object.defineProperty 是无能为力的。
  2. 所以我们要知道,存储数据描述符设计的初衷并不是为了去监听一个完整的对象。

Proxy 的基本使用

Proxy 介绍

在 ES6 中,新增了一个Proxy 类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的:

  • 也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy 对象);
  • 之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作;

我们可以将上面的案例用 Proxy 来实现一次:

  • 首先,我们需要 new Proxy 对象,并且传入需要侦听的对象以及一个处理对象,可以称之为 handler;
    const p = new Proxy(target, handler)
  • 其次,我们之后的操作都是直接对 Proxy 的操作,而不是原有的对象,因为我们需要在 handler 里面进行侦听;

21_Proxy-Reflect - 图1

set 和 get 捕获器

如果我们想要侦听某些具体的操作,那么就可以在 handler 中添加对应的捕捉器(Trap):

  • set 和 get 分别对应的是函数类型;
    1. set 函数有四个参数:
    2. target:目标对象(侦听的对象);
    3. property:将被设置的属性 key;
    4. value:新属性值;
    5. receiver:调用的代理对象;
  • get 函数有三个参数:
    1. target:目标对象(侦听的对象);
    2. property:被获取的属性 key;
    3. receiver:调用的代理对象;
  1. const obj = {
  2. name: 'why',
  3. age: 18
  4. }
  5. const objProxy = new Proxy(obj, {
  6. //get获取值时的捕获器
  7. get: function (target, key) {
  8. console.log(`监听到对象的${key}属性被访问了`, target)
  9. return target[key]
  10. },
  11. //set设置值时的捕获器
  12. set: function (target, key, newValue) {
  13. console.log(`监听到对象的${key}属性被设置了`, target)
  14. target[key] = newValue
  15. }
  16. })
  17. console.log(objProxy.name)
  18. console.log(objProxy.age)
  19. objProxy.name = 'dh'
  20. console.log(objProxy.name)

Proxy 所有捕获器

监听判断是否存在或者删除操作。

  1. const obj = {
  2. name: 'why',
  3. age: 18
  4. }
  5. const objProxy = new Proxy(obj, {
  6. //get获取值时的捕获器
  7. get: function (target, key) {
  8. console.log(`监听到对象的${key}属性被访问了`, target)
  9. return target[key]
  10. },
  11. //set设置值时的捕获器
  12. set: function (target, key, newValue) {
  13. console.log(`监听到对象的${key}属性被设置了`, target)
  14. target[key] = newValue
  15. },
  16. //判断是否存在
  17. has: function (target, key) {
  18. console.log(`监听到对象的${key}属性的in操作了`, target)
  19. return key in target
  20. },
  21. // 删除操作
  22. deleteProperty: function (target, key) {
  23. console.log(`监听到对象的${key}属性的被删除操作了`, target)
  24. delete target[key]
  25. }
  26. })
  27. console.log(objProxy.name)
  28. console.log(objProxy.age)
  29. objProxy.name = 'dh'
  30. console.log(objProxy.name)
  31. //监听到对象的name属性的in操作了 { name: 'dh', age: 18 }
  32. console.log('name' in objProxy) //true
  33. // 监听到对象的name属性的被删除操作了 { name: 'dh', age: 18 }
  34. delete objProxy.name
  35. console.log(objProxy) //{ age: 18 }
  1. handler.getPrototypeOf()
    • Object.getPrototypeOf 方法的捕捉器。
  2. handler.setPrototypeOf()
    • Object.setPrototypeOf 方法的捕捉器。
  3. handler.isExtensible()
    • Object.isExtensible 方法的捕捉器。
  4. handler.preventExtensions()
    • Object.preventExtensions 方法的捕捉器。
  5. handler.getOwnPropertyDescriptor()
    • Object.getOwnPropertyDescriptor 方法的捕捉器。
  6. handler.defineProperty()
    • Object.defineProperty 方法的捕捉器。
  7. handler.ownKeys()
    • Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
  8. handler.has()
    • in 操作符的捕捉器。
  9. handler.get()
    • 属性读取操作的捕捉器。
  10. handler.set()
    • 属性设置操作的捕捉器。
  11. handler.deleteProperty()
    • delete 操作符的捕捉器。
  12. handler.apply()
    • 函数调用操作的捕捉器。
  13. handler.construct()
    • new 操作符的捕捉器。

Proxy 的 construct 和 apply

最后两个是 apply 和 constructor 适用于函数对象。

  1. function foo() {}
  2. const fooProxy = new Proxy(foo, {
  3. apply: function (target, thisArg, argArray) {
  4. console.log('对foo函数进行了apply调用')
  5. target.apply(thisArg, argArray)
  6. },
  7. construct: function (target, argArray, newTarget) {
  8. console.log('对foo函数进行了new调用')
  9. return new target(...argArray)
  10. }
  11. })
  12. fooProxy.apply({}, ['abc', 'cba'])
  13. new fooProxy('abc', 'cba')

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 对象上;
  • 那么 Object 和 Reflect 对象之间的 API 关系,可以参考 MDN 文档:链接

Reflect 的常见方法

21_Proxy-Reflect - 图2

Reflect 的使用

我们不希望直接去操作对象,所以使用 Reflect

  1. const obj = {
  2. name: 'why',
  3. age: 18
  4. }
  5. const objProxy = new Proxy(obj, {
  6. get: function (target, key, receiver) {
  7. console.log('get----------')
  8. return Reflect.get(target, key)
  9. },
  10. set: function (target, key, newValue, receiver) {
  11. console.log('set----------')
  12. const result = Reflect.set(target, key, newValue)
  13. if (result) {
  14. } else {
  15. }
  16. }
  17. })
  18. objProxy.name = 'dh'
  19. console.log(objProxy['name'])

Receiver 的作用

我们发现在使用 getter、setter 的时候有一个 receiver 的参数,它的作用是什么呢?

  • 如果我们的源对象(obj)有 setter、getter 的访问器属性,那么可以通过 receiver 来改变里面的 this;

我们来看这样的一个对象:此时这个 this 是 obj 原对象。

21_Proxy-Reflect - 图3

我们希望访问的是也就是这个 objProxy,当我们向 Reflect 传入 recevier。就会改变 this 指向。这样子就能起到拦截的作用

21_Proxy-Reflect - 图4

Reflect 的 construct

  1. function Student(name, age) {
  2. this.name = name
  3. this.age = age
  4. }
  5. function Teacher() {}
  6. //执行Student函数中的内容,但是创建出来对象是Teacher对象
  7. const teacher = Reflect.construct(Student, ['why', 18], Teacher)
  8. console.log(teacher) //Teacher { name: 'why', age: 18 }
  9. console.log(teacher.__proto__ == Teacher.prototype) //true

什么是响应式?

我们先来看一下响应式意味着什么?我们来看一段代码:

  • m 有一个初始化的值,有一段代码使用了这个值;
  • 那么在 m 有一个新的值时,这段代码可以自动重新执行;
  1. let m = 100
  2. console.log(m)
  3. console.log(m * 2)

响应式函数设计

首先,执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中:

  • 那么我们的问题就变成了,当数据发生变化时,自动去执行某一个函数;

首先我们实现一个简易的响应式

  • 这个时候我们封装一个新的函数 watchFn;
  • 凡是传入到 watchFn 的函数,就是需要响应式的;
  • 其他默认定义的函数都是不需要响应式的;
  1. let reactiveFns = []
  2. function watchFn(fn) {
  3. reactiveFns.push(fn)
  4. }
  5. const obj = {
  6. name: 'why',
  7. age: 18
  8. }
  9. watchFn(function () {
  10. const objName = obj.name
  11. console.log('你好啊,李银河')
  12. console.log('hello')
  13. console.log(obj.name)
  14. })
  15. watchFn(function () {
  16. console.log(obj.name)
  17. })
  18. reactiveFns.forEach((fn) => {
  19. fn()
  20. })

响应式依赖的收集。

目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:

  • 我们在实际开发中需要监听很多对象的响应式;
  • 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数;
  • 我们不可能在全局维护一大堆的数组来保存这些响应函数;

所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数:

  • 相当于替代了原来的简单 reactiveFns 的数组;
  1. class Depend {
  2. constructor() {
  3. this.reactiveFns = []
  4. }
  5. addDepend(reactiveFn) {
  6. this.reactiveFns.push(reactiveFn)
  7. }
  8. notify() {
  9. this.reactiveFns.forEach((fn) => {
  10. fn()
  11. })
  12. }
  13. }
  14. //封装一个响应式函数
  15. const depend = new Depend()
  16. function watchFn(fn) {
  17. depend.addDepend(fn)
  18. }
  19. const obj = {
  20. name: 'why',
  21. age: 18
  22. }
  23. watchFn(function () {
  24. const objName = obj.name
  25. console.log('你好啊,李银河')
  26. console.log('hello')
  27. console.log(obj.name)
  28. })
  29. watchFn(function () {
  30. console.log(obj.name)
  31. })
  32. obj.name = '123'
  33. depend.notify()

监听对象的变化

监听对象变化。然后实现自动响应

那么我们接下来就可以通过之前学习的方式来监听对象的变量:

  • 方式一:通过 Object.defineProperty 的方式(vue2 采用的方式);
  • 方式二:通过 new Proxy 的方式(vue3 采用的方式);

我们这里先以 Proxy 的方式来监听:

21_Proxy-Reflect - 图5

  1. class Depend {
  2. constructor() {
  3. this.reactiveFns = []
  4. }
  5. addDepend(reactiveFn) {
  6. this.reactiveFns.push(reactiveFn)
  7. }
  8. notify() {
  9. this.reactiveFns.forEach((fn) => {
  10. fn()
  11. })
  12. }
  13. }
  14. //封装一个响应式函数
  15. const depend = new Depend()
  16. function watchFn(fn) {
  17. depend.addDepend(fn)
  18. }
  19. const obj = {
  20. name: 'why',
  21. age: 18
  22. }
  23. const objProxy = new Proxy(obj, {
  24. get: function (target, key, receiver) {
  25. return Reflect.get(target, key, receiver)
  26. },
  27. set: function (target, key, newValue, receiver) {
  28. Reflect.set(target, key, newValue, receiver)
  29. depend.notify()
  30. }
  31. })
  32. watchFn(function () {
  33. const objName = objProxy.name
  34. console.log('你好啊,李银河')
  35. console.log('hello')
  36. console.log(objProxy.name)
  37. })
  38. watchFn(function () {
  39. console.log(objProxy.name + 'demo function~')
  40. })
  41. objProxy.name = '123'
  42. objProxy.name = '1'
  43. objProxy.name = 'dh'

对象的依赖管理

我们目前是创建了一个 Depend 对象,用来管理对于 name 变化需要监听的响应函数:

  • 但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;
  • 我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?

在前面我们刚刚学习过 WeakMap,并且在学习 WeakMap 的时候我讲到了后面通过 WeakMap 如何管理这种响应式的数据依赖:

21_Proxy-Reflect - 图6

对象依赖管理的实现

  1. //封装一个depend对象
  2. const targetMap = new WeakMap()
  3. function getDepend(target, key) {
  4. //根据target获取map的过程
  5. let map = targetMap.get(target)
  6. if (!map) {
  7. map = new Map()
  8. targetMap.set(target, map)
  9. }
  10. //根据key获取depend对象
  11. let depend = map.get(key)
  12. if (!depend) {
  13. depend = new Depend()
  14. map.set(key, depend)
  15. }
  16. return depend
  17. }
  1. const objProxy = new Proxy(obj, {
  2. get: function (target, key, receiver) {
  3. return Reflect.get(target, key, receiver)
  4. },
  5. set: function (target, key, newValue, receiver) {
  6. Reflect.set(target, key, newValue, receiver)
  7. const depend = getDepend(target, key)
  8. depend.notify()
  9. }
  10. })

正确的依赖收集

我们之前收集依赖的地方是在 watchFn 中:

  • 但是这种收集依赖的方式我们根本不知道是哪一个 key 的哪一个 depend 需要收集依赖;
  • 你只能针对一个单独的 depend 对象来添加你的依赖对象;
  • 那么正确的应该是在哪里收集呢?应该在我们调用了 Proxy 的 get 捕获器时;
  • 因为如果一个函数中使用了某个对象的 key,那么它应该被收集依赖;
  1. class Depend {
  2. constructor() {
  3. this.reactiveFns = []
  4. }
  5. addDepend(reactiveFn) {
  6. this.reactiveFns.push(reactiveFn)
  7. }
  8. notify() {
  9. this.reactiveFns.forEach((fn) => {
  10. fn()
  11. })
  12. }
  13. }
  14. //封装一个响应式函数
  15. let activeReactiveFn = null
  16. function watchFn(fn) {
  17. //1. 找到对应的属性
  18. activeReactiveFn = fn
  19. fn()
  20. activeReactiveFn = null
  21. }
  22. //封装一个depend对象
  23. const targetMap = new WeakMap()
  24. function getDepend(target, key) {
  25. //根据target获取map的过程
  26. let map = targetMap.get(target)
  27. if (!map) {
  28. map = new Map()
  29. targetMap.set(target, map)
  30. }
  31. //根据key获取depend对象
  32. let depend = map.get(key)
  33. if (!depend) {
  34. depend = new Depend()
  35. map.set(key, depend)
  36. }
  37. return depend
  38. }
  39. const obj = {
  40. name: 'why',
  41. age: 18
  42. }
  43. const objProxy = new Proxy(obj, {
  44. get: function (target, key, receiver) {
  45. const depend = getDepend(target, key)
  46. depend.addDepend(activeReactiveFn)
  47. return Reflect.get(target, key, receiver)
  48. },
  49. set: function (target, key, newValue, receiver) {
  50. Reflect.set(target, key, newValue, receiver)
  51. const depend = getDepend(target, key)
  52. depend.notify()
  53. }
  54. })
  55. watchFn(function () {
  56. console.log('name开始--------------')
  57. console.log(objProxy.name + 'demo function~')
  58. })
  59. watchFn(function () {
  60. console.log('age开始--------------')
  61. console.log(objProxy.age + 'demo function~')
  62. })
  63. watchFn(function () {
  64. console.log(objProxy.age + '新函数')
  65. })
  66. objProxy.age = 22

首先会先执行一遍

name 开始———————
whydemo function~
age 开始———————
18demo function~
18 新函数

当我们执行objProxy.age = 22,就会重新执行 age

age 开始———————
22demo function~
22 新函数

对 Depend 进行重构

  1. 问题一:如果函数中有用到两次 key,比如 name,那么这个函数会被收集两次;
  2. 问题二:我们并不希望将添加 reactiveFn 放到 get 中,以为它是属于 Dep 的行为;

所以我们需要对 Depend 类进行重构:

  1. 解决问题一的方法:不使用数组,而是使用 Set;
  2. 解决问题二的方法:添加一个新的方法,用于收集依赖;

我们想要 get 获取时,不需要关心是哪一个依赖。

我们并不希望将添加 reactiveFn 放到 get 中,以为它是属于 Dep 的行为;

21_Proxy-Reflect - 图7

那么。我们可以,把活跃的函数写在最前面。

21_Proxy-Reflect - 图8

然后在代理里面

21_Proxy-Reflect - 图9

还有个问题就是:如果我们在函数中用了两次,就会调用两次。如果函数中有用到两次 key,比如 name,那么这个函数会被收集两次;

21_Proxy-Reflect - 图10

其实我们,是不关心在一个函数中用了几次。

21_Proxy-Reflect - 图11

创建响应式对象

我们目前的响应式是针对于 obj 一个对象的,我们可以创建出来一个函数,针对所有的对象都可以变成响应式对象:

21_Proxy-Reflect - 图12

甚至可以这样子简写

21_Proxy-Reflect - 图13

以上就是 vue3 的响应式

vue2 响应式原理

我们前面所实现的响应式的代码,其实就是 Vue3 中的响应式原理:

  • Vue3 主要是通过 Proxy 来监听数据的变化以及收集相关的依赖的;
  • Vue2 中通过我们前面学习过的 Object.defineProerty 的方式来实现对象属性的监听;

我们可以将 reactive 函数进行如下的重构:

  • 在传入对象时,我们可以遍历所有的 key,并且通过属性存储描述符来监听属性的获取和修改;
  • 在 setter 和 getter 方法中的逻辑和前面的 Proxy 是一致的;

21_Proxy-Reflect - 图14

  1. //封装一个响应式函数
  2. let activeReactiveFn = null
  3. class Depend {
  4. constructor() {
  5. this.reactiveFns = new Set()
  6. }
  7. depend() {
  8. if (activeReactiveFn) {
  9. this.reactiveFns.add(activeReactiveFn)
  10. }
  11. }
  12. notify() {
  13. this.reactiveFns.forEach((fn) => {
  14. fn()
  15. })
  16. }
  17. }
  18. //封装一个响应式函数
  19. function watchFn(fn) {
  20. //1. 找到对应的属性
  21. activeReactiveFn = fn
  22. fn()
  23. activeReactiveFn = null
  24. }
  25. //封装一个depend对象
  26. const targetMap = new WeakMap()
  27. function getDepend(target, key) {
  28. //根据target获取map的过程
  29. let map = targetMap.get(target)
  30. if (!map) {
  31. map = new Map()
  32. targetMap.set(target, map)
  33. }
  34. //根据key获取depend对象
  35. let depend = map.get(key)
  36. if (!depend) {
  37. depend = new Depend()
  38. map.set(key, depend)
  39. }
  40. return depend
  41. }
  42. function reactive(obj) {
  43. Object.keys(obj).forEach((key) => {
  44. let value = obj[key]
  45. Object.defineProperty(obj, key, {
  46. get: function () {
  47. const depend = getDepend(obj, key)
  48. depend.depend()
  49. return value
  50. },
  51. set: function (newValue) {
  52. value = newValue
  53. const depend = getDepend(obj, key)
  54. depend.notify()
  55. }
  56. })
  57. })
  58. return obj
  59. }
  60. const obj = {
  61. name: 'why',
  62. age: 18
  63. }
  64. const objProxy = reactive(obj)
  65. const infoProxy = reactive({
  66. address: '北京'
  67. })
  68. watchFn(() => {
  69. console.log('开始')
  70. console.log(infoProxy.address + '-------')
  71. console.log(infoProxy.address + '+++++++')
  72. })
  73. infoProxy.address = 'dh'