vue3响应式原理之Ref用法及说明

 更新时间:2022年12月03日 11:12:21   作者:石头山_S  
这篇文章主要介绍了vue3响应式原理之Ref用法及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

theme: fancy

一. Ref 用法

这是 ref 最基本的用法,返回来的count是一个响应式的代理值

const count = ref(0)

二. 实现

1. ref 函数

我们调用的ref函数,传进来一个 val 值,调用 createRef 函数,我们来看下该函数的实现

源码路径:packages/reactivity/src/ref.ts

function ref(value?: unknown) {
  return createRef(value, false)
}

2. createRef 函数

该函数里边做了一个判断,该值如果已经是一个Ref,则直接返回,否则创建一个 Ref 实例,让我们看下 RefImpl 类的实现

源码路径:packages/reactivity/src/ref.ts

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

3. RefImpl 类的实现

定义了四个变量

  • _value: 用来保存加工后实现响应化的值
  • _rawValue: 用来保存当前未经加工过的值
  • dep: 用来收集依赖,是一个 Set类型
  • _v_isRef: 用来标识该值是否经过 ref 加工

在constructor中,我们为_rawValue 和_value实现了初始化,分别调用了toRaw和toReactive函数,toReactive将 object类型转换为响应式,所以ref中也可以实现对象的响应式管理,我们将他放在下篇文章中来讲,它是实现对象响应式的主要方法,我们今天只讲基本类型。

源码路径:packages/reactivity/src/ref.ts

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

4. trackRefValue 依赖收集

从上边代码中我们可以看到,我们使用 get 和 set对 value 属性实现了拦截,其实如同Object.defineProperty,在 get中实现依赖收集,set中通知依赖更新,而 vue3中是通过调用trackRefValue来实现跟踪依赖。

在该方法中,调用了trackEffect方法,在收集第一个依赖时执行createDep方法来作为参数传入。

我们接着往下看。

源码路径:packages/reactivity/src/ref.ts

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

5. createDep 创建依赖容器

其实就是创建了一个 Set 类型,之后我们进入到下一步 trackEffects。

源码路径:packages/reactivity/src/dep.ts

export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set<ReactiveEffect>(effects) as Dep
  dep.w = 0
  dep.n = 0
  return dep
}

6. trackEffects

可以看到这里我们将activeEffect 放入的我们的Ref的dep中,也就是Set类型,那这个activeEffect是什么呢?

它其实就是我们的componentUpdateFn,每次更改value值时,通知对应的组件更新。我们来看下activeEffect是如何赋值的。

源码路径:packages/reactivity/src/effect.ts

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (shouldTrack) {
    // 放入我们的依赖集合 
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)

}

三. activeEffect 的赋值

我们在上边说过,activeEffect其实就是componentUpdateFn函数,所以该值应该是一个变化的值,它是如何准确无误的将每个组件更新函数来放入到对应的dep中的呢,我们回到setupRenderEffect函数里边来看一下。

//源码路径 core/packages/runtime-core/src/renderer.ts

  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    const componentUpdateFn = () => { }
    // create reactive effect for rendering
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(instance.update),
      instance.scope // track it in component's effect scope
    ))
    const update = (instance.update = effect.run.bind(effect) as SchedulerJob)

    update.id = instance.uid
    // allowRecurse
    // #1801, #2043 component render effects should allow recursive updates
    toggleRecurse(instance, true)


    update()
  }

从上边源码中我们可以看到,在setRenderEffectFn方法中实现了componentUpdateFn的定义,同时在此时创建了一个ReactiveEfect的对象,同时调用了ReactiveEffect中的run方法,并且使用bind来改变其中的this指向该effect。我们来进一步看下run中发生了什么。

源码路径:packages/reactivity/src/effect.ts

  run() {
    if (!this.active) {
      return this.fn()
    }
    let parent: ReactiveEffect | undefined = activeEffect
  
    try {
      this.parent = activeEffect
      activeEffect = this
      shouldTrack = true

      trackOpBit = 1 << ++effectTrackDepth

      if (effectTrackDepth <= maxMarkerBits) {
        initDepMarkers(this)
      } else {
        cleanupEffect(this)
      }
      return this.fn()
    } finally {
      if (effectTrackDepth <= maxMarkerBits) {
        finalizeDepMarkers(this)
      }

      trackOpBit = 1 << --effectTrackDepth

      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      this.parent = undefined
    }
  }

从上边代码中我们看到,在try中,我们先将this赋值给activeEffect,然后调用传入的componentUpdateFn函数,在该函数中会获取我们定义的ref变量的值,触发get,然后将该this也就是effect保存到该Ref的dep中,这就完成了我们的整个依赖收集的过程。

四. 依赖收集

收集依赖后,下一步就是在数据改变之后通知依赖更新,我们来看下set中是如何做的。

在set中我们调用了triggerRefValue方法,传入了this,也就是当前Ref实例,还有新的值。

源码路径:packages/reactivity/src/ref.ts  

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

1. triggerRefValue

该方法中调用了triggerEffects方法,将ref.dep也就是之前收集依赖的Set,传入。让我们接着往下看。

// 源码路径:packages/reactivity/src/ref.ts

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

2. triggerEffects

该方法中,遍历Set,然后调用每个依赖上的run方法,也就是执行更新函数,进而使页面刷新。实现数据到页面的响应式渲染。

// 源码路径:packages/reactivity/src/effect.ts

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect !== activeEffect || effect.allowRecurse) {
      if (__DEV__ && effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
      }
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        effect.run()
      }
    }
  }
}

五. 总结

vue3 中对Ref的实现基本没有太大改变,也是利用setter和getter对数据实现数据劫持,而Reactive的响应式原理就与vue2的方案截然不同了。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • vue computed计算属性显示undefined的解决

    vue computed计算属性显示undefined的解决

    这篇文章主要介绍了vue computed计算属性显示undefined的解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-11-11
  • vue 项目中使用websocket的正确姿势

    vue 项目中使用websocket的正确姿势

    这篇文章主要介绍了vue 项目中使用websocket的实例代码,通过实例代码给大家介绍了在utils下新建websocket.js文件的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-01-01
  • vue实现弹框遮罩点击其他区域弹框关闭及v-if与v-show的区别介绍

    vue实现弹框遮罩点击其他区域弹框关闭及v-if与v-show的区别介绍

    vue如何简单的实现弹框,遮罩,点击其他区域关闭弹框, 简单的思路是以一个div作为遮罩,这篇文章给大家详细介绍了vue实现弹框遮罩点击其他区域弹框关闭及v-if与v-show的区别介绍,感兴趣的朋友一起看看吧
    2018-09-09
  • vue项目使用高德地图的定位及关键字搜索功能的实例代码(踩坑经验)

    vue项目使用高德地图的定位及关键字搜索功能的实例代码(踩坑经验)

    这篇文章主要介绍了vue项目使用高德地图的定位及关键字搜索功能的实例代码,也是小编踩了无数坑总结出来的经验,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-03-03
  • vue+echarts实现多条折线图

    vue+echarts实现多条折线图

    这篇文章主要为大家详细介绍了vue+echarts实现多条折线图,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • vue.js数据响应式原理解析

    vue.js数据响应式原理解析

    这篇文章主要介绍了vue.js数据响应式原理解析,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的朋友可以参考一下
    2022-08-08
  • VueJS实现用户管理系统

    VueJS实现用户管理系统

    这篇文章主要为大家详细介绍了VueJS实现用户管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-05-05
  • Vue.js之$emit用法案例详解

    Vue.js之$emit用法案例详解

    这篇文章主要介绍了Vue.js之$emit用法案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-09-09
  • vue2利用Bus.js如何实现非父子组件通信详解

    vue2利用Bus.js如何实现非父子组件通信详解

    这篇文章主要给大家介绍了关于vue2利用Bus.js如何实现非父子组件通信的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧。
    2017-08-08
  • Vue实现动态圆环百分比进度条

    Vue实现动态圆环百分比进度条

    这篇文章主要为大家详细介绍了Vue实现动态圆环百分比进度条,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-09-09

最新评论