Vue 3 响应式回顾

  • Proxy 对象实现属性监听
  • 多层属性嵌套,在访问过程中处理下一级属性
  • 默认监听动态添加的属性
  • 默认监听属性的删除操作
  • 默认监听数组索引和 length 属性
  • 可以作为单独的模块使用

    Proxy 和 Reflect

    Proxy 可以对目标对象的读取和修改等操作进行拦截,然后进行处理。创建 Proxy 对象需要两个参数,一个是目标对象,第二个是包含 get、set、deleteProperty 函数的拦截器对象,目标对象的 getter/setter,deleteProperty就设置成这三个函数。
    在 Vue 3 中,reactive 的实现就是通过 Proxy 配合 Reflect 实现。
    Reflect 也是 ES6 中新增的 API,可以用于获取目标对象的行为,它与 Object 类似,但是更易读,为操作对象提供了一种更优雅的方式。它的方法与 Proxy 是对应的,例如 get、set、deleteProperty。
    在严格模式下,Proxy 中传入的 set 和 deleteProperty 需要返回布尔类型的值,如果返回 false 或者不返回的话会抛出 Type Error 的异常。
  1. 'use strict'
  2. const target = {
  3. foo: 'xxx',
  4. bar: 'yyy'
  5. }
  6. // Reflect.getPrototypeOf()
  7. // Object.getPrototypeOf()
  8. const proxy = new Proxy(target, {
  9. get (target, key, receiver) {
  10. // return target[key]
  11. return Reflect.get(target, key, receiver)
  12. },
  13. set (target, key, value, receiver) {
  14. // target[key] = value
  15. return Reflect.set(target, key, value, receiver)
  16. },
  17. deleteProperty (target, key) {
  18. // delete target[key]
  19. return Reflect.deleteProperty(target, key)
  20. }
  21. })

Proxy 和 Reflect 中使用的 receiver 有区别
Proxy 中 receiver:Proxy 或者继承 Proxy 的对象
Reflect 中 receiver:如果 target 对象中设置了 getter,getter 中的 this 指向 receiver

  1. 'use strict'
  2. const obj = {
  3. get foo() {
  4. console.log(this)
  5. return this.bar
  6. }
  7. }
  8. const proxy = new Proxy(obj, {
  9. get (target, key, receiver) {
  10. if (key === 'bar') {
  11. return 'value - bar'
  12. }
  13. // return Reflect.get(target, key) // this 指向 obj
  14. return Reflect.get(target, key, receiver) // this 指向 receiver,在这里就是 proxy 对象
  15. }
  16. })
  17. console.log(proxy.foo) // value-bar

reactive 的手写实现

  • 接收一个参数,判断这参数是否对象。reactive 只能处理对象,这和 Vue 2 不同
  • 创建拦截器对象 handler,设置 get/set/deleteProperty
  • 返回 Proxy 对象 ```javascript const isObject = val => val !== null && typeof val === ‘object’ const convert = target => isObject(target) ? reactive(target) : target

export function reactive(target) { if (!isObject(target)) return

const handler = { get (target, key, receiver) { // 收集依赖 console.log(‘get’, key)

  1. const result = Reflect.get(target, key, receiver)
  2. return convert(result) // 判断当前属性是否对象,是还要递归处理
  3. },
  4. set (target, key, value, receiver) {
  5. const oldValue = Reflect.get(target, key, receiver)
  6. let result = true
  7. if (oldValue !== value) {
  8. result = Reflect.set(target, key, value, receiver)
  9. // 触发更新
  10. console.log('set', key, value)
  11. }
  12. return result
  13. },
  14. deleteProperty (target, key) {
  15. let result = true
  16. if (Reflect.has(target, key)) {
  17. result = Reflect.deleteProperty(target, key)
  18. console.log('delete', key);
  19. }
  20. return result
  21. }

}

return new Proxy(target, handler) }

  1. demo
  2. ```html
  3. <!DOCTYPE html>
  4. <html lang="en">
  5. <head>
  6. <meta charset="UTF-8">
  7. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  8. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  9. <title>Document</title>
  10. </head>
  11. <body>
  12. <script type="module">
  13. import { reactive } from './reactivity/index.js'
  14. const obj = reactive({
  15. name: '张三',
  16. age: 18
  17. })
  18. obj.name = 'lisa'
  19. delete obj.age
  20. console.log(obj)
  21. </script>
  22. </body>
  23. </html>

收集依赖

当访问响应式对象属性时,getter 里要收集依赖。修改对象属性时,set 和 deleteProperty 里要触发更新。

  1. 新建一个 WeakMap,在里面存储键值对,键是被访问的响应式目标对象,值是一个 Map 对象。
  2. Map 也是一个存储键值对的对象,在这里的作用是存储依赖于目标对象的属性的集合。所以 Map 对象的键是属性名,值是一个 Set 集合,里面存储了对属性进行操作的函数。
  3. get 中收集依赖的过程大概如下:
    1. 判断调用 get 的函数是否为空,如果为空直接返回
    2. 从 WeakMap 中获取以当前对象为键的键值对的值,也就是 Map,如果没有,调用 WeakMap 的 set 方法创建出来。
    3. 从 Map 中获取以当前属性的名称为键的键值对的值,也就是 Set,如果没有,调用 Map 的 set 方法创建出来。
    4. 在 Set 中添加函数。
  4. 触发更新的过程大概如下:
    1. 从 WeakMap 中获取以当前对象为键的键值对的值,也就是 Map,如果没有,直接返回
    2. 从 Map 中获取以当前属性的名称为键的键值对的值,也就是 Set,如果有,从里面获取每一个函数并执行。

image.png

  1. const isObject = val => val !== null && typeof val === 'object'
  2. const convert = target => isObject(target) ? reactive(target) : target
  3. export function reactive(target) {
  4. if (!isObject(target)) return
  5. const handler = {
  6. get (target, key, receiver) {
  7. // 收集依赖
  8. track(target, key)
  9. const result = Reflect.get(target, key, receiver)
  10. return convert(result) // 判断当前属性是否对象,是还要递归处理
  11. },
  12. set (target, key, value, receiver) {
  13. const oldValue = Reflect.get(target, key, receiver)
  14. let result = true
  15. if (oldValue !== value) {
  16. result = Reflect.set(target, key, value, receiver)
  17. // 触发更新
  18. trigger(target, key)
  19. }
  20. return result
  21. },
  22. deleteProperty (target, key) {
  23. let result = true
  24. if (Reflect.has(target, key)) {
  25. result = Reflect.deleteProperty(target, key)
  26. result && trigger(target, key)
  27. }
  28. return result
  29. }
  30. }
  31. return new Proxy(target, handler)
  32. }
  33. let activeEffect = null
  34. export function effect (callback) {
  35. activeEffect = callback
  36. callback() // 访问响应式对象属性,去收集依赖
  37. activeEffect = null
  38. }
  39. let targetMap = new WeakMap()
  40. /**
  41. * 对象的属性收集依赖
  42. * @param {object} target 跟踪对象
  43. * @param {string} key 属性名
  44. */
  45. export function track (target, key) {
  46. if (!activeEffect) return
  47. let depsMap = targetMap.get(target)
  48. if (!depsMap) {
  49. targetMap.set(target, (depsMap = new Map()))
  50. }
  51. let dep = depsMap.get(key)
  52. if (!dep) {
  53. depsMap.set(key, (dep = new Set()))
  54. }
  55. dep.add(activeEffect)
  56. }
  57. /**
  58. * 触发更新
  59. * @param {*} target
  60. * @param {*} key
  61. */
  62. export function trigger (target, key) {
  63. const depsMap = targetMap.get(target)
  64. if (!depsMap) return
  65. const dep = depsMap.get(key)
  66. if (!dep) return
  67. dep.forEach(effect => {
  68. effect()
  69. })
  70. }

demo:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Document</title>
  7. </head>
  8. <body>
  9. <script type="module">
  10. import { reactive, effect } from './reactivity/index.js'
  11. const product = reactive({
  12. name: 'iPhone',
  13. price: 5000,
  14. count: 3
  15. })
  16. let total = 0
  17. effect(() => {
  18. total = product.price * product.count
  19. })
  20. console.log(total)
  21. product.price = 4000
  22. console.log(total)
  23. product.count = 1
  24. console.log(total)
  25. </script>
  26. </body>
  27. </html>

ref 的手写实现

ref 把基本数据类型转换成响应式的,如果传入的是对象,调用 reactive。

  1. export function ref (raw) {
  2. // 判断 raw 是否是 ref 的对象,如果是的话,直接返回
  3. if (isObject(raw) && raw.__v_isRef) return raw
  4. let value = convert(raw)
  5. const r = {
  6. __v_isRef: true,
  7. get value () {
  8. track(r, 'value')
  9. return value
  10. },
  11. set value (newValue) {
  12. if (newValue !== value) {
  13. raw = newValue
  14. value = convert(raw)
  15. trigger(r, 'value')
  16. }
  17. }
  18. }
  19. return r
  20. }

ref vs reactive

  • ref 可以把基本数据类型数据,转成响应式对象
  • ref 返回的对象,重新赋值成对象也是响应式的
  • reactive 返回的对象,重新赋值丢失响应式
  • reactive 返回的对象不可以解构

toRefs

toRefs 接收一个 reactive 返回的 Proxy 对象,如果不是 Proxy 对象,直接返回。
toRefs 会把基本数据类型的属性,处理成像 ref 那种格式,把转换后的属性挂到一个新的对象,返回。

  1. export function toRefs (proxy) {
  2. const ret = proxy instanceof Array ? new Array[proxy.length] : {}
  3. for (const key in proxy) {
  4. ret[key] = toProxyRef(proxy, key)
  5. }
  6. return ret
  7. }
  8. function toProxyRef (proxy, key) {
  9. const r = {
  10. __v_isRef: true,
  11. get value () {
  12. return proxy[key]
  13. },
  14. set value (newValue) {
  15. proxy[key] = newValue
  16. }
  17. }
  18. return r
  19. }

computed

computed 接收一个 getter 函数,需要在里面收集依赖。

  1. export function computed (getter) {
  2. const result = ref()
  3. effect(() => (result.value = getter()))
  4. return result
  5. }