likes
comments
collection
share

Vue3源码解析之 ref

作者站长头像
站长
· 阅读数 24

前言

我们知道 Vue3 中声明响应式是通过 reactiveref 这两个函数,上篇我们分析了 reactive 的实现原理,接下来我们再来看下 ref 是如何实现的。

案例

首先引入 refeffect 两个函数,之后声明 name 响应式数据,接着又执行 effect 函数,该函数传入了一个匿名函数,最后两秒后又修改 name 值。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      const { ref, effect } = Vue
      
      const name = ref('jc')
      
      effect(() => {
        document.querySelector('#app').innerHTML = name.value
      })

      setTimeout(() => {
        name.value = 'cc'
      }, 2000)
    </script>
  </body>
</html>

ref 实现

ref 函数定义在 packages/reactivity/src/ref.ts 文件下:

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)
}

ref 函数实际执行的是 createRef 方法,而该方法实际是返回了一个 RefImpl 构造函数的实例对象:

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {
    // 记录原始值
    this._rawValue = __v_isShallow ? value : toRaw(value)
    // 操作值
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    // 依赖收集
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    newVal = this.__v_isShallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this.__v_isShallow ? newVal : toReactive(newVal)
      // 依赖触发 
      triggerRefValue(this, newVal)
    }
  }
}

RefImpl 构造函数会接收传入的值,可能是基本类型也可能是复杂类型,通过 _rawValue 记录原始值,用于之后依赖触发时新旧值的比较,我们需关注 this._value = __v_isShallow ? value : toReactive(value)toReactive 函数被定义在 packages/reactivity/src/reactive.ts 中:

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

Vue3源码解析之 ref

我们回过来再看下 RefImpl 构造函数中还有 get value()set value() 这两个方法,那它们具体有什么用?举个例子:

// RefImpl 构造函数
class RefImpl {
    // 实例的 getter 行为: ref.value
    get value() {
      return 'get value'
    }
    // 实例的 setter 行为: ref.value = xxx
    set value(newVal) {
      console.log('set value')
    }
}
const newRef = new RefImpl()
console.log(newRef)

看下输出结果:

Vue3源码解析之 ref

当我们执行 newRef.value 时会触发 getter,而修改值时会触发 setter这也是为什么我们赋值或者修改 ref 值时,需要加上 .value

另外我们还需知道,对于基本类型的数据 ref 是不具备数据监听的,当赋值或修改值时主动触发了 getset 方法。

之后执行 effect 函数(该原理可查看上篇),传入一个匿名函数,接着执行赋值行为触发 get 方法:

get value() {
    // 依赖收集
    trackRefValue(this)
    return this._value
}

get 方法核心 trackRefValue(this) 实际触发了 trackRefValue 方法进行数据的依赖收集,该方法定义在 packages/reactivity/src/effect.ts 文件中:

export function trackRefValue(ref: RefBase<any>) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    if (__DEV__) {
      trackEffects(ref.dep || (ref.dep = createDep()), {
        target: ref,
        type: TrackOpTypes.GET,
        key: 'value'
      })
    } else {
      trackEffects(ref.dep || (ref.dep = createDep()))
    }
  }
}

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit // set newly tracked
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack({
        effect: activeEffect!,
        ...debuggerEventExtraInfo!
      })
    }
  }
}

这块逻辑同 reactive,给指定属性绑定对应的 fn,目的是 dep 对象与 ReactiveEffect 相关联,完成整个依赖收集的过程。之后两秒后进行修改值触发 set 方法:

set value(newVal) {
    newVal = this.__v_isShallow ? newVal : toRaw(newVal)
    // 新旧值比较
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this.__v_isShallow ? newVal : toReactive(newVal)
      // 依赖触发 
      triggerRefValue(this, newVal)
    }
}

set 方法和兴 triggerRefValue(this, newVal) 进行依赖触发:

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref)
  if (ref.dep) {
    if (__DEV__) {
      triggerEffects(ref.dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: newVal
      })
    } else {
      triggerEffects(ref.dep)
    }
  }
}

triggerRefValue 方法实际执行了 triggerEffects,该方法定义在packages/reactivity/src/effect.ts 文件中:

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

上篇 reactive 我们也分析了这块逻辑,最终执行的是每个 effect.run 方法,即传入的匿名函数,从而触发赋值操作,此时整个依赖触发的过程完成。

Vue3源码解析之 ref

总结

  1. ref 函数本质上做了三件事:一是返回 RefImpl 的实例;二是对数据处理,如果当前数据为基本类型,则直接返回;如果为复杂类型,则调用 reactive 返回 reactive 数据;三是 RefImpl 提供 get valueset value 方法,这就是为什么设置 ref 值时,需要带上 .value
  2. ref 基本类型的数据不具备数据监听,赋值或修改值都是主动触发 getset 方法。
  3. 为什么 ref 类型数据,必须要通过 .value 访问值呢? a. 因为 ref 需要处理基本数据类型的响应性,但是对于基本类型数据而言,它无法通过 proxy 建立代理。 b. 而 vue 通过 get value()set value() 定义了两个属性函数,通过主动触发这两个函数(属性调用)的形式来进行依赖收集和依赖触发。 c. 所以我们必须通过 .value 来保证响应性。

Vue3 源码实现

vue-next-mini

Vue3 源码解析系列