Vue 3 学习 三、Vue.js 3.0 响应式系统原理及模拟

Vue.js 3.0 响应式回顾

与Vue.js 2.x 的区别

  • Proxy 对象实现属性监听
    • Vue.js 2.x 需要遍历所有属性,转化成响应式
  • 多层属性嵌套,在访问属性过程中处理下一级属性
    • Vue 2.x 需要在初始化的时候转化
  • 默认监听动态添加的属性
    • Vue 2.x 需要调用 Vue.set()
  • 默认监听属性的删除操作
    • Vue 2.x 不能
  • 默认监听数组索引和 length 属性
    • Vue 2.x 不能
  • 可以作为单独的模块使用
    • Vue 2.x 没有将响应式系统单独提取为一个模块

核心方法

稍后将模拟 Vue 3 的核心方法来了解 Vue 3 中响应式系统的原理。

  • reactive/ref/toRefs/computed
  • effect
    • watch/watchEffect 是 Vue 3 的 runtime-core 中实现的
    • 使用了一个 effect 的底层函数
  • track/trigger
    • Vue 3 中收集依赖和触发更新的函数

Proxy 对象回顾

'use strict'

// 创建目标对象 - 将被代理的对象
const target = {
  foo: 'xxx',
  bar: 'yyy'
}

// 通过Proxy代理target对象
// 创建Proxy对象的时候,传入了第二个对象参数(handler),这个对象可以称为处理器或监听器
const proxy = new Proxy(target, {
  // 监听属性的访问
  get(target, key, receiver) {
    // return target[key]
    return Reflect.get(target, key, receiver)
  },
  // 监听属性的赋值
  set(target, key, value, receiver) {
    // return target[key] = value
    return Reflect.set(target, key, value, receiver)
  },
  // 监听属性的删除
  deleteProperty(target, key) {
    // return delete target[key]
    return Reflect.deleteProperty(target, key)
  }
})
  • Reflect
    • 翻译是反射的意思,是 ES6 新增的成员,Java 中也有 Reflect。
    • Reflect 是在代码运行期间用来获取或设置对象中的成员。从 Java 中借鉴而来。
    • 因为 JavaScript 在运行过程中可以随意向对象增加成员或者获取成员信息,所以过去的时候 JavaScript 中并没有 Reflect
    • 过去 JavaScript 随意的把很多方法挂载到 Object 中,例如 Object.getPrototyoeOf()。Reflect 也有对应的方法:Reflect.getPrototypeOf(),方法的作用是一样的,只是表达语义的问题。
    • 如果 Reflect 中有对应的 Object 的方法,建议使用 Reflect 中的方法,Vue 3 中就是如此。
  • 严格模式
    • setdeleteProperty 方法都应当返回一个布尔值,返回 true 代表操作成功。
    • 严格模式下,如果没有返回 true 则会抛出要给 TypeError 异常。
      • ESM 默认开启严格模式。
  • receiver
    • get/set 方法中可以接收的可选参数
    • 代表当前的Proxy对象(proxy)
    • 如果 target 目标对象中设置了 getter,getter 中的 this 指向 receiver
    • Vue 3 中的响应式源码中,在获取或设置值的时候都会传入 receiver,防止意外发生
'use strict'

// 创建目标对象 - 将被代理的对象
const target = {
  get foo() {
    console.log(this === proxy) // true
    return this.bar // target 并没有 bar 属性,这里返回 proxy.bar
  }
}

const proxy = new Proxy(target, {
  get(target, key, receiver) {
    if (key === 'bar') {
      return 'value - bar'
    }
    return Reflect.get(target, key, receiver)
  }
})

console.log(proxy.foo) // value - bar
// return Reflect.get(target, key) 将返回 undefined

reactive 创建响应式对象

  • 接收一个参数
  • 首先会判断参数是否是对象,如果不是,直接返回
  • reactive 只能把对象转化成响应式对象,原始类型的属性要使用 ref 转化
  • 然后创建拦截器对象 handler,设置 get/set/deleteProperty
  • 最后返回 Proxy 对象,并传入拦截器对象
// /reactivity/index.js
// 判断是否为对象
const isObject = val => val !== null && typeof val === 'object'

// 用于 reactive 内部递归转化响应式对象
const convert = target => (isObject(target) ? reactive(target) : target)

// 接收 Object 原型上的 hasOwnProperty 方法
const hasOwnProperty = Object.prototype.hasOwnProperty
// 判断对象本身是否有指定属性
const hasOwn = (target, key) => hasOwnProperty.call(target, key)

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

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

      console.log('get', key)

      const result = Reflect.get(target, key, receiver)
      // 如果获取的属性值也是一个对象,也要将其转化成响应式对象
      return convert(result)
    },
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver)
      let result = true
      if (oldValue !== value) {
        result = Reflect.set(target, key, value, receiver)

        // 触发更新
        // ...

        console.log('set', key, value)
      }
      return result
    },
    deleteProperty(target, key) {
      // 判断 target 中是否有自己的 key 属性
      const hasKey = hasOwn(target, key)
      // 判断是否删除成功(如果不存在 key 属性,也会返回成功)
      const result = Reflect.deleteProperty(target, key)
      if (hasKey && result) {
        // 触发更新
        // ...

        console.log('delete', key)
      }
      return result
    }
  }

  return new Proxy(target, handler)
}
<!-- /index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <!-- ESM 方式加载js(需要启动一个web服务) -->
    <script type="module">
      import { reactive } from './reactivity/index.js'

      const obj = reactive({
        name: '张三',
        age: 18
      })

      obj.name = '李四'
      delete obj.age
      console.log(obj)
    </script>
  </body>
</html>

effect 收集依赖 & 触发更新

收集依赖思路

通过演示 Vue 3 中响应式系统模块 reactivity 的使用,总结实现依赖收集的思路。

安装 @vue/reactivity

npm install @vue/reactivity

实现一个简单功能

<!-- effct.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script type="module">
      import { reactive, effect } from './node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'

      // 创建响应式对象
      const product = reactive({
        name: 'iPhone',
        price: '12000',
        count: 3
      })

      let total = 0

      // effect 和 watchEffect 用法一样
      // watchEffect 内部就是调用 effect 实现的
      // effect 接收的函数首先会执行一次
      // 当函数中引用的响应式数据发生变化,就会再次执行
      effect(() => {
        total = product.price * product.count
      })

      console.log(total)

      product.price = 10000
      console.log(total)

      product.count = 1
      console.log(total)
    </script>
  </body>
</html>

分析过程

关注 effect(fn)函数

  1. 首次加载时,首先会执行 effect(fn) 函数,effect()内部首先会调用接收的箭头函数 fn
  2. 箭头函数fn中又访问了 reactive() 创建的响应式对象(代理对象) product
  3. 当访问 product.price的时候,会执行它的 get 方法,在 get 方法中要收集依赖
  4. 收集依赖的过程,就是存储 目标对象、对应的属性price 和 回调函数 fn
    1. 注意:存储的目标对象是 product 代理的目标对象,不是 product 本身。目标对象将在 get 方法中被传递给收集依赖的方法track
  5. 在触发更新的时候,再根据这个属性 price 找到对应的 effect 回调函数 fn
  6. 访问 product.count 的过程与 product.price 一样
  7. 当给 product.price 赋值的时候,会执行 price 属性对应的 set 方法,在 set 方法中会触发更新
  8. 触发更新,就是找到依赖收集过程中存储的目标对象的price属性对应的effect 回调函数fn,并立即执行。

以上是 依赖收集触发更新 的简单过程。

图解过程

在这里插入图片描述

在依赖收集的过程中,会创建三个集合:

  • targetMap(WeakMap 类型) - 用来记录 [目标对象]:[depsMap] 的字典。
    • key 是 目标对象
    • value 是 对应的 depsMap
    • WeakMap 弱引用的类型,当目标对象失去引用后可以销毁。
  • depsMap(Map 类型) - 用来记录 [目标对象中的属性的名称]:[dep] 的字典。
    • key 是 目标对象中属性的名称
    • value 是 dep
    • Map 类型
  • dep(Set 类型) - 用来存储 属性对应的 effect 回调函数
    • 一个属性可以存储多个 effect 回调函数
    • Set 集合中存储的元素不会重复

在触发更新的时候,根据目标对象的属性在这个结构中,找到 effect 回调函数,然后执行。

稍后要实现的 收集依赖的 track 函数,内部首先要根据当前的 targetMap 找到 depsMap

如果没有找到,要给当前对象创建一个 depsMap 并添加到 targetMap 中。

如果找到了,再去根据当前使用的属性,在 depsMap 中找到对应的 dep

dep 中存储的是 effect 回调函数,如果没有找到,为当前属性创建对应的 dep,并存储到 depsMap 中。

如果找到了,就把当前的effect 回调函数,存储到 dep 集合中。

收集依赖实现 effect && track

编写 effecttrack 方法,并在 get 方法中调用。

// /reactivity/index.js

/*...*/

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

  const handler = {
    get(target, key, receiver) {
      // 收集依赖
      track(target, key)

      const result = Reflect.get(target, key, receiver)
      // 如果获取的属性值也是一个对象,也要将其转化成响应式对象
      return convert(result)
    },
    set(target, key, value, receiver) { /*...*/ },
    deleteProperty(target, key) { /*...*/ }
  }

  return new Proxy(target, handler)
}

// 当前活动的 effect 函数
let activeEffect = null
export function effect(callback) {
  activeEffect = callback

  // 首先执行一次接收的函数
  // 访问响应式对象属性,去收集依赖
  callback()

  // 重置
  activeEffect = null
}

let targetMap = new WeakMap()

// 收集依赖
export function track(target, key) {
  if (!activeEffect) return

  let depsMap = targetMap.get(target)
  // 如果没有,创建 depsMap 并添加到字典中
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }

  let dep = depsMap.get(key)
  // 如果没有,创建 dep 并添加到字典中
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }

  // 添加 effect 回调函数
  dep.add(activeEffect)
}

触发更新实现 trigger

编写 trigger 方法,并在 set 方法中调用。

// /reactivity/index.js

/* ... */

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

  const handler = {
    get(target, key, receiver) {/* ... */},
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver)
      let result = true
      if (oldValue !== value) {
        result = Reflect.set(target, key, value, receiver)

        // 触发更新
        trigger(target, key)
      }
      return result
    },
    deleteProperty(target, key) {
      // 判断 target 中是否有自己的 key 属性
      const hasKey = hasOwn(target, key)
      // 判断是否删除成功(如果不存在 key 属性,也会返回成功)
      const result = Reflect.deleteProperty(target, key)
      if (hasKey && result) {
        // 触发更新
        trigger(target, key)
      }
      return result
    }
  }

  return new Proxy(target, handler)
}

// 当前活动的 effect 函数
let activeEffect = null
export function effect(callback) {/* ... */}

let targetMap = new WeakMap()

// 收集依赖
export function track(target, key) {/* ... */}

// 触发更新
export function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  const dep = depsMap.get(key)
  if (!dep) return

  // 遍历 dep 集合,执行 effect 回调函数
  dep.forEach(effect => {
    effect()
  })
}

测试

替换之前示例的 reactivity 模块

<!-- effct.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script type="module">
      // import { reactive, effect } from './node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'
      import { reactive, effect } from './reactivity/index.js'

      /* ... */
    </script>
  </body>
</html>

ref 创建响应式对象

实现 ref

  • reactive 只能将对象转化成响应式对象。

  • ref 可以接收 原始值 和 对象。

    • 如果接收的对象是 ref 创建的,则直接返回。
    • 如果接收的是普通对象,内部调用 reactive 创建响应式对象并返回。
    • 如果是原始值,则创建一个只有 value 属性的 响应式对象,并返回。
// /reactivity/index.js

/* ... */

export function reactive(target) {/* ... */}

// 当前活动的 effect 函数
let activeEffect = null
export function effect(callback) {/* ... */}

let targetMap = new WeakMap()

// 收集依赖
export function track(target, key) {/* ... */}

// 触发更新
export function trigger(target, key) {/* ... */}

export function ref(raw) {
  // 判断 raw 是否是 ref 创建的对象,如果是,直接返回
  if (isObject(raw) && raw.__v_isRef) return raw

  // convert 判断是否是对象,如果是,就调用reactive,如果不是,直接返回
  let value = convert(raw)

  const r = {
    __v_isRef: true, // 标识,表示该对象是 ref 创建的
    get value() {
      track(r, 'value')
      return value
    },
    set value(newValue) {
      // 判断新旧值是否相等
      if (newValue !== value) {
        raw = newValue
        value = convert(raw)
        trigger(r, 'value')
      }
    }
  }

  return r
}

<!-- ref.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script type="module">
      import { reactive, effect, ref } from './reactivity/index.js'

      // 创建响应式对象
      // const product = reactive({
      //   name: 'iPhone',
      //   price: '12000',
      //   count: 3
      // })
      const price = ref(12000)
      const count = ref(3)

      let total = 0

      effect(() => {
        // ref 创建的响应式对象,要使用它的 value 属性
        total = price.value * count.value
      })

      console.log(total)

      price.value = 10000
      console.log(total)

      count.value = 1
      console.log(total)
    </script>
  </body>
</html>

reactive vs ref

  • ref 可以把基本数据类型数据,转化成响应式对象
  • ref 返回的对象们重新赋值成对象也是响应式的
  • reactive 返回的对象,重新赋值丢失响应式
  • reactive 返回的对象不可以解构,可以通过 toRefs 将代理对象的属性转化成类似 ref 创建的响应式对象(包含value属性的对象),才可以使用解构语法。

toRefs

toRefsreactive 返回的对象的每一个属性,转换成类似 ref 返回的对象,从而可以对 reactive 返回的对象进行解构。

// /reactivity/index.js

/* ... */

export function reactive(target) {/* ... */}

// 当前活动的 effect 函数
let activeEffect = null
export function effect(callback) {/* ... */}

let targetMap = new WeakMap()

// 收集依赖
export function track(target, key) {/* ... */}

// 触发更新
export function trigger(target, key) {/* ... */}

export function ref(raw) {/* ... */}

export function toRefs(proxy) {
  // 判断是否是响应式的数组
  const ret = proxy instanceof Array ? new Array(proxy.length) : {}

  for (const key in proxy) {
    ret[key] = toProxyRef(proxy, key)
  }

  return ret
}

function toProxyRef(proxy, key) {
  const r = {
    __v_isRef: true,
    get value() {
      // proxy 是响应式对象,所以这里不需要收集依赖
      return proxy[key]
    },
    set value(newValue) {
      proxy[key] = newValue
    }
  }
  return r
}

<!-- toRefs.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script type="module">
      import { reactive, effect, toRefs } from './reactivity/index.js'

      function useProduct() {
        // 创建响应式对象
        const product = reactive({
          name: 'iPhone',
          price: '12000',
          count: 3
        })

        return toRefs(product)
      }

      const { price, count } = useProduct()

      let total = 0

      effect(() => {
        // ref 创建的响应式对象,要使用它的 value 属性
        total = price.value * count.value
      })

      console.log(total)

      price.value = 10000
      console.log(total)

      count.value = 1
      console.log(total)
    </script>
  </body>
</html>

computed

computed 需要接收一个有返回值的函数作为参数,这个函数的返回值就是计算属性的值。

并且要监听这个函数中使用的响应式数据的变化,最后将这个函数执行的结果返回。

// /reactivity/index.js

/* ... */

export function reactive(target) {/* ... */}

// 当前活动的 effect 函数
let activeEffect = null
export function effect(callback) {/* ... */}

let targetMap = new WeakMap()

// 收集依赖
export function track(target, key) {/* ... */}

// 触发更新
export function trigger(target, key) {/* ... */}

export function ref(raw) {/* ... */}

export function toRefs(proxy) {/* ... */}

function toProxyRef(proxy, key) {/* ... */}

export function computed(getter) {
  const result = ref()

  // 通过 effect 监听响应式数据的变化
  // 内部调用 getter 并将结果赋值给 result.value
  effect(() => {
    result.value = getter()
  })

  return result
}

<!-- computed.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script type="module">
      import { reactive, effect, computed } from './reactivity/index.js'

      // 创建响应式对象
      const product = reactive({
        name: 'iPhone',
        price: '12000',
        count: 3
      })

      let total = computed(() => {
        return product.price * product.count
      })

      // computed 返回的是 ref 创建的对象,所以要用属性 value
      console.log(total.value)

      product.price = 10000
      console.log(total.value)

      product.count = 1
      console.log(total.value)
    </script>
  </body>
</html>

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值