vue3 ref实现原理

vue3 ref实现原理

我们都知道在vue中实现响应式有:1、reactive;2、ref。在上一篇文章中,我们了解了vue3响应式实现和reactive的工作原理,这篇文章中我们来看看ref是怎样实现响应式的。

我们知道,对于 reactive 函数而言,它会把传入的 object 作为 proxy 的 target 参数,而对于 proxy 而言,他只能代理对象,而不能代理简单数据类型,所以说:我们不可以使用 reactive 函数,构建简单数据类型的响应性。reactive 只能用于对象类型(包括对象、数组以及 Map 和 Set 等集合类型),不能用于原始类型如 string、number 或 boolean。下面我们从源码来看看ref是怎么运行的。

ref源码

这里测试ref创建对象类型的响应式数据

我们从Vue中导入ref方法来测试一下,下面是测试代码:

<!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>
</body>
<script>
  const { ref, effect} = Vue
  // 这里测试ref创建对象类型的响应式数据
  const obj = ref({
    name: '张三'
  })
  effect(()=> {
    document.querySelector('#app').innerHTML = obj.value.name
  })
  setTimeout(()=>{
    obj.value.name = '李四'
  },2000)
</script>
</html>

首先是ref方法,它接收一个value,通过createRef创建一个RefImpl类型的实例

export function ref(value?: unknown) {
  return createRef(value, false)
}
​
function createRef(rawValue: unknown, shallow: boolean) {
  // isRef通过判断__v_isRef属性来判断当前传入的rawValue是否已经创建过,如果创建过就直接返回就信
  if (isRef(rawValue)) {
    return rawValue
  }
  // 创建RefImpl类实例
  return new RefImpl(rawValue, shallow)
}

下面来看看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)
    // 从上面的代码来看__v_isShallow是false,所以执行的是toReactive。该方法将对象类型数据通过reactive方法创建响应式对象,将普通类型数据直接返回赋值给this._value
    this._value = __v_isShallow ? value : toReactive(value)
  }
​
  get value() {
    trackRefValue(this)
    return this._value
  }
​
  set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

RelImpl类的构造函数中,执行了一个 toReactive 的方法,传入了 value 并把返回值赋值给了 this._value,那么我们来看看 toReactive 的作用:

// 判断当前传入创建的参数是否是对象,如果是对象就通过reaction()方法来创建响应式对象,如果不是对象类型的数据就直接返回原值。
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

由以上代码ref创建对象类型数据逻辑可知:

  1. 对于 ref 而言,主要生成了 RefImpl 的实例

  2. 在构造函数中对传入的数据进行了处理:复杂数据类型:转为响应性的 proxy 实例,简单数据类型:不去处理。

  3. RefImpl 分别提供了get value()set value() 以此来完成对 gettersetter 的监听,注意这里并没有使用 proxy

ref 函数执行完成之后,测试实例开始执行 effect 函数。effect 函数我们上一篇文章说过它的执行流程,我们知道整个 effect 主要做了3 件事情:

  1. 生成 ReactiveEffect 实例

  2. 触发 fn 方法,从而激活 getter

  3. 建立了 targetMapactiveEffect 之间的联系

    1. dep.add(activeEffect)

    2. activeEffect.deps.push(dep)

通过以上可知,effect 中会触发 fn 函数,也就是说会执行 obj.value.name ,那么根据 get value() 机制,此时会触发 RefImplget value() 方法。

我们来看看get value()的执行:

get value() {
    // 触发trackRefValue方法,收集依赖
    trackRefValue(this)
    return this._value
}
​
export function trackRefValue(ref: RefBase<any>) {
  // 触发 trackEffects 函数,并且在此时为 ref 新增了一个 dep 属性, trackEffects 主要的作用就是:收集所有的依赖 
  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()))
    }
  }
}

至此 get value()执行完毕,然后就是执行:

setTimeout(()=>{
  obj.value.name = '李四'
},2000)

这里执行时可以看成:

const value = obj.value
value.name = '李四'

所以在这里并不会触发RefImplset value(),而是触发get value()

get value() 函数中:

  1. 再次执行 trackRefValue 函数: 但是此时 activeEffectundefined,所以不会执行后续逻辑

  2. 返回 this._value:通过 构造函数,我们可知,此时的 this._value 是经过 toReactive 函数过滤之后的数据,在当前实例中为 proxy 实例。

  3. get value() 执行完成。

总结:

  1. 对于 ref 函数,会返回 RefImpl 类型的实例

  2. 在该实例中,会根据传入的数据类型进行分开处理

    1. 复杂数据类型:转化为 reactive 返回的 proxy 实例

    2. 简单数据类型:不做处理

  1. 无论我们执行 obj.value.name 还是 obj.value.name = xxx 本质上都是触发了 get value()

  2. 之所以会进行 响应性 是因为 obj.value 是一个 reactive 函数生成的 proxy

下面测试ref创建简单数据类型的响应式数据

下面是测试代码:

<script>
  const { ref, effect} = Vue
  const obj = ref('张三')
  effect(()=> {
    document.querySelector('#app').innerHTML = obj.value
  })
  setTimeout(()=>{
    obj.value = '李四'
  },2000)
</script>

代码运行过程中整个 ref 初始化的流程与之前完全相同,但是有一个不同的地方,需要 特别注意:因为当前不是复杂数据类型,所以在 toReactive 函数中,不会通过 reactive 函数处理 value。所以 this._value 不是 一个 proxy

接着是effect函数执行:整个 effect 函数的流程与之前创建对象数据类型响应式相同。effect函数中obj.value会触发get value()这里的流程也和上面相同就略过。

不同的是在简单数据类型中修改obj.value = '李四'时触发的是set value()。这里也是 ref 可以监听到简单数据类型响应性的关键。

set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    // 比较新值与旧值是否发生变化,如果发生变化就触发依赖 triggerRefValue。
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, newVal)
    }
  }
​
// 在triggerRefValue里又触发了triggerEffects去触发依赖,完成响应性(triggerEffects在《vue3响应式实现》中看过)
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)
    }
  }
}
​
export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  // 执行 computed 依赖
  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))
    }
    // 最主要的是以下代码,这里是执行依赖函数:如果 effect 是一个调度器,就会执行 scheduler
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      // 否则直接执行 effect.run()
      effect.run()
    }
  }
}

至此测试代码完成,由以上代码可知:

  1. 简单数据类型的响应性,不是基于 proxyObject.defineProperty 进行实现的,而是通过:set value() 语法,将对象属性绑定到查询该属性时将被调用的函数 上,使其触发 xxx.value = '李四' 属性时,其实是调用了 set value('李四') 函数。

  2. value 函数中,触发依赖

所以,我们可以说:对于 ref 标记的简单数据类型而言,它其实 “并不具备响应性”,所谓的响应性只不过是因为我们 主动触发了 value 方法 而已。

  • 23
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值