Vue3 响应式系统原理

282 阅读7分钟

一、Proxy 对象

  • Proxy(代理) 是 ES6中新增的一个特性,Proxy 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式

  • Proxy 可以理解为在目标对象之前架设一层拦截,外部所有的访问或者修改都必须先通过这层拦截,因此提供了一种机制,可以对外部的操作进行过滤和修改。这个词的原理为代理,在这里可以表示由它来“代理”某些操作,译为“代理器”。

  • Proxy 构造函数第一个参数是要代理的对象,第二个参数是一个对象(可以称为处理器,或者监听器)

Proxy 出现的问题一:

问题一: set 和 deleteProperty 中需要返回布尔类型的值,在严格模式下,如果返回 false 的话会出现 Type Error 的异常

解决方法:使用 ES6 的 Reflect

Reflect 是 ES6 为操作对象而提供的新API,而这个API设计的目的只要有:

  1. 将 Object 对象的一些属于语言内部的方法放到 Reflect 对象上,从 Reflect 上能拿到语言内部的方法。如:Object.defineProperty

  2. 修改某些 object 方法返回的结果。如:Object.defineProperty(obj, name, desc) 在无法定义属性的时候会报错,而 Reflect.defineProperty(obj, name, desc) 则会返回 false ,如果定义成功会返回 true

  3. 让 Object 的操作都变成函数行为。如 object 的命令式:name in obj 和delete obj[name] 则与 Reflect.has(obj, name)、Reflect.deleteProperty(obj, name)相等

  4. Reflect 对象的方法与 Proxy 对象的方法一一对应,只要 proxy 对象上有的方法 reflect 也能找到

 // 开启严格模式
    'use strict'
    // 问题1: set 和 deleteProperty 中需要返回布尔类型的值
    // 在严格模式下,如果返回 false 的话会出现 Type Error 的异常
    const target = {
      foo: 'xxx',
      bar: 'yyy'
    }
    // Reflect.getPrototypeOf()
    // Object.getPrototypeOf()

    // 通过 Proxy 代理 target 对象
    // Proxy 构造函数第一个参数是要代理的对象,第二个参数是一个对象(可以称为处理器,或者监听器)
    const proxy = new Proxy(target, {
      // 监听属性访问操作
      get(target, key, receiver) { // 第三个参数 receiver 指的是当前的 proxy 对象
        // Reflect 是反射的意思,是 es6 中新增的成员,用来代码运行期间获取或者设置成员
        return Reflect.get(target, key, receiver)
      },
      // 监听属性赋值操作
      set(target, key, value, receiver) {
        // Reflect.set 设置成功会返回 true ,
        return Reflect.set(target, key, value, receiver)
      },
      // 监听属性删除操作
      deleteProperty(target, key) {
        return Reflect.deleteProperty(target, key)
      }
    })

    proxy.foo = 'zzz'
    // // delete proxy.foo

Proxy 出现的问题二:

问题2:Proxy 和 Reflect 中使用的 receiver

  • Proxy 中 receiver:Proxy 或者继承 Proxy 的对象
  • Reflect 中 receiver:如果 target 对象中设置了 getter,那么 getter 中的 this 指向 receiver
// 问题2:Proxy 和 Reflect 中使用的 receiver

const obj = {
  get foo() {
    // 如果没有设置 receiver,此处 this 指向 obj,如果设置了 receiver,此处 this 指向向代理对象 proxy
    console.log(this)
    return this.bar
  }
}

const proxy = new Proxy(obj, {
  get(target, key, receiver) {
    if (key === 'bar') {
      return 'value - bar'
    }
    return Reflect.get(target, key, receiver) // 设置 receiver
  }
})
console.log(proxy.foo)

二、实现 reactive

reactive 主要三点

  1. 接收一个参数,判断这个参数是不是对象
  2. 创建拦截器对象 handler ,设置 get/set/deleteProperty 方法
  3. 返回 Proxy 对象
const isObject = val => val !== null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target) : target
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) => hasOwnProperty.call(target, key)

// reactive
export function reactive(target) {

  // 1、判断是不是对象,如果不是直接返回
  if (!isObject(target)) return target

  // 2、创建拦截器对象 handler ,设置 get/set/deleteProperty 方法
  const handler = {
    get(target, key, receiver) {
      // 收集依赖
      track(target, key)
      const result = Reflect.get(target, key, receiver)
      // 如果 result 是一个对象,就将这个对象传到 reactive 做一层代理
      return convert(result)
    },
    set(target, key, value, receiver) {
      // 先获取这个 key 属性的值
      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 hadKey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (hadKey && result) {
        // 触发更新
        trigger(target, key)
      }
      return result
    }
  }

  // 3、返回经过 Proxy 代理后的对象
  return new Proxy(target, handler)
}

三、实现依赖收集

  • 先实现 effect 方法,effect 方法的使用和 watchEffect 类似,watchEffect 内部就是调用 effect 方法的。

  • effect 方法是用来监视响应式数据的变化的,一旦变化,就执行该回调函数

// effect 实现
let activeEffect = null // 用来记录 callback,因为收集依赖函数track要用
export function effect(callback) {
  activeEffect = callback
  callback() // 执行 callback 函数,这样就可以访问响应式对象属性,去收集依赖
  activeEffect = null
}
  • 收集依赖的过程就是存储这个属性和这个回调函数,然而属性又和对象相关,所以在 track 方法中首先会存储 target 目标对象,然后是 target 对应的属性,和属性对于的箭头函数

image.png

收集依赖 track 方法实现

// 存储目标对象
let targetMap = new WeakMap()
// 收集依赖 ( track 第一个参数是目标对象,第二个参数是要收集的属性 )
export function track(target, key) {
  if (!activeEffect) return // 如果 activeEffect 不存在,就不收集依赖
  let depsMap = targetMap.get(target) // 判断 targetMap 中是否有这个目标对象
  if (!depsMap) {
    // 如果 targetMap 没有这个目标对象,targetMap 设置这个目标对象为 key,并设置 value 为一个新的 Map 对象
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key) // 存储 key 和对应的回调函数集合
  if (!dep) {
    // 如果 depsMap 没有这个属性,depsMap 就设置这个属性为 key,并设置value 为一个新的 Set 对象(Set集合用来收集回调函数)
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(activeEffect)
}

四、实现触发更新

触发更新 trigger 函数实现

// 触发更新
export function trigger(target, key) {
  // 从 targetMap 中找到触发更新的 depsMap
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  // 根据 key 找到 depsMap 中的对应的 dep 集合(集合中存储的就是 effect 回调函数)
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => {
      effect()
    })
  }
}

五、实现 ref

export function ref(raw) {
  // 1、判断 raw 是否是 ref 创建的对象,如果是的话直接返回
  if (isObject(raw) && raw.__v_isRef) {
    return
  }
  // 2、判断 raw 是否是对象,如果是对象直接调用 reactive 创建响应式对象,否者直接返回
  let value = convert(raw)
  // 3、创建一个带有 value 属性的响应式对象返回
  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
}

总结:

  1. ref 可以将基本数据类型数据转成响应式对象
  2. ref 返回的对象,被重新赋值成对象也是响应式的
  3. reactive 返回的对象,重新赋值会丢失响应式
  4. reactive 返回的对象不可以解构,需要结构必须先调用 toRefs

六、实现 toRefs

// toRefs 实现
export function toRefs(proxy) {
  // 1、判断传入的对象是否是 proxy 对象,如果不是就发送警告

  // 2、判断传入的对象是否是数组,如果是创建一个长度为 proxy.length 的数组
  const ret = proxy instanceof Array ? new Array(proxy.length) : {}

  // 3、遍历 proxy 中所有属性(如果是数组,就遍历所有索引)
  for (const key in proxy) {
    // 4、把每一个属性都转换成 ref 返回的对象
    ret[key] = toProxyRef(proxy, key)
  }

  return ret
}

function toProxyRef(proxy, key) {
  const r = {
    __v_isRef: true,
    get value() {
      // 这里不需要收集依赖,因为访问 proxy[key] 的时候代理对象内部已经收集了依赖
      return proxy[key]
    },
    set value(newValue) {
      // 同理,代理对象已经触发更新
      proxy[key] = newValue
    }
  }
  return r
}

七、实现 computed

computed 接收一个有返回值的函数作为参数,这个函数的返回值就是计算属性的值,并且会监听函数内部使用的响应式数据的变化,最后把函数执行结果返回

export function computed(getter) {

  // 1、创建一个 ref 响应式对象 result
  const result = ref() // 默认传入 undefined

  // 2、把 getter 函数结果赋值给 result
  effect(() => (result.value = getter()))

  // 3、返回
  return result
}