vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > vue3响应式原理之Ref

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

作者:石头山_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 类的实现

定义了四个变量

在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的方案截然不同了。

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

您可能感兴趣的文章:
阅读全文