Vue3源码解析之 ref

3,623 阅读5分钟

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue3 源码解析系列第 3 篇,关注专栏

前言

我们知道 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

可以看出,如果传入的数据为对象类型则调用 reactive 方法,这块逻辑可参考 Vue3源码解析之 reactive ,否则就直接返回当前值,此时 ref 执行完毕,当前 name 被赋值为 jc

ref.png

我们回过来再看下 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)

看下输出结果:

newRef.png

当我们执行 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 方法,即传入的匿名函数,从而触发赋值操作,此时整个依赖触发的过程完成。

ref-set.png

总结

  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 源码解析系列

  1. Vue3源码解析之 源码调试
  2. Vue3源码解析之 reactive
  3. Vue3源码解析之 ref
  4. Vue3源码解析之 computed
  5. Vue3源码解析之 watch
  6. Vue3源码解析之 runtime
  7. Vue3源码解析之 h
  8. Vue3源码解析之 render(一)
  9. Vue3源码解析之 render(二)
  10. Vue3源码解析之 render(三)
  11. Vue3源码解析之 render(四)
  12. Vue3源码解析之 render component(一)
  13. Vue3源码解析之 render component(二)
  14. Vue3源码解析之 render component(三)
  15. Vue3源码解析之 render component(四)
  16. Vue3源码解析之 render component(五)
  17. Vue3源码解析之 diff(一)
  18. Vue3源码解析之 diff(二)
  19. Vue3源码解析之 compiler(一)
  20. Vue3源码解析之 compiler(二)
  21. Vue3源码解析之 compiler(三)
  22. Vue3源码解析之 createApp