preface

This analysis refers to vuE3 source, Cui Da’s Mini-Vue, Huo Chunyang’s “Vuejs Design and implementation” as far as possible to record my VUE3 source reading learning process. I will combine my own thinking, put forward questions, find answers, and attach them to the bottom of each article. I hope everyone can learn and make progress together in my article. If you get something, can you give the author a small praise as encouragement? Thank you!

Handwriting easy vue3 Refs correlation & Computed correlation implementation

Refs

We know that proxies are used to represent objects, so there is no way to convert primitive values (Boolean, Number, BigInt, String, Symbol, undefined, null, etc.) to reactive values. So now we have ref

Note: the ref is different from the ref we used in VUe2

The preliminary implementation

The definition of ref is as follows:

We need to have a ref method that accepts a value, this value

  1. It’s an object, and you’re going to make it reactive
  2. Is the original value, so return a reactive ref object

The ref object has a value attribute, which means that the ref. Value can be retrieved from the ref

Let’s look at the test case:

it('happy path'.() = > {
  // Const is used because it is an object
  // Create a ref object with value 1
  const a = ref(1)
  // Access a.value he should get 1
  expect(a.value).toBe(1)});Copy the code

The condition should be clear enough that we can create an instance of Ref directly:

class RefImpl {
  private _value: any
  constructor(value) {
    // If value is an object, it needs to be reactive
    this._value = convert(value)
  }
  // Accessing the value attribute returns the _value directly
  get value() {
    return this._value
  }
}

// Check whether the new value is an object or not
function convert(value) {
  // isObject = return value! == null && typeof value === 'object'
  return isObject(value) ? reactive(value) : value
}

export function ref(value) {
  // Call the ref method to generate an instance directly
  return new RefImpl(value)
}
Copy the code

After the above implementation we just implemented a ref object that can return a value with.value, but it is not reactive yet

The REF object implements the reactive form

Next we will implement the ref object responsivity, which must mean that we trigger a change to the.value that will have other effects

We can simply externalize the functionality we want to implement with the following test cases:

it('should be reactive'.() = > {
  // Create a ref object with value 1
  const a = ref(1)
  let dummy
  let calls = 0
  // create a side effect function that will execute itself once, dummy should be equal to 1 and calls + 1 since the function is called
  effect(() = > {
    calls++
    dummy = a.value
  })
  expect(calls).toBe(1)
  expect(dummy).toBe(1)
  // a.value = set value
  a.value = 2
  // The side effect function should be triggered by trigger, and both variables will be updated
  expect(calls).toBe(2)
  expect(dummy).toBe(2)
  // Optimization: Assignment should not trigger the response as before
  a.value = 2
  expect(calls).toBe(2)
  expect(dummy).toBe(2)});Copy the code

Let’s summarize what we need to pay attention to through the above test case.

  1. A. value = 2 should trigger the set value of the ref instance
  2. Set value checks to see if it is a new value
  3. The set Value needs to trigger for the side effect function to execute
  4. If there is trigger, track is definitely needed to collect dependencies

So let’s first process the original track and trigger for ref and get the following two functions:

export function trackEffects(dep) {
  // There is no need to add in deP
  if (dep.has(activeEffect)) return
  dep.add(activeEffect) // Add the corresponding effect instance to set
  activeEffect.deps.push(dep)
}
export function triggerEffects(dep) {
  for (const effect of dep) {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}
Copy the code

If you are careful, you can find that the code in these two methods is exactly the same as that in the original tarck and trigger. There is no change. We just separate these two pieces of code and reuse them.

Our two new methods accept only one dependent DEP (set type)

Add a trackRefValue method to class RefImp:

class RefImpl {
  // Keep the old value, need to compare the new value of the set, because _value can be a responsive object, so it cannot be compared with _value
  private _rawValue: any
  private _value: any
  // Collect dependencies in track and trigger dependencies in trigger
  public dep
  constructor(value) {
    this._rawValue = value
    // If value is an object, it needs to be reactive
    this._value = convert(value)
    // Initialize Set
    this.dep = new Set()}get value() {
    // Collect dependencies, but determine if there is an effect before doing so
    trackRefValue(this)
    return this._value
  }
  set value(newValue) {
    // Semantically obejct.js means return without changes
    if(! hasChanged(this._rawValue, newValue)) return
    // The value must be changed before the trigger is called to ensure that _value is correct
    this._rawValue = newValue
    this._value = convert(newValue)
    triggerEffects(this.dep)
  }
}
// ref get depends on collection
function trackRefValue(ref) {
  // isTraking is shouldTrack && activeEffect! == undefined
  if (isTracking()) trackEffects(ref.dep)
}
Copy the code

With the above implementation, we also incidentally implemented the responsivity for objects, which we can now directly pass through the following test cases

it('should make nested properties reactive'.() = > {
  const a = ref({
    count: 1
  })
  let dummy
  // call the side effect function to assign the value a.value.count to dummy
  effect(() = > {
    dummy = a.value.count
  })
  expect(dummy).toBe(1)
  // Changing a.view.count can also trigger reactive because this._value is actually created by reactive()
  a.value.count = 2
  expect(dummy).toBe(2)});Copy the code

IsRef and unRef (off ref)

IsReactive, isReadonly, isRef, isReactive, isReactive, isReadonly, isRef

Definition of isRef: Checks whether the current value is a ref object

The implementation method is very simple:

class RefImpl {
  // ...
  // Define a __v_isRef attribute
  public __v_isRef = true
  // constructor() ....
}

// check if it is a ref
export function isRef(ref) {
  // If the object passed is not a ref object, it has no __v_isRef attribute. Convert it to a Boolean type
  return!!!!! ref.__v_isRef }Copy the code

UnRef: also known as unRef, one of its functions is actually to return the value of a ref object when it is passed in, or if it is not a ref object it is returned.

What are the application scenarios of unRef (unRef)? Why design this thing?

// if ref returns ref.value, if not, return ref.value
export function unRef(ref) {
  return isRef(ref) ? ref.value : ref
}
Copy the code

As you can see, the implementation of the unRef is actually quite simple, using the hot isRef above.

ProxyRefs (automatic de-ref)

After we implement the above content, we find that we need.value every time to get the corresponding value, which is really troublesome, it is impossible to write template also let me.value!! Also too pit!! And then we have ProxyRefs

Let’s start with a test case to see what we need to implement:

it('proxyRefs'.() = > {
  // Create a user whose age field is the ref object
  const user = {
    age: ref(18),
    name: "Ben"
  }
  // Create a proxyUser using a proxyRefs method
  const proxyUser = proxyRefs(user)
  // Does not affect the use of user.age.value
  expect(user.age.value).toBe(18)
  // proxyUser can get values directly from.age instead of.value
  expect(proxyUser.age).toBe(18)
  expect(proxyUser.name).toBe("Ben")
  // set proxyuser. age to the original value that is not ref
  proxyUser.age = 20
  // The age of user is still the ref object, which can get updates from.value
  expect(user.age.value).toBe(20)
  // The updated age is still accessible
  expect(proxyUser.age).toBe(20)
  // set proxyuser. age to a ref object
  proxyUser.age = ref(25)
  // The age of user is still the ref object, which can get updates from.value
  expect(user.age.value).toBe(25)
  // The updated age is still accessible
  expect(proxyUser.age).toBe(25)});Copy the code

From the above test case, it is not difficult to find that the object obtained by ProxyRefs method does not need to obtain the value through.value, and changing the type of the object’s ref object attribute does not affect.

Let’s write ProxyRefs:

export function proxyRefs(objectWithRefs) {
  return new Proxy(objectWithRefs, {
    // When get is triggered we unref it
    get(target, key) {
      return unRef(Reflect.get(target, key))
    },
    set(target, key, newValue) {
      // If the current value is ref and the value passed is not ref, then.value is assigned
      if(isRef(target[key]) && ! isRef(newValue)) {return target[key].value = newValue
      } else {
        // Otherwise, set it to newValue
        return Reflect.set(target, key, newValue)
      }
    }
  })
}
Copy the code

In fact, we have implemented the unRef function to do the unRef function, it should be noted that the set may be a little round, we combined with the test case digest it

toRefs

On second thought, we also want to implement an incoming object where all the attributes become ref objects. How do we do that?

It is easy to iterate over the key of the object and ref by ref:

export function toRefs(obj) {
  const ret = {}
  / / for... In traverses the object
  for (const key of obj) {
    ret[key] = ref(obj, key)
  }
  return ret
}
Copy the code

That’s it!! Isn’t it very simple!!

Computed

Next comes the implementation of computed tomography, which we are curious about!! We know that computed is cacheable, and it should be lazy.

Let’s use the test case to see what we need to do:

it('should compute lazily'.() = > {
  // lazy execution (I will not trigger cvalue. value without calling cvalue. value)
  const value = reactive({
    foo: 1
  })
  const getter = jest.fn(() = > {
    return value.foo
  })
  // Create a computed instance
  const cValue = computed(getter)
  // Lazy getters should not be called
  expect(getter).not.toHaveBeenCalled()
  // Cvalue. value is triggered once
  expect(cValue.value).toBe(1)
  expect(getter).toHaveBeenCalledTimes(1)
  // Accessing this property should not trigger the getter cache again!!
  cValue.value
  expect(getter).toHaveBeenCalledTimes(1)
  // Attribute modification needs to trigger
  value.foo = 2
  expect(getter).toHaveBeenCalledTimes(1)
  // Accessing this property should not trigger getter or cache again!!
  cValue.value
  expect(getter).toHaveBeenCalledTimes(2)});Copy the code

This test case illustrates two characteristics of computed well:

  1. Lazy to perform
  2. The cache

Next we implement computed:

class ComputedRefImpl {
  private _getter: any
  // If the cache variable is true, the value needs to be updated
  private _dirty: boolean = true 
  private _value: any
  // Save the ReactiveEffect instance
  private _effect: any
  constructor(getter) {
    this._getter = getter
    // To take advantage of the second scheduler argument, only the scheduler is executed when the trigger is triggered after a getter is executed
    this._effect = new ReactiveEffect(getter, () = > {
      // Change dirty to true when it is false to indicate that it has been updated
      if (!this._dirty) this._dirty = true})}get value() {
    // if dirty is not true, value is not changed
    if (this._dirty) {
      this._dirty = false
      // effect.run() actually fires the getter passed in
      this._value = this._effect.run()
    }
    return this._value
  }
}
// Returns a ComputedRefImpl instance
export function computed(getter) {
  return new ComputedRefImpl(getter)
}
Copy the code

Now that we have computed, we need to pay attention to two things:

  1. We use the dirty flag to determine whether data is updated
  2. This is where the Effect scheduler parameter becomes particularly clever, meaning that computed is also the focus of scheduler

Question: Are ref in Vue2 and ref in Vue3 the same thing? What’s the difference?

The ref in Vue2 is an attribute that marks an element/child component, so you can use $refs in the parent component to find the element/child component.

<! -- 'vm.$refs.p'
<p ref="p">hello</p>

<! -- 'vm.$refs.child' you get an instance of chld-Component -->
<child-component ref="child"></child-component>
Copy the code

With Vue3 we can still use ref in this way. (As an attribute, get the reference with $refs)

But Vue3 also has a ref. The ref is actually a method carried by a reactive API called Refs that uses ref(XXX) to make XXX (the raw value) a REF object with reactive functionality. (If XXX were an object, reactive would still be called to become a proxy object.)

The ref object has only one.value property

Here’s how we use it:

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

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

Question: What are the application scenarios of unRef (unRef)? Why design this thing? What is automatic deref?

Because ref objects can only access the corresponding value through ref.value, this imposes a serious mental burden on us

When we use reactive objects generated by Reactive:

const obj = reactive({ foo: 1.bar: 2 })
obj.foo / / 1
obj.bar / / 2
Copy the code

And when we use the reactive data generated by ref:

const a = ref(1)
a.value / / 1

constb = {... toRefs({foo: 1.bar: 2 })}
b.value.foo / / 1
b.value.bar / / 2
Copy the code

Then you’re obviously feeling a lot of pressure. What? Not enough stress??

That…

<! -- Normal -->
<p>{{ foo }} / {{ bar }}</p>
<! -- ref -->
<p>{{ foo.value }} / {{ bar.value }}</p>
Copy the code

How about now? !!!!!

Therefore, the purpose of deref is to reduce the mental burden of users.

That’s why we have automatic deref, right

With the ability to automatically unref, users do not need to depend on whether a value is ref when using reactive data in templates.