Vue3响应式方案及ref reactive的区别详解

目录
  • 一、前言
  • 二、新的方案
    • 1. 缘由
    • 2. Proxy 和 Reflect
      • 1) Proxy
      • 2) Reflect
    • 3. reactive
      • 1) createReactiveObject() 函数
      • 2) mutableHandlers() 函数 -> 对象类型的 handlers
      • 3) mutableInstrumentations() 函数 -> Map Set等类型的 handlers
    • 4. ref
      • 1) createRef()
      • 2) toReactive()
      • 3)proxyRefs() 自动脱 ref
  • 三、 结语

一、前言

距离 Vue3 出来了已经有一段时间了, 最近呢重温了一下Vue3的响应式方案,以及ref reactive的区别,相信你看完本文,也能对 Vue3 响应式的了解有所提高

在看源码的过程中,时常会感到框架的设计之美,看起来也会感到赏心悦目, 这也是能坚持把源码看下去的动力

二、新的方案

1. 缘由

  • 已知在 Vue2 中, 响应式原理一直是采用 Object.defineProperty 来进行,那这样做有什么权限呢? 下面一一道来

    • 这个API, 只能拦截 get / set 的属性
    • 如对象 新增 或者 删除 了属性,则无法监听到改变
    • 对于数组,若使用数组的原生方法改变数组元素的时候 也无法监听到改变
  • 所以呢在Vue3中采用了 ProxyReflect搭配来代理数据

2. Proxy 和 Reflect

1) Proxy

既然Vue3中响应式数据是基于 Proxy 实现的,那么什么是Proxy呢?

使用Proxy可以创建一个代理对象,它可以实现对 对象数据 的 代理, 所以它 无法对非对象值进行代理,也就是为什么Vue3中对于非对象值要使用 ref 来进行响应式的原因 (后面讲解ref的时候再细说)

  • 代理是指 允许我们拦截并重新定义对一个对象的基本操作。 例如: 拦截读取、 修改等操作.
const obj = {}

const newP = new Proxy(obj, {
     // 拦截读取
  get(){/*...*/ },

  // 拦截设置属性操作
  set(){/*...*/ }
})

2) Reflect

说完了Proxy, 接下来我们来说说 Reflect

通过观察 MDN 官网可以发现, Reflect的方法Proxy的拦截器方法 名字基本一致

那就出现了一个问题,我们为什么要用 Reflect 呢

主要还是它的第三个参数,你可以理解为函数调用过程中的this,我们来看看它配合 Proxy 具体的用途吧

const obj = {
  foo: 1,

  // obj 中有一个 getter属性 通过this获取foo的值
  get getFoo() { return this.foo; }
};

const newP = new Proxy(obj,
  {
    // 拦截读取
    get(target, key) {
      console.log('读取', key); // 注意这里目前没有使用 Reflect
      return target[key];
    },

    // 拦截设置属性操作
    set(target, key, newVal) {
      console.log('修改', key);
      target[key] = newVal
    }
  })

obj.foo++
console.log(newP.getFoo);  

执行上面代码你会发现, 在 Proxy 中 get 拦截的中,只会触发对 getFoo 属性进行读取的拦截, 而无法触发在 getFoo 里面对 this.foo 进行读取的拦截!

问题就出现在 getFoo 这个getter里, 这里面的 this 在我们 未使用 Reflect 的时候指向它的原始对象,所以我们才无法通过 Proxy 拦截到属性读取

只需修改一下上面代码中 Proxy 里面的 get 拦截方法

    // 拦截读取
    get(target, key, receiver) {
      console.log('读取', key);
      return Reflect.get(target, key, receiver); // 使用 Reflect返回读取的属性值
    },

这下再执行上面的例子,就会发现能正常对 getFoo 里面的 foo 属性进行读取的拦截。 因为这个时候的 this 已经指向了代理对象 newP

以上呢,就是对 Proxy 和 Reflect 的简易讲解,接下来我们讲讲 Vue3 中的 reactive

3. reactive

看源码会发现,我们平时使用 reactive 的时候,会调用一个 createReactiveObject 的方法

这个地方在: packages\reactivity\src\reactive

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,  // 普通对象的 handlers
    mutableCollectionHandlers, // Set Map 等类型的 handlers
    reactiveMap
  )
}

1) createReactiveObject() 函数

其中主要是做一些前置判断,然后建立响应式地图

WeakMap -> Map -> Set

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {

  //  若目标数据是不是对象则直接返回
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }

  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  // raw 代表原始数据
  // 或者是非响应式数据就直接返回 原数据
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }

  // 如已被代理则直接返回代理的这个对象
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // only a whitelist of value types can be observed.
  // 只有在白名单中的类型才可以被代理
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }

  // 建立代理 Proxy
  const proxy = new Proxy(
    target,
    // 使用不同的 hanlders
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )

  // 存储到响应式地图中
  proxyMap.set(target, proxy)
  return proxy
}

2) mutableHandlers() 函数 -> 对象类型的 handlers

这个地方在: packages\reactivity\src\baseHandlers

主要讲讲getset

export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(), // 读取属性
  set: createSetter(), // 设置属性
  deleteProperty,      // 删除属性
  has,                 // 判断是否存在对应属性
  ownKeys              // 获取自身的属性值
}

get

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 判断返回一些特定的值 例如 是 readonly 的就返回 readonlyMap,是 reactive 的就返回 reactiveMap 等等
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return shallow
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target
    }

    // 如果是数组要进行一些特殊处理
    const targetIsArray = isArray(target)

    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      //  重写数组的方法
      // 'includes', 'indexOf', 'lastIndexOf', 'push', 'pop', 'shift', 'unshift', 'splice'
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    // 获取属性值
    const res = Reflect.get(target, key, receiver)

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    // 如果非只读属性 才进行依赖收集
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    // 浅层响应则直接返回对应的值
    if (shallow) {
      return res
    }

    // 如果是ref 则自动进行 脱ref
    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    // 返回值是对象
    // 如果是只读就用 readonly 包裹返回数据
    // 否则则进行递归深层包裹 reactive 返回 Proxy 代理对象
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    // 如都不是上面的判断 则返回这个数据
    return res
  }
}

set

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // 缓存旧值
    let oldValue = (target as any)[key]

    if (!shallow && !isReadonly(value)) {
      if (!isShallow(value)) {
        value = toRaw(value)
        oldValue = toRaw(oldValue)
      }

      // 若是 ref 并且非只读 则直接修改 ref的值
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    // 是否有对于的key
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)

    // 修改对应的值
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original

    // 若目标是原型链上的内容就不触发依赖
    if (target === toRaw(receiver)) {

      // 这里主要是判断是 新增属性 还是修改属性的操作
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }

    // 最终返回结果
    return result
  }
}

3) mutableInstrumentations() 函数 -> Map Set等类型的 handlers

这个地方在: packages\reactivity\src\collectionHandlers

其主要是为了解决 代理对象 无法访问集合类型的属性和方法

function createInstrumentations() {
   // 主要就是代理了 Map Set等类型的方法 具体实现各位可以去上面地址中的文件里查看
  const mutableInstrumentations: Record<string, Function> = {
    get(this: MapTypes, key: unknown) {
      return get(this, key)
    },
    get size() {
      return size(this as unknown as IterableCollections)
    },
    has,
    add,
    set,
    delete: deleteEntry,
    clear,
    forEach: createForEach(false, false)
  }

  const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
  iteratorMethods.forEach(method => {
    mutableInstrumentations[method as string] = createIterableMethod(
      method,
      false,
      false
    )
  })

  return [
    mutableInstrumentations
  ]
}

4. ref

之前说过 Proxy 代理的必须是对象数据类型,而非对象数据类型 例如: string number 等等 则不能用其进行代理, 所以有了 ref 的概念

联想到上面说的 reactive 和我们日常使用的 .value 的形式, 是不是就认为 ref 直接把原始数据包裹成对象 然后通过 Proxy 进行代理的呢?

最开始我也以为是这样,但是查看了源码中发现其实并不是, 其实是创建 ref 的时候, 实例化了一个 class -> new RefImpl(rawValue, shallow) ,然后通过自定义的 get set来进行依赖收集和依赖更新

源码地址: packages\reactivity\src\ref

1) createRef()

export function ref(value?: unknown) {
   // 调用创建方法
  return createRef(value, false)
}

function createRef(rawValue: unknown, shallow: boolean) {
  // 如果已经是一个ref 则直接返回
  if (isRef(rawValue)) {
    return rawValue
  }

  // 实例化 class
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined

  // 用于区分 ref 的不可枚举属性 例如 isRef 方法就是直接判断这个属性
  public readonly __v_isRef = true

  // 构造函数
  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    // 依赖收集
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
     // 拿到原始值
    newVal = this.__v_isShallow ? newVal : toRaw(newVal)

    // 判断是否有变化 如有才进行更新
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this.__v_isShallow ? newVal : toReactive(newVal)
      // 依赖更新
      triggerRefValue(this, newVal)
    }
  }
}

2) toReactive()

我们日常使用的时候会发现, ref 传入一个对象 也能正常使用,其玄机就在 创建class 的时候,构造函数中调用了 toReactive 这个函数

export const toReactive = &lt;T extends unknown&gt;(value: T): T =&gt;
  // 如果是一个对象则利用 reactive 代理成 Proxy 返回
  isObject(value) ? reactive(value) : value
复制代码

3)proxyRefs() 自动脱 ref

我们在使用 ref 的时候会发现,从 setup 返回的 ref, 在页面中使用并不需要 .value ,这都归功 proxyRefs 这个函数,减少了我们在模板中需要判断 ref 的心智负担

<template>
   // 这里并不需要 .value
   // 并且如果我 直接在模板的点击事件中 使用 count++ 响应式也不会丢失
  <div @click="count++"> {{ count }} </div>
</template>

const myComponent = {
   setup() {
    const count = ref(0)

    return { count }
   }
}

下面我们就来看看 proxyRefs 的实现

export function proxyRefs<T extends object>(
  objectWithRefs: T
): ShallowUnwrapRef<T> {
   // 如果是 reactive 则不处理
  return isReactive(objectWithRefs)
    ? objectWithRefs
    // 如果是 ref 则直接通过 Proxy 代理一下
    : new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

export function unref<T>(ref: T | Ref<T>): T {
   // 如果是 ref 直接返回 .value 的值
  return isRef(ref) ? (ref.value as any) : ref
}

const shallowUnwrapHandlers: ProxyHandler<any> = {
  // get 的时候直接脱 ref
  get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),

  set: (target, key, value, receiver) => {
    const oldValue = target[key]

    // 如果旧值是 ref 而新值不是 ref 直接把 新值 替换 旧值 的.value属性
    if (isRef(oldValue) && !isRef(value)) {
      oldValue.value = value
      return true
    } else {
      return Reflect.set(target, key, value, receiver)
    }
  }
}

然后我们会发现在模板调用中,会自动把setup的返回值通过 proxyRefs 调用一遍

通过上面的源码来个总结:

  • 我们在编写 Vue 组件的时候, 组件中 setup 的函数所返回的数据会自动传给 proxyRefs 函数处理一遍,所以我们在页面中使用 无需 .value
  • ref 最后在 模板中 还是被 Proxy 代理 了一遍

三、 结语

以上呢就是对 Vue3 的响应式的方案解析了, 以及关于 reactive ref的区别相信你如果仔细看完了,也会心知肚明了

到此这篇关于Vue3响应式方案及ref reactive的区别详解的文章就介绍到这了,更多相关Vue3响应式及ref reactive区别内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Vue3中的ref和reactive响应式原理解析

    目录 1 ref 2 isref判断是不是一个ref对象 3 shallowref创建一个跟踪自身.value变化的 ref,但不会使其值也变成响应式的 4 triggerRef 5 customRef 6 reactive用来绑定复杂的数据类型 7 readonly 8 shallowReactive 9toRef 10toRefs 11toRaw Vue3系列4--ref和reactive响应式 本节主要介绍了响应式变量和对象,以及变量和对象在响应式和非响应式之间的转换. 1 ref 接受一

  • setup+ref+reactive实现vue3响应式功能

    setup 是用来写组合式 api ,内部的数据和方法需要通过 return 之后,模板才能使用.在之前 vue2 中,data 返回的数据,可以直接进行双向绑定使用,如果我们把 setup 中数据类型直接双向绑定,发现变量并不能实时响应.接下来就看看setup如何实现data的响应式功能? 一.ref setup 内的自定义属性不具备响应式能力,所以引入了 ref ,ref 底层通过代理,把属性包装值包装成一个 proxy ,proxy 内部是一个对象,使得基础类型的数据具备响应式能力,使用之

  • vue3中如何使用ref和reactive定义和修改响应式数据(最新推荐)

    需求:vue3中setup组合式api中如何定义响应式数据并且修改赋值呢? 1.字符串/数字:“ref”是vue3中用来存储值的响应式数据源,它可以定义字符串,数字等 <script setup> import { ref } from 'vue' // "ref"是用来存储值的响应式数据源. // 理论上我们在展示该字符串的时候不需要将其包装在 ref() 中, const message = ref('Hello World!') // 但是在这个示例中更改这个值的时候

  • Vue3响应式对象Reactive和Ref的用法解读

    目录 一.内容简介 二.Reactive 1. 关键源码 2. 源码流程分析 三.代理拦截操作 1. 数组操作 2.Get操作 3. Set操作 4. 其余行为拦截操作 四.Ref对象 1. 思考一个问题 2. 简要说明 3. 关键源码 四. 源码解析 五.总结 一.内容简介 本篇文章着重结合源码版本V3.2.20介绍Reactive和Ref.前置技能需要了解Proxy对象的工作机制,以下贴出的源码均在关键位置备注了详细注释. 备注:本篇幅只讲到收集依赖和触发依赖更新的时机,并未讲到如何收集依赖

  • Vue3响应式方案及ref reactive的区别详解

    目录 一.前言 二.新的方案 1. 缘由 2. Proxy 和 Reflect 1) Proxy 2) Reflect 3. reactive 1) createReactiveObject() 函数 2) mutableHandlers() 函数 -> 对象类型的 handlers 3) mutableInstrumentations() 函数 -> Map Set等类型的 handlers 4. ref 1) createRef() 2) toReactive() 3)proxyRefs(

  • vue3响应式Object代理对象的读取示例详解

    目录 正文 读取属性 xx in obj for ... in 正文 从这一章开始,作者将更新深入的讲解响应式,尤其是vue3响应式的具体的实现.其实在前面一章,如果你仔细阅读,你是可以实现一个简单的响应式函数的,类似于@vue/reactive,当然那只是个demo,是个玩具,我能不能在生产环境上去使用的,它差了太多功能和边界条件. 现在,我们才是真正的深入@vue/reactive. 在vue中,obj.a是一个读取操作,但是仔细想来,读取这个操作很宽泛. obj.a // 访问一个属性 '

  • Vue3中Vite和Vue-cli的特点与区别详解

    目录 1. 创建3.0项目 Vite 与 Vue-cli 是什么? Vue-cli 的特点: Vite 的特点: Vite 和 Vue-cli的区别: 总结: 1. 创建3.0项目 vue-cli : 安装并执行 npm init vue@latest 选择项目功能时: 除了第一项的项目名字外,其他可以暂时No cd title npm install npm run dev :运行 npm run build: 打包 (生成一个dist文件夹) vite: 使用vite 体验更快速 npm i

  • Vue3 响应式侦听与计算的实现

    响应式侦听和计算 有时我们需要依赖于其他状态的状态--在 Vue 中,这是用组件 计算属性 处理的,以直接创建计算值,我们可以使用 computed 方法:它接受 getter 函数并为 getter 返回的值返回一个不可变的响应式 ref 对象. 我们先来看看一个简单的例子,关于计算值的方式,同样我们在 src/TemplateM.vue 写下如下代码: <template> <div class="template-m-wrap"> count --->

  • 浅析vue3响应式数据与watch属性

    是Vue3的 composition API中2个最重要的响应式API ref用来处理基本类型数据, reactive用来处理对象(递归深度响应式) 如果用ref对象/数组, 内部会自动将对象/数组转换为reactive的代理对象 ref内部: 通过给value属性添加getter/setter来实现对数据的劫持 reactive内部: 通过使用Proxy来实现对对象内部所有数据的劫持, 并通过Reflect操作对象内部数据 ref的数据操作: 在js中要.value, 在模板中不需要(内部解析

  • 详解vue3 响应式的实现原理

    目录 核心设计思想 Vue.js 2.x 响应式 Vue.js 3.x 响应式 依赖收集:get 函数 派发通知:set 函数 总结 源码参考 核心设计思想 除了组件化,Vue.js 另一个核心设计思想就是响应式.它的本质是当数据变化后会自动执行某个函数,映射到组件的实现就是,当数据变化后,会自动触发组件的重新渲染.响应式是 Vue.js 组件化更新渲染的一个核心机制. Vue.js 2.x 响应式 我们先来回顾一下 Vue.js 2.x 响应式实现的部分: 它在内部通过 Object.defi

  • 一文详解Vue3响应式原理

    目录 回顾 vue2.x 的响应式 vue3的响应式 Reflect 回顾 vue2.x 的响应式 实现原理: 对象类型:通过object.defineProperty()对属性的读取.修改进行拦截(数据劫持) 数组类型:通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了包裹) Object.defineProperty(data,'count ",{ get(){}, set(){} }) 存在问题: 新增属性.删除属性,界面不会更新 直接通过下标修改数组,界面不会自动更新 但是

  • vue3 响应式对象如何实现方法的不同点

    目录 vue3响应式对象实现方法的不同点 Vue2和Vue3响应式原理对比 响应式原理实现逻辑 Vue2响应式原理简化 Vue2响应式原理弊端 Vue3响应式原理简化 vue3响应式对象实现方法的不同点 vue 响应式对象是利用 defindeProperty 实现,vue3 则是使用 Proxy 来实现响应式的. 二者,虽然都实现了 响应式的功能,但实现方式不一样,解决问题也有些不同. vue2 中,如果想要实现,数组元素的更新,则是需要 用到检测更新的方法 set 之类的,但 vue3的响应

  • Vue3响应式对象是如何实现的(1)

    目录 简单的响应式实现 Proxy与响应式 为什么需要Proxy? Proxy创建的代理对象与原始对象有何不同? 多副作用函数的响应式实现 简单的响应式实现 为了方便说明,先来看一个简单的例子. const obj = { text: 'hello vue' } function effect() { document.body.innerText = obj.text } 这段代码中,如果obj是一个响应式数据,会产生什么效果呢?当obj.text中的内容改变时,document.body.i

随机推荐