4
头图

Among the newly launched responsive APIs in Vue3, the Ref series is undoubtedly one of the most frequently used APIs, and the calculated attribute is an option that is very familiar in the previous version, but it also provides independent in Vue3 The api makes it easy for us to directly create calculated values. In today's article, the author will explain to you the realization principle of ref and computed, let's start the study of this chapter together.

ref

When we have an independent primitive value, such as a string, we can create an object when we want it to become reactive, put this string in the object as a key-value pair, and then pass it to reactive. And Vue provides us with an easier way to do it through ref.

import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

ref will return a mutable responsive object, which maintains its internal value as a responsive reference. This is the source of the ref name. The object contains only a property named value.

And how is ref achieved?

The source code location of ref is in the library of @vue/reactivity, and the path is packages/reactivity/src/ref.ts. Next, let's look at the implementation of ref together.

export function ref<T extends object>(value: T): ToRef<T>
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)
}

From the function signature of ref api, we can see that the ref function receives a value of any type as its value parameter and returns a value of type Ref.

export interface Ref<T = any> {
  value: T
  [RefSymbol]: true
  _shallow?: boolean
}

It can be seen from the type definition of the return value Ref that the return value of ref has a value attribute, a private symbol key, and a _shallow boolean attribute that identifies whether it is shallowRef.

The function body directly returns the return value of the createRef function.

createRef

function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

The implementation of createRef is also very simple. The input parameters are rawValue and shallow, rawValue records the original value of the creation ref, and shallow is a shallow responsive api that indicates whether it is shallowRef.

The logic of the function is to first use isRef to determine whether it is rawValue, and if so, return the ref object directly.

Otherwise, it returns a newly created instance object of the RefImpl class.

RefImpl class

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, public readonly _shallow: boolean) {
    // 如果是 shallow 浅层响应,则直接将 _value 置为 _rawValue,否则通过 convert 处理 _rawValue
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    // 读取 value 前,先通过 track 收集 value 依赖
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    // 如果需要更新
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      // 更新 _rawValue 与 _value
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      // 通过 trigger 派发 value 更新
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

In the RefImpl class, there is a private variable _value used to store the latest value of ref; the public read-only variable __v_isRef is used to identify that the object is a ref reactive object. The tag is the same as the ReactiveFlag when talking about the reactive api. .

In the constructor of RefImpl, a private _rawValue variable is accepted to store the old value of ref; the public _shallow variable distinguishes whether it is a shallow response. Inside the constructor, first judge whether _shallow is true, if it is shallowRef, directly assign the original value to _value, otherwise, it will be converted and then assigned by convert.

Inside the conver function, it is actually to determine whether the passed parameter is an object, if it is an object, create a proxy object through the reactive api and return, otherwise directly return the original parameter.

When we read the ref value in the form of ref.value, the value getter method will be triggered. In the getter, the value dependency of the ref object will be collected through the track first, and the ref value will be returned after the collection is completed.

When we modify ref.value, the setter method of value will be triggered, which will compare the old and new values. If the values are different and need to be updated, the old and new values will be updated first, and then the update of the value attribute of the ref object will be dispatched through the trigger. Let side-effect functions that depend on the ref perform the update.

If you have friends who rely on track collection and the trigger distribution update is confused, I suggest you read my previous article . In the previous article, the author explained this process carefully. So far, the author will explain the realization of ref to everyone clearly. Up.

computed

In the document, the computed api is introduced as follows: accept a getter function, and return an immutable reactive ref object with the return value of the getter function. Or it can also use an object with get and set functions to create a writable ref object.

computed function

According to the description of this api, it is obvious that computed accepts a function or object type parameter, so let's start with its function signature.

export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
)

In the overload of the computed function, the first line of code receives the parameter of the getter type and returns the function signature of the ComputedRef type. This is the first case described in the document. It accepts the getter function and returns an immutable with the return value of the getter function. The responsive ref object.

In the second line of code, the computed function accepts an options object and returns a writable ComputedRef type, which is the second case of the document, creating a writable ref object.

The third line of code is the most general case of this function overloading, and the parameter name has already mentioned this point: getterOrOptions.

Let's take a look at the related type definitions in the computed api:

export interface ComputedRef<T = any> extends WritableComputedRef<T> {
  readonly value: T
}

export interface WritableComputedRef<T> extends Ref<T> {
  readonly effect: ReactiveEffect<T>
}

export type ComputedGetter<T> = (ctx?: any) => T
export type ComputedSetter<T> = (v: T) => void

export interface WritableComputedOptions<T> {
  get: ComputedGetter<T>
  set: ComputedSetter<T>
}

Knowing from the type definition: WritableComputedRef and ComputedRef are both extended from the Ref type, which also understands why the document says that computed returns a reactive object of type ref.

Next, take a look at the complete logic in the function body of the computed api:

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  // 如果 参数 getterOrOptions 是一个函数
  if (isFunction(getterOrOptions)) {
       // 那么这个函数必然就是 getter,将函数赋值给 getter
    getter = getterOrOptions
    // 这种场景下如果在 DEV 环境下访问 setter 则报出警告
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    // 这个判断里,说明参数是一个 options,则取 get、set 赋值即可
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  
  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any
}

In the computed api, it will first determine whether the passed parameter is a getter function or an options object. If it is a function, then this function can only be a getter function. No doubt, assigning the getter at this time and accessing the setter in the DEV environment will not succeed. , And a warning will be reported at the same time. If the input is a function, computed will treat it as an object with get and set properties, and assign the get and set in the object to the corresponding getter and setter. Finally, after the processing is completed, an instance object of the ComputedRefImpl class will be returned, and the computed api will be processed.

ComputedRefImpl class

This class is similar to the RefImpl Class we introduced earlier, but the logic in the constructor is a bit different.

First look at the member variables in the class:

class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true

  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean
}

Compared with the RefImpl class, the _dirty private member variable is added, an effect read-only side effect function variable, and a __v_isReadonly tag is added.

Then look at the logic in the constructor:

constructor(
  getter: ComputedGetter<T>,
  private readonly _setter: ComputedSetter<T>,
  isReadonly: boolean
) {
  this.effect = effect(getter, {
    lazy: true,
    scheduler: () => {
      if (!this._dirty) {
        this._dirty = true
        trigger(toRaw(this), TriggerOpTypes.SET, 'value')
      }
    }
  })

  this[ReactiveFlags.IS_READONLY] = isReadonly
}

In the constructor, a side effect function is created for the getter, and the side effect option is set to delayed execution, and a scheduler is added. The scheduler will determine whether the this._dirty tag is false, if so, set this._dirty to true, and use the trigger to dispatch the update. If you are confused about the timing of the execution of this side effect and when the scheduler will execute these problems in the side effect, it is recommended to read the previous article , first understand the effect side effects, and then understand other responsive APIs. It's a multiplier.

get value() {
  // 这个 computed ref 有可能是被其他代理对象包裹的
  const self = toRaw(this)
  if (self._dirty) {
    // getter 时执行副作用函数,派发更新,这样能更新依赖的值
    self._value = this.effect()
    self._dirty = false
  }
  // 调用 track 收集依赖
  track(self, TrackOpTypes.GET, 'value')
  // 返回最新的值
  return self._value
}

set value(newValue: T) {
  // 执行 setter 函数
  this._setter(newValue)
}

In computed, when the value is obtained through the getter function, the side-effect function will be executed first, and the return value of the side-effect function will be assigned to _value, and the value of _dirty will be assigned to false, which can ensure that if the dependency in the computed does not occur If it changes, the side-effect function will not be executed again, so the _dirty obtained in the getter is always false, and there is no need to execute the side-effect function again, saving overhead. After that, the dependencies are collected through the track, and the value of _value is returned.

In the setter, just execute the setter logic we passed in, so far the implementation of the computed api has been explained.

to sum up

In this article, based on the above knowledge points of side-effect functions and dependent collection and distribution updates, the author explains for you the implementation of the two most commonly used apis in Vue3 responsiveness, ref and computed. These two apis are both created at the time of creation. A class instance is returned, and the constructor in the instance and the get and set set to the value attribute complete the reactive tracking.

When we learn to use these at the same time, and know why it must be able to help us play its greatest role in using these apis, and at the same time, it can also allow you to quickly write some code that does not meet your expectations. If you locate the problem, you can figure out whether it is written by yourself or the api itself does not support a certain calling method.

Finally, if this article can help you understand the implementation principles of responsive api ref and computed in Vue3, I hope I can give this article a little like ❤️. If you want to continue to follow up the follow-up articles, you can also follow my account or follow my github , thank you again for the lovely officials.


Originalix
165 声望63 粉丝

前端工程师,欢迎来 github 互相 follow, originlaix