• 技术文章 >web前端 >Vue.js

    详解vue3中reactive和ref的区别(源码解析)

    青灯夜游青灯夜游2022-08-22 19:53:30转载685
    vue中reactive和ref的区别是什么?下面本篇文章带大家深入源码彻底搞清vue3中reactive和ref的区别,希望对大家有所帮助!

    大前端零基础入门到就业:进入学习

    在vue3的日常开发中,我发现很多人都是基于自己的习惯reactiveref一把梭,虽然这样都可以实现需求,既然这样那为什么已经有了reactive还需要再去设计一个ref呢?这两者的实际运用场景以及区别是什么呢?

    并且关于ref的底层逻辑,有的人说ref的底层逻辑还是reactive。有的人说ref的底层是classvalue只是这个class的一个属性,那这两种说法哪种正确呢?都有没有依据呢?

    抱着这样的疑问我们本次就深入源码,彻底搞清vue3中reactiveref的区别。(学习视频分享:vue视频教程

    不想看源码的童鞋,可以直接拉到后面看总结

    reactive

    源码地址:packages/reactivity/reactive.ts

    首先我们看一下vue3中用来标记目标对象target类型的ReactiveFlags

    // 标记目标对象 target 类型的 ReactiveFlags
    export const enum ReactiveFlags {
      SKIP = '__v_skip',
      IS_REACTIVE = '__v_isReactive',
      IS_READONLY = '__v_isReadonly',
      RAW = '__v_raw'
    }
    
    export interface Target {
      [ReactiveFlags.SKIP]?: boolean          // 不做响应式处理的数据
      [ReactiveFlags.IS_REACTIVE]?: boolean   // target 是否是响应式
      [ReactiveFlags.IS_READONLY]?: boolean   // target 是否是只读
      [ReactiveFlags.RAW]?: any               // 表示proxy 对应的源数据, target 已经是 proxy 对象时会有该属性
    }

    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 (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
        return target
      }
      // 创建 observe
      return createReactiveObject(
        target,
        false,
        mutableHandlers,
        mutableCollectionHandlers,
        reactiveMap
      )
    }

    reactive函数接收一个target对象,如果target对象只读则直接返回该对象

    若非只读则直接通过createReactiveObject创建observe对象

    createReactiveObject

    看着长不要怕,先贴createReactiveObject完整代码,我们分段阅读

    /**
     * 
     * @param target 目标对象
     * @param isReadonly 是否只读
     * @param baseHandlers 基本类型的 handlers
     * @param collectionHandlers 主要针对(set、map、weakSet、weakMap)的 handlers
     * @param proxyMap  WeakMap数据结构
     * @returns 
     */
    
    function createReactiveObject(
      target: Target,
      isReadonly: boolean,
      baseHandlers: ProxyHandler<any>,
      collectionHandlers: ProxyHandler<any>,
      proxyMap: WeakMap<Target, any>
    ) {
    
      // typeof 不是 object 类型的,在开发模式抛出警告,生产环境直接返回目标对象
      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
      // 已经是响应式的就直接返回(取ReactiveFlags.RAW 属性会返回true,因为进行reactive的过程中会用weakMap进行保存,
      // 通过target能判断出是否有ReactiveFlags.RAW属性)
      // 例外:对reactive对象进行readonly()
      if (
        target[ReactiveFlags.RAW] &&
        !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
      ) {
        return target
      }
      // target already has corresponding Proxy
      // 对已经Proxy的,则直接从WeakMap数据结构中取出这个Proxy对象
      const existingProxy = proxyMap.get(target)
      if (existingProxy) {
        return existingProxy
      }
      // only a whitelist of value types can be observed.
      // 只对targetTypeMap类型白名单中的类型进行响应式处理
      const targetType = getTargetType(target)
      if (targetType === TargetType.INVALID) {
        return target
      }
      // proxy 代理 target
      // (set、map、weakSet、weakMap) collectionHandlers
      // (Object、Array) baseHandlers
      const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
      )
      proxyMap.set(target, proxy)
      return proxy
    }

    首先我们看到createReactiveObject接收了五个参数

      target: Target,
      isReadonly: boolean,
      baseHandlers: ProxyHandler<any>,
      collectionHandlers: ProxyHandler<any>,
      proxyMap: WeakMap<Target, any>

    target 目标对象

    isReadonly 是否只读

    baseHandlers 基本类型的 handlers 处理数组,对象

    collectionHandlers 处理 set、map、weakSet、weakMap

    proxyMap WeakMap数据结构存储副作用函数


    这里主要是通过ReactiveFlags.RAWReactiveFlags.IS_REACTIVE判断是否是响应式数据,若是则直接返回该对象

     if (
        target[ReactiveFlags.RAW] &&
        !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
      ) {
        return target
      }

    对于已经是Proxy的,则直接从WeakMap数据结构中取出这个Proxy对象并返回

      const existingProxy = proxyMap.get(target)
      if (existingProxy) {
        return existingProxy
      }

    这里则是校验了一下当前target的类型是不是ObjectArrayMapSetWeakMapWeakSet,如果都不是则直接返回该对象,不做响应式处理

     // 只对targetTypeMap类型白名单中的类型进行响应式处理
      const targetType = getTargetType(target)
      if (targetType === TargetType.INVALID) {
        return target
      }

    校验类型的逻辑

    function getTargetType(value: Target) {
      return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
        ? TargetType.INVALID
        : targetTypeMap(toRawType(value))
    }
    
    function targetTypeMap(rawType: string) {
      switch (rawType) {
        case 'Object':
        case 'Array':
          return TargetType.COMMON
        case 'Map':
        case 'Set':
        case 'WeakMap':
        case 'WeakSet':
          return TargetType.COLLECTION
        default:
          return TargetType.INVALID
      }
    }

    所有的前置校验完后,就可以使用proxy 代理target对象了

    这里使用了一个三目运算符通过TargetType.COLLECTION来执行不同的处理逻辑

    // proxy 代理 target
      // (set、map、weakSet、weakMap) collectionHandlers
      // (Object、Array) baseHandlers
      const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
      )
      proxyMap.set(target, proxy)
      return proxy

    现在对createReactiveObject的执行逻辑是不是就很清晰了

    到这里还没有结束,createReactiveObject中最后proxy是如何去代理target的呢?这里我们用baseHandlers举例,深入baseHandlers的内部去看看

    baseHandlers

    源码地址:packages/reactivity/baseHandlers.ts

    reactive.ts中我们可以看到一共引入了四种 handler

    import {
      mutableHandlers,
      readonlyHandlers,
      shallowReactiveHandlers,
      shallowReadonlyHandlers
    } from './baseHandlers'

    我们以mutableHandlers为例

    // 可变处理
    // const get = /*#__PURE__*/ createGetter()
    // const set = /*#__PURE__*/ createSetter()
    // get、has、ownKeys 会触发依赖收集 track()
    // set、deleteProperty 会触发更新 trigger()
    export const mutableHandlers: ProxyHandler<object> = {
      get,                  // 用于拦截对象的读取属性操作
      set,                  // 用于拦截对象的设置属性操作
      deleteProperty,       // 用于拦截对象的删除属性操作
      has,                  // 检查一个对象是否拥有某个属性
      ownKeys               // 针对 getOwnPropertyNames,  getOwnPropertySymbols, keys 的代理方法
    }

    这里的getset分别对应着createGetter()createSetter()

    先上完整版代码

    /**
     * 用于拦截对象的读取属性操作
     * @param isReadonly 是否只读
     * @param shallow 是否浅观察
     * @returns 
     */
    function createGetter(isReadonly = false, shallow = false) {
      /**
       * @param target 目标对象
       * @param key 需要获取的值的键值
       * @param receiver 如果遇到 setter,receiver 则为setter调用时的this值
       */
      return function get(target: Target, key: string | symbol, receiver: object) {
        // ReactiveFlags 是在reactive中声明的枚举值,如果key是枚举值则直接返回对应的布尔值
        if (key === ReactiveFlags.IS_REACTIVE) {
          return !isReadonly
        } else if (key === ReactiveFlags.IS_READONLY) {
          return isReadonly
        } else if (
          // 如果key是raw  receiver 指向调用者,则直接返回目标对象。
          // 这里判断是为了保证触发拦截 handle 的是 proxy 本身而不是 proxy 的继承者
          // 触发拦的两种方式:一是访问 proxy 对象本身的属性,二是访问对象原型链上有 proxy 对象的对象的属性,因为查询会沿着原型链向下找
          key === ReactiveFlags.RAW &&
          receiver ===
            (isReadonly
              ? shallow
                ? shallowReadonlyMap
                : readonlyMap
              : shallow
              ? shallowReactiveMap
              : reactiveMap
            ).get(target)
        ) {
          return target
        }
    
        const targetIsArray = isArray(target)
        // 如果目标对象 不为只读、是数组、key属于arrayInstrumentations:['includes', 'indexOf', 'lastIndexOf']方法之一,即触发了这三个方法之一
        if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
          // 通过 proxy 调用,arrayInstrumentations[key]的this一定指向 proxy
          return Reflect.get(arrayInstrumentations, key, receiver)
        }
    
        const res = Reflect.get(target, key, receiver)
    
        // 如果 key 是 symbol 内置方法,或者访问的是原型对象__proto__,直接返回结果,不收集依赖
        if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
          return res
        }
    
        // 不是只读类型的 target 就收集依赖。因为只读类型不会变化,无法触发 setter,也就会触发更新
        if (!isReadonly) {
          track(target, TrackOpTypes.GET, key)
        }
    
        // 如果是浅观察,不做递归转化,就是说对象有属性值还是对象的话不递归调用 reactive()
        if (shallow) {
          return res
        }
    
        // 如果get的结果是ref
        if (isRef(res)) {
          // ref unwrapping - does not apply for Array + integer key.
          // 返回 ref.value,数组除外
          const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
          return shouldUnwrap ? res.value : res
        }
    
        // 由于 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
      }
    }

    看着长,最终就是track()依赖收集

    track()依赖收集内容过多,和trigger()触发更新一起,单开一篇文章

    /**
     * 拦截对象的设置属性操作
     * @param shallow 是否是浅观察
     * @returns 
     */
    function createSetter(shallow = false) {
      /**
       * @param target 目标对象
       * @param key 设置的属性名称
       * @param value 要改变的属性值
       * @param receiver 如果遇到setter,receiver则为setter调用时的this值
       */
      return function set(
        target: object,
        key: string | symbol,
        value: unknown,
        receiver: object
      ): boolean {
        let oldValue = (target as any)[key]
        // 如果模式不是浅观察模式
        if (!shallow) {
          // 拿新值和老值的原始值,因为新传入的值可能是响应式数据,如果直接和 target 上原始值比较是没有意义的
          value = toRaw(value)
          oldValue = toRaw(oldValue)
          // 目标对象不是数组,旧值是ref,新值不是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
        }
        // 检查对象是否有这个属性
        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
        // reactive是proxy实例才触发更新,防止通过原型链触发拦截器触发更新
        if (target === toRaw(receiver)) {
          if (!hadKey) {
            // 如果不存在则trigger ADD
            trigger(target, TriggerOpTypes.ADD, key, value)
          } else if (hasChanged(value, oldValue)) {
            // 如果新旧值不相等则trigger SET
            trigger(target, TriggerOpTypes.SET, key, value, oldValue)
          }
        }
        return result
      }
    }

    trigger()触发更新

    ref

    源码地址:packages/reactivity/src/ref.ts

    接收一个可选unknown,接着直接调用createRef()

    export function ref(value?: unknown) {
      return createRef(value, false)
    }

    1.png

    ref的区别就是在调用createRef()时第二个值传的是true

    export function shallowRef(value?: unknown) {
      return createRef(value, true)
    }

    看一下官方文档上对shallowRef的解释

    2.png

    createRef

    通过isRef()判断是否是ref数据,是则直接返回该数据,不是则通过new RefImpl创建ref数据

    在创建时会传两个值一个是rawValue(原始值),一个是shallow(是否是浅观察),具体使用场景可看上面refshallowRef的介绍

    function createRef(rawValue: unknown, shallow: boolean) {
      // 是否是 ref 数据
      if (isRef(rawValue)) {
        return rawValue
      }
      return new RefImpl(rawValue, shallow)
    }

    通过__v_isRef只读属性判断是否是ref数据,此属性会在RefImpl创建ref数据时添加

    export function isRef(r: any): r is Ref {
      return Boolean(r && r.__v_isRef === true)
    }

    RefImpl

    class RefImpl<T> {
      private _value: T
      private _rawValue: T
    
      public dep?: Dep = undefined
      // 只读属性 __v_isRef 判断是否是ref数据的静态标识
      public readonly __v_isRef = true
    
      constructor(value: T, public readonly _shallow: boolean) {
        this._rawValue = _shallow ? value : toRaw(value)  // 非浅观察用toRaw()包裹原始值
        this._value = _shallow ? value : toReactive(value) // 非浅观察用toReactive()处理数据
      }
    
      get value() {
      // 依赖收集
        trackRefValue(this)
        return this._value
      }
    
      set value(newVal) {
        newVal = this._shallow ? newVal : toRaw(newVal) // 非浅观察用toRaw()包裹值
        // 两个值不相等
        if (hasChanged(newVal, this._rawValue)) {
          this._rawValue = newVal
          this._value = this._shallow ? newVal : toReactive(newVal)
          triggerRefValue(this, newVal) // 触发依赖,派发更新
        }
      }
    }

    根据RefImpl我们可以看到ref的底层逻辑,如果是对象确实会使用reactive进行处理,并且ref的创建使用的也是RefImpl class实例,value只是RefImpl的属性

    在我们访问设置 ref的value值时,也分别是通过getset拦截进行依赖收集派发更新

    我们来看一下toReactive()这个方法,在RefImpl中创建ref数据时会调用toReactive()方法,这里会先判断传进来的值是不是对象,如果是就用reactive()包裹,否则就返回其本身

    export const toReactive = <T extends unknown>(value: T): T =>
      isObject(value) ? reactive(value) : value

    ref的依赖收集方法

    export function trackRefValue(ref: RefBase<any>) {
      if (isTracking()) {
        ref = toRaw(ref)
        if (!ref.dep) {
          ref.dep = createDep()
        }
        if (__DEV__) {
          trackEffects(ref.dep, {
            target: ref,
            type: TrackOpTypes.GET,
            key: 'value'
          })
        } else {
          trackEffects(ref.dep)
        }
      }
    }

    ref的派发更新方法

    export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
      ref = toRaw(ref)
      if (ref.dep) {
        if (__DEV__) {
          triggerEffects(ref.dep, {
            target: ref,
            type: TriggerOpTypes.SET,
            key: 'value',
            newValue: newVal
          })
        } else {
          triggerEffects(ref.dep)
        }
      }
    }

    总结

    看完reactiveref源码,相信对本文一开始的几个问题也都有了答案,这里也总结了几个问题:

    答:ref底层会通过 new RefImpl()来创造ref数据,在new RefImpl()会首先给数据添加__v_isRef只读属性用来标识ref数据。而后判断传入的值是否是对象,如果是对象则使用toReactive()处理成reactive,并将值赋给RefImpl()value属性上。在访问设置ref数据的value时会分别触发依赖收集派发更新流程。


    答:RefImpl中非浅观察会调用toReactive()方法处理数据,toReactive()中会先判断传入的值是不是一个对象,如果是对象则使用reactive进行处理,不是则直接返回值本身。


    答: 因为vue3响应式方案使用的是proxy,而proxy的代理目标必须是非原始值,没有任何方式能去拦截对原始值的操作,所以就需要一层对象作为包裹,间接实现原始值的响应式方案。


    答:这是因为要解决响应式丢失的问题,举个例子:

    // obj是响应式数据
    const obj = reactive({ foo: 1, bar: 2 })
    
    // newObj 对象下具有与 obj对象同名的属性,并且每个属性值都是一个对象
    // 该对象具有一个访问器属性 value,当读取 value的值时,其实读取的是 obj 对象下相应的属性值 
    const newObj = {
        foo: {
            get value() {
                return obj.foo
            }
        },
        bar: {
            get value() {
                return obj.bar
            }
        }
    }
    
    effect(() => {
        // 在副作用函数内通过新对象 newObj 读取 foo 的属性值
        console.log(newObj.foo)
    })
    // 正常触发响应
    obj.foo = 100

    可以看到,在现在的newObj对象下,具有与obj对象同名的属性,而且每个属性的值都是一个对象,例如foo 属性的值是:

    {
        get value() {
            return obj.foo
        }
    }

    该对象有一个访问器属性value,当读取value的值时,最终读取的是响应式数据obj下的同名属性值。也就是说,当在副作用函数内读取newObj.foo时,等价于间接读取了obj.foo的值。这样响应式数据就能够与副作用函数建立响应联系

    (学习视频分享:web前端开发编程基础视频

    以上就是详解vue3中reactive和ref的区别(源码解析)的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除

    前端(VUE)零基础到就业课程:点击学习

    清晰的学习路线+老师随时辅导答疑

    快捷开发Web应用及小程序:点击使用

    支持亿级表,高并发,自动生成可视化后台。

    专题推荐:ref reactive vue3
    上一篇:组件间怎么通信?盘点Vue组件通信方式(值得收藏) 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • ❤️‍🔥共22门课程,总价3725元,会员免费学• ❤️‍🔥接口自动化测试不想写代码?• 20+个实用的 Vue 组件库,快来收藏!• Vue组件间怎么通信?组件通信的几种方式• 尤雨溪:新版Vue3中文文档上线了!• 深入剖析vue2.x中diff算法的原理• 看看这3道必问面试题,检验你的Vue掌握程度!• 聊聊vite+vue3.0+ts中如何封装axios?
    1/1

    PHP中文网