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 objectRefImpl
, and realizes data hijacking by rewriting its set and get methods. In essence, it still uses Object.defineProperty tovalue attribute of code>RefImpl is hijacked. reactive
is hijacked through Proxy. Proxy cannot operate on basic data types, which makesreactive
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 ofRefImpl
, which holds the original data through the _value private variable and rewritesvalue
The get method. Therefore, when you want to access the original object, you need to trigger the get function to get the data throughxxx.value
. Similarly, when modifying data, the set function should also be triggered by means ofxxx.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 refCount
—watch()
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
ref
can store primitive types, butreactive
cannot.ref
needs to access data via.value
, whilereactive()
can be used directly as a regular object.- A new object can be reassigned to the
value
property of aref
, whilereactive()
cannot. ref
is of typeRef
, and the reactive type returned byreactive
is the original type itself.- Based on the fourth article,
ref
can manage dependencies by itself whilereactive
uses global variables to manage dependencies in the form of key-value pairs. - By default,
watch
only observes thevalue
ofref
, and implements deep monitoring forreactive
. ref
defaults to deep response transformations with primitive values of thereactive
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
.