The difference between ref() and reactive() in Vue

Foreword

You must know the responsive programming in Vue, which provides the ability to automatically update the UI when the data changes, abandoning the traditional way of manually updating the UI when the data is updated. Before Vue 3.0, the data we defined in the data function would be automatically converted to responsive. In the Composition API, there are two ways for us to define reactive objects: ref() and reactive(). But how do they differ?

The difference between ref and reactive

Before comparing, let’s take a look at how to use them. Their usage methods are very simple and similar:

<template>
  <div>{<!-- -->{<!-- -->user.first_name}} {<!-- -->{<!-- -->user.last_name}}</div>
  <div>{<!-- -->{<!-- --> age }}</div>
</template>
<script>
import {<!-- --> reactive } from 'vue'
export default {<!-- -->
  setup() {<!-- -->
    const age = ref(18)
    const user = reactive({<!-- -->
      first_name: "Karl",
      last_name: "Max",
    })
    return {<!-- --> user , age}
  }
}
</script>

Next, let’s analyze their differences:

Acceptable raw data types are different

Both ref() and reactive() receive an ordinary raw data and convert it into a responsive object, such as user and age. But the difference is: ref can handle basic data types and objects at the same time, while reactive can only handle objects and support basic data types.

const numberRef = ref(0); // OK
const objectRef = ref({ count: 0 }) // OK

//TS2345: Argument of type 'number' is not assignable to parameter of type 'object'.
const numberReactive = reactive(0);
const objectReactive = reactive({ count: 0}); // OK

This is because the two responsive data implementations are different:

  • ref holds data through an intermediate object RefImpl, and realizes data hijacking by rewriting its set and get methods. In essence, it still uses Object.defineProperty to value attribute of code>RefImpl is hijacked.
  • reactive is hijacked through Proxy. Proxy cannot operate on basic data types, which makes reactive helpless when faced with basic data types.

The source code corresponding to ref is as follows:

export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {<!-- -->
  return createRef(value, false)
}

function createRef(rawValue: unknown, shallow: boolean) {<!-- -->
  if (isRef(rawValue)) {<!-- -->
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {<!-- -->
  private_value: T
  private_rawValue: T


  constructor(value: T, public readonly __v_isShallow: boolean) {<!-- -->
    this._rawValue = __v_isShallow? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }

 /**
   * Rewrite the get and set methods,
   * Essentially via Object.defineProperty
   * Hijack the attribute value */
  get value() {<!-- -->
    // collect dependencies
    track()
    return this._value
  }

  set value(newVal) {<!-- -->
    if (hasChanged(newVal, this._rawValue)) {<!-- -->
      this._value = newVal
      //trigger dependencies
      trigger()
    }
  }
}

The reactive code after deletion and integration is as follows:

export function reactive(target: object) {<!-- -->
  return createReactiveObject(
    target,
    false
  )
}

function createReactiveObject(
  target: Target,
  isReadonly: boolean
) {<!-- -->
  const proxy = new Proxy(
    target,
    {<!-- -->
      get(target, key) {<!-- -->
// collect dependencies
        track()
        return target[propKey]
      },
      set(target, key, value) {<!-- -->
        target[propKey] = value
        //trigger dependencies
        trigger()
      }
    }
  )
  return proxy
}

Details about Object.defineProperty, Proxy and data hijacking can be found in: Vue3 data hijacking optimization.

Summary: ref can store basic data types but reactive cannot

The return value type is different

Run the following code:

const count1 = ref(0)
const count2 = reactive({count:0})
console.log(count1)
console.log(count2)

The output is:

RefImpl?{<!-- -->__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 0, _value: 0}
Proxy(Object)?{<!-- -->count: 0}

ref() returns an instance of RefImpl that holds the original data. The type returned by reactive() is the proxy Proxy instance of the original data

Therefore, when defining data types, there is a slight difference:

interface Count {<!-- -->
  num:number
}
const countRef:Ref<number> = ref(0)
const countReactive: Count = reactive({<!-- -->num:1})

In addition, if there is a responsive object in reactive, it will be automatically expanded, so the following code is correct:

const countReactiveRef: Count = reactive({<!-- -->num:ref(2)})

Since ref returns a RefImpl instance, and reactive returns a proxy, its type itself is the type of the incoming target object. Therefore, the former can hold dependencies by itself, while the latter uses the global object targetMap to manage dependencies.

The ref source code logic is as follows:

class RefImpl<T> {<!-- -->
  private_value: T
  private_rawValue: T

//Hold dependencies through dep
  public dep?: Dep = undefined

  constructor(value: T, public readonly __v_isShallow: boolean) {<!-- -->}

  get value() {<!-- -->
trackEffects(this.dep)
}

  set value(newVal) {<!-- -->}
}

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {<!-- -->
//The main job of dependency collection is to place dependencies in dep:
   dep. add(activeEffect!)
   activeEffect!.deps.push(dep)
}

And reactive uses a global weak reference Map to store dependencies:

function createGetter(isReadonly = false, shallow = false) {<!-- -->
    return function get(target: Target, key: string | symbol, receiver: object) {<!-- -->
        track(target, TrackOpTypes. GET, key)
    }
}

const targetMap = new WeakMap<any, KeyToDepMap>()
export function track(target: object, type: TrackOpTypes, key: unknown) {<!-- -->
  if (shouldTrack & amp; & amp; activeEffect) {<!-- -->
    let depsMap = targetMap. get(target)
    //If the targetMap does not store the data of the current object
    //Create a new depsMap and put it into targetMap
    if (!depsMap) {<!-- -->
      targetMap.set(target, (depsMap = new Map()))
    }
    //If depsMap does not store the data of the current attribute
    //Then create a new dep and put it in depsMap
    let dep = depsMap. get(key)
    if (!dep) {<!-- -->
      depsMap.set(key, (dep = createDep()))
    }
    //The logic here is the same as ref
    trackEffects(dep, eventInfo)
  }
}

Conclusion: ref(value: T) returns the Ref type, while reactive(object: T) returns the proxy of T type, which also leads to the former being able to rely on Its own properties manage dependencies, while the latter manages dependencies with the help of global variablestargetMap.

Different ways of accessing data

Different types of return values will result in different data access methods. It can be seen from the above:

  • ref() returns an instance object of RefImpl, which holds the original data through the _value private variable and rewrites value The get method. Therefore, when you want to access the original object, you need to trigger the get function to get the data through xxx.value. Similarly, when modifying data, the set function should also be triggered by means of xxx.value = yyy.
  • reactive() returns the proxy of the original object, the proxy object has the same properties as the original object, so we can directly access the data through .xxx

The response is as follows in code:

const objectRef = ref({ count: 0 });
const refCount = objectRef. value. count;

const objectReactive = reactive({ count: 0});
const reactiveCount = objectReactive.count;

Summary: ref needs to access data indirectly through the value attribute (vue is automatically expanded in templates, .value can be omitted), while reactive can be accessed directly.

Mutability of primitive objects is different

ref holds the original data through a RefImpl instance, and then uses the .value attribute to access and update. For an instance, its attribute value can be modified. Therefore, you can redistribute data for ref by means of .value, without worrying about the instance of RefImpl being changed and destroying the responsive style:

const count = ref({count:1})
console.log(count.value.count)
//Modify the original value
count.value = {count:3}
console.log(count.value.count)
//Modify the original value
count.value = {name:"Karl"}
console.log(count.value.count)
console.log(count.value.name)

//The output is as follows:
//1
//3
//undefined
//karl

And reactive returns the proxy of the original object, so the object cannot be reassigned to it, and the attribute value can only be modified through attribute access, otherwise the responsiveness will be destroyed:

let objectReactive = reactive({<!-- --> count: 0})
effect(() => {<!-- -->
  console.log(`Data changed: ${<!-- -->objectReactive.count}`)
})
//You can modify the value normally
objectReactive.count = 1
objectReactive.count = 2
// After modifying objectReactive, the effect will no longer receive notifications of data changes
objectReactive = {<!-- -->count:3}
objectReactive.count = 4
console.log("End")
//The output is as follows:
//Data changed: 0
//Data changed: 1
//Data changed: 2
//it's over

The reason is simple: the effect function listens to the proxy objectReactive of the original value { count: 0}, when the data is modified through the proxy , which can trigger the callback. But when the program runs to objectReactive = {count:3}, the point of objectReactive is no longer the agent of {count: 0}, Instead, it points to the new object {count:3}. At this time, objectReactive.count = 4 modifies no longer the proxy object monitored by effect, but a new ordinary non-responsive object { count: 3}. effect will not be able to monitor data changes, and the responsiveness of objectReactive will be destroyed as a result.

If you directly modify the point of ref, the responsiveness of ref will also be invalid:

let count = ref(0)
effect(() => {
  console.log(`Data changed: ${count.value}`)
})
count.value = 1
count = ref(0)
//effect will not listen to changes here
count.value = 2
console.log("End")

Conclusion: The value of ref can be reassigned to a new object, while reactive can only modify the properties of the current proxy

ref uses reactive to realize in-depth monitoring of Object type data

Combined with the source code of RefImpl above:

constructor(value: T, public readonly __v_isShallow: boolean) {<!-- -->
    this._rawValue = __v_isShallow? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
}

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

When ref finds that the original object being monitored is of Object type, it will convert the original object into reactive and assign it to the _value attribute. At this time, ref.value returns not the original object, but its proxy.

Verify with the following code:

const refCount = ref({count:0})
console.log(refCount.value)
//Output result:
//Proxy(Object)?{count: 0}

Conclusion: “ref() wraps the original data through reactive and then assigns it to _value when the original data is in Object type.

Different effects on listening properties

Execute the following code:

let refCount = ref({count:0})
watch(refCount,() => {
     console.log(`refCount data changed`)
})
refCount. value = {count: 1}
//Output result:
//refCount data changed

watch() can detect changes in ref.value. However, continue to execute the following code

let refCount = ref({count:0})
watch(refCount,() => {
    console.log(`refCount data changed`)
})
refCount.value.count = 1

let reactiveCount = reactive({count:0})
watch(reactiveCount,() => {
    console.log(`reactiveCount data changed`)
})
reactiveCount.count = 1
// output result
//reactiveCount data has changed

This time watch() did not monitor the data changes of refCountwatch() does not observe ref deeply by default. If watch observes ref deeply, you need to modify the parameters as follows:

watch(refCount, () => {<!-- -->
  console.log('reactiveCount data has changed!')
}, {<!-- --> deep: true })

For reactive, no matter whether you declare deep: true or not, watch will observe deeply.

Conclusion: watch() by default only monitors the changes of ref.value, but performs deep monitoring on reactive.

Summary and Usage

  1. ref can store primitive types, but reactive cannot.
  2. ref needs to access data via .value, while reactive() can be used directly as a regular object.
  3. A new object can be reassigned to the value property of a ref, while reactive() cannot.
  4. ref is of type Ref, and the reactive type returned by reactive is the original type itself.
  5. Based on the fourth article, ref can manage dependencies by itself while reactive uses global variables to manage dependencies in the form of key-value pairs.
  6. By default, watch only observes the value of ref, and implements deep monitoring for reactive.
  7. ref defaults to deep response transformations with primitive values of the reactive object type.

Usage habits: Although there is no rule stipulating when to use ref or reactive, or a mixture of them. These all depend on the developer’s programming habits. But in order to keep the code consistent and readable, I tend to use ref instead of reactive.