vue3响应式原理之Ref用法及说明
作者:石头山_S
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的方案截然不同了。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。