Welcome to CoderStan’s handwritten Mini-Vue3 column and join me in writing your own Mini-Vue3. Ref and computed in the ReActivity module will be implemented simply in this chapter. (Thanks to Atri CXR mini-Vue)

If you feel good, please support it. If you want to see other parts of the article, you can pay attention to me or pay attention to my handwritten mini-vue3 column. If you want to see the annotated source code, welcome to visit GitHub warehouse, and please also point a star to support it.

3. Realize the reactivity

3.11 implementationref

Check the responsiveness API section of the Vue3 API documentation for an introduction to REF.

Takes an internal value and returns a reactive and mutable REF object. The ref object has a single property.value pointing to an internal value.

Example:

const count = ref(0)
console.log(count.value) / / 0

count.value++
console.log(count.value) / / 1
Copy the code

If you assign an object to a REF value, you make it highly responsive by using reactive functions.

Type declaration:

interface Ref<T> {
  value: T
}

function ref<T> (value: T) :Ref<T>
Copy the code

Sometimes we may need to specify complex types for the inner values of ref. To do this concisely, we can pass a generic parameter when calling ref to override the default inference:

const foo = ref<string | number> ('foo') / / foo types: Ref < string | number >

foo.value = 123 // ok!
Copy the code

If the type of the generic is unknown, it is recommended to convert ref

to ref

:

function useState<State extends string> (initial: State) {
  const state = ref(initial) as Ref<State> // state.value -> State extends string
  return state
}
Copy the code

① Implement the most basicref

To implement ref, first create the ref test file ref.spec.ts in the SRC /reactivity/__tests__ directory and add the following test code:

describe('reactivity/ref'.() = > {
  it('should hold a value'.() = > {
    // Create the ref object
    const a = ref(1)
    // The value property of the ref object is equal to the value passed in
    expect(a.value).toBe(1)
    // The value property of the ref object is mutable
    a.value = 2
    expect(a.value).toBe(2)
  })

  it('should be reactive'.() = > {
    const a = ref(1)
    let dummy
    let calls = 0
    effect(() = > {
      calls++
      dummy = a.value
    })
    expect(calls).toBe(1)
    expect(dummy).toBe(1)
    // ref objects are reactive
    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
    // The set of the value property of the ref object is cached and does not trigger dependencies repeatedly
    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)})})Copy the code

To pass the above test, create a ref.ts file in the SRC /reactivity/ SRC directory, implement an incomplete ref and export it. During the implementation process, use the ref interface implementation class to encapsulate the operation:

// Interface of the ref object
interface Ref {
  value
}

// The implementation class of the Ref interface that encapsulates the operation
class RefImpl {
  private _value

  constructor(value) {
    // Assign the value passed to the instance's private property _value
    this._value = value
  }

  // Get of value property returns the value of the private property _value
  get value() {
    // TODO collects dependencies

    // Returns the value of the instance's private property _value
    return this._value
  }

  // Set of value property modifies the private property _value value
  set value(newVal) {
    // TODO triggers dependencies

    // Process the value of set and assign the result to the instance's private property _value
    this._value = newVal
  }
}

export function ref(value) :Ref {
  // Return an instance of the RefImpl class, the ref object
  return new RefImpl(value)
}
Copy the code

This enables an incomplete REF, that is, the ability to convert the passed value into a REF object. Then extract the track and trigger functions from the effect.ts file in the SRC /reactivity/ SRC directory and export isTracking, trackEffects, and triggerEffects functions:

export function track(target, key) {
  // Return the dependency if it should not be collected
  if(! isTracking()) {return
  }

  /* Other code */

  trackEffects(dep)
}

// Used to determine whether dependencies should be collected
export function isTracking() {
  returnshouldTrack && activeEffect ! = =undefined
}

// Used to add the currently executing instance of the ReactiveEffect class to the DEP and deP to the dePS property of the currently executing instance of the ReactiveEffect class
export function trackEffects(dep) {
  if(dep.has(activeEffect!) ) {return} dep.add(activeEffect!) activeEffect? .deps.push(dep) }export function trigger(target, key) {
  /* Other code */

  triggerEffects(dep)
}

// A scheduler or run method that iterates through the DEP, calling each instance of the ReactiveEffect class
export function triggerEffects(dep) {
  for (const reactiveEffect of dep) {
    if (reactiveEffect.scheduler) {
      reactiveEffect.scheduler()
    } else {
      reactiveEffect.run()
    }
  }
}
Copy the code

After that, create a private property DEP in the RefImpl class to hold the dependencies associated with the current ref object, collect the dependencies in the get of the value property, and trigger the dependencies in the set:

class RefImpl {
  private _value
  // Used to store dependencies associated with the current ref object
  private dep

  constructor(value) {
    this._value = value
    this.dep = new Set()}get value() {
    if (isTracking()) {
      // Collect dependencies
      trackEffects(this.dep)
    }
    return this._value
  }

  set value(newVal) {
    // If the value of set is the same as before, it is returned directly
    if(! hasChanged(newVal,this._value)) {
      return
    }

    this._value = newVal
    // Trigger dependencies
    triggerEffects(this.dep)
  }
}
Copy the code

Run the yarn test ref command to run the test of ref. You can see that the test passes. This completes the basic implementation of ref.

(2) perfectref

If the value passed in is an object, you need to use Reactive to transform that object.

Add the following test code to ref’s test file ref.spec.ts:

describe('reactivity/ref'.() = > {
  /* Other test code */

  it('should make nested properties reactive'.() = > {
    const a = ref({
      count: 1
    })
    let dummy
    effect(() = > {
      dummy = a.value.count
    })
    expect(dummy).toBe(1)
    // The value property of the ref object is a responsive object
    a.value.count = 2
    expect(dummy).toBe(2)})})Copy the code

To pass the above tests, the ref implementation needs to be refined. First, implement and export the toReactive function in the reactivity. ts file in the SRC /reactivity/ SRC directory:

// It is used to process values, and reactive is used to process objects, otherwise it is returned directly
export const toReactive = value= > (isObject(value) ? reactive(value) : value)
Copy the code

Then add a private property _rawValue to the RefImpl class to hold the value passed in and the value of the set, and use toReactive to process the value before assigning it to the instance’s private property _value:

class RefImpl {
  // Used to hold the value passed in and the value of set
  private _rawValue
  private _value
  private dep

  constructor(value) {
    // Assign the value passed to the instance's private property _rawValue
    this._rawValue = value
    // Process the value passed in and assign the result to the instance's private property _value
    this._value = toReactive(value)
    this.dep = new Set()}get value() {
    if (isTracking()) {
      trackEffects(this.dep)
    }

    return this._value
  }

  set value(newVal) {
    // If the value of set is different from the previous value, modify and trigger the dependency
    if (hasChanged(newVal, this._rawValue)) {
      // Assign the value of set to the instance's private property _rawValue
      this._rawValue = newVal
      // Process the value of set and assign the result to the instance's private property _value
      this._value = toReactive(newVal)
      // Trigger dependencies
      triggerEffects(this.dep)
    }
  }
}
Copy the code

Run the yarn test ref command to run the test of ref and see that the test passes, which further improves the implementation of ref.

3.12 implementationisRefandunRef

Check the responsiveness API section of the Vue3 API documentation for descriptions of isRef and unRef.

isRef

Check if the value is a ref object.

unref

If the argument is a ref, the internal value is returned, otherwise the argument itself is returned. This is val = isRef(val), right? Val. value: The syntactic sugar function of val.

function useFoo(x: number | Ref<number>) {
  const unwrapped = unref(x) // Unwrapped must now be a numeric type
}
Copy the code

Before implementing isRef and unRef, first add isRef and unRef test code to ref. Spec. ts test file:

describe('reactivity/ref'.() = > {
  it('isRef'.() = > {
    expect(isRef(ref(1))).toBe(true)
    expect(isRef(reactive({ foo: 1 }))).toBe(false)
    expect(isRef(0)).toBe(false)
    expect(isRef({ bar: 0 })).toBe(false)
  })

  it('unref'.() = > {
    expect(unref(1)).toBe(1)
    expect(unref(ref(1))).toBe(1)})})Copy the code

To pass the above test, add a common property __v_isRef to the RefImpl class to indicate that the instance is a REF object, and then implement isRef and unRef in the RefImpl /reactivity/ SRC file refref.

class RefImpl {
  // Used to hold the value passed in and the value of set
  private _rawValue
  private _value
  // Used to store dependencies associated with the current ref object
  private dep
  // Is used to indicate that the instance is a ref object
  public __v_isRef = true
}

// Determine if a value is a ref object
export function isRef(value) :boolean {
  return!!!!! value.__v_isRef }// Get the value of the ref object's value property
export function unref(ref) {
  return isRef(ref) ? ref.value : ref
}
Copy the code

Run the yarn test ref command to run the test of ref and see that the test passes. In this way, isRef and unRef are implemented.

3.13 implementationproxyRefsfunction

The proxyRefs function takes an object as an argument and returns an instance of Proxy that proxies the get and set of the object. If a property of the object is a ref object, You can directly obtain the incoming value of the REF object by obtaining the corresponding property value of proxy, and directly modify the corresponding property value of proxy to modify the incoming value of the REF object or replace the REF object.

Ref = ref. Spec. ts proxyRefs = ref.

describe('reactivity/ref'.() = > {
  it('proxyRefs'.() = > {
    const obj = {
      foo: ref(1),
      bar: 'baz'
    }
    const proxyObj = proxyRefs(obj)
    expect(proxyObj.foo).toBe(1)
    expect(proxyObj.bar).toBe('baz')

    proxyObj.foo = 2
    expect(proxyObj.foo).toBe(2)

    proxyObj.foo = ref(3)
    expect(proxyObj.foo).toBe(3)})})Copy the code

To pass the above test, implement and export the proxyRefs function in the ref.ts file under the SRC /reactivity/ SRC directory.

export function proxyRefs(objectWithRefs) {
  // Return the instance of Proxy
  return new Proxy(objectWithRefs, {
    // Proxies get and set of the property of the passed object
    get: function (target, key) {
      // Get the property value of the passed object and call unref to process it
      return unref(Reflect.get(target, key))
    },
    set: function (target, key, value) {
      const oldValue = target[key]
      // If the property of the object passed in is a ref object and the value of set is not a ref object, change the value of the ref object, otherwise change the value of the property directly
      if(isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true
      } else {
        return Reflect.set(target, key, value)
      }
    }
  })
}
Copy the code

Run the yarn test ref command to test ref and see that the test passes.

3.14 implementationcomputed

Check the responsive API section of the Vue3 API documentation for an introduction to computed.

Takes a getter function and returns an immutable reactive ref object based on the return value of the getter.

const count = ref(1)
const plusOne = computed(() = > count.value + 1)

console.log(plusOne.value) / / 2

plusOne.value++ / / error
Copy the code

Alternatively, you can take an object with get and set functions to create a writable ref object.

const count = ref(1)
const plusOne = computed({
  get: () = > count.value + 1.set: val= > {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) / / 0
Copy the code

Type declaration:

/ / read-only
function computed<T> (getter: () => T, debuggerOptions? : DebuggerOptions) :Readonly<Ref<Readonly<T>>> // writablefunction computed<T> (
  options: {
    get: () => T
    set: (value: T) => void}, debuggerOptions? : DebuggerOptions) :Ref<T>
interface DebuggerOptions {
  onTrack? : (event: DebuggerEvent) = >void
  onTrigger? : (event: DebuggerEvent) = >void
}
interface DebuggerEvent {
  effect: ReactiveEffect
  target: any
  type: OperationTypes
  key: string | symbol | undefined
}
Copy the code

① Implement the most basiccomputed

Before implementing computed, create a test file for computed, computed.spec.ts, in the SRC /reactivity/__tests__ directory, and add the following test code:

describe('reactivity/computed'.() = > {
  it('should return updated value'.() = > {
    const value = reactive({ foo: 1 })
    // Take a getter to create a read-only responsive ref object,
    const cValue = computed(() = > value.foo)
    expect(cValue.value).toBe(1)
    value.foo = 2
    expect(cValue.value).toBe(2)})})Copy the code

To pass the above test, create a computed. Ts file in the SRC /reactivity/ SRC directory, implement and export a basic computed object, and use the implementation class of the Ref interface to encapsulate the operation during implementation. At the same time, the ReactiveEffect class removed from the implementation of Effect is used, so we need to export the ReactiveEffect class in the effect.ts file under the SRC /reactivity/ SRC directory:

// effect.ts
export class ReactiveEffect {
  /* Implement */
}

// computed.ts
// The implementation class of the Ref interface
class ComputedImpl {
  // Save an instance of the ReactiveEffect class
  private _effect: ReactiveEffect

  constructor(getter) {
    Create an instance of the ReactiveEffect class using the getter function
    this._effect = new ReactiveEffect(getter)
  }

  // Get of value property returns the return value of the run method that called the private property _effect, that is, the return value of the getter function
  get value() {
    return this._effect.run()
  }
}

export function computed(getter) {
  // Return an instance of the RefImpl class, the ref object
  return new ComputedImpl(getter)
}
Copy the code

Run the YARN test computed command to run a test for computed, and you can see that the test passes, thus completing the most basic implementation of computed.

(2) perfectcomputed

Computed lazily executes getters, and the get of the value property of a responsive ref object is cached.

Add the following test code in computed’s test file, comput.spec.ts:

describe('reactivity/computed'.() = > {
  it('should compute lazily'.() = > {
    const value = reactive({ foo: 1 })
    const getter = jest.fn(() = > value.foo)
    const cValue = computed(getter)

    // The getter is executed when the value of the ref object's value property is obtained
    expect(getter).not.toHaveBeenCalled()
    expect(cValue.value).toBe(1)
    expect(getter).toHaveBeenCalledTimes(1)
    // If the property value of the dependent responsive object is not updated, then fetching the value of the ref object's value property again does not repeat the getter
    cValue.value
    expect(getter).toHaveBeenCalledTimes(1)
    Getters are not executed when modifying the property value of a dependent reactive object
    value.foo = 1
    expect(getter).toHaveBeenCalledTimes(1)

    / / in response to rely on the property of the objects of type values are updated, for ref value the value of the property of the object again getter
    expect(cValue.value).toBe(1)
    expect(getter).toHaveBeenCalledTimes(2)
    cValue.value
    expect(getter).toHaveBeenCalledTimes(2)})})Copy the code

To pass these tests, the implementation of computed needs to be improved.

class ComputedImpl {
  private _effect: ReactiveEffect
  // Save the result of the getter function
  private _value
  // This is used to record whether caching is not used
  private _dirty = true

  constructor(getter) {
    // Create an instance of the ReactiveEffect class using a getter function and a method
    this._effect = new ReactiveEffect(
      getter,
      // To turn off the cache
      () = > {
        this._dirty = true})}// Get of value property returns the return value of the run method that called the private property _effect, that is, the return value of the getter function
  get value() {
    if (this._dirty) {
      // Call the run method of an instance of the ReactiveEffect class, that is, execute the getter function and assign the result to the _value property
      this._value = this._effect.run()
      this._dirty = false
    }

    return this._value
  }
}
Copy the code

Execute the YARN test computed command to run a test for computed and see that the test passes, further perfecting the implementation of computed.