likes
comments
collection
share

从 Vue3.x 源码上深入理解响应式原理

作者站长头像
站长
· 阅读数 15

什么是数据驱动视图?

了解 Vue 的小伙伴都知道,Vue 中有一种数据驱动视图的概念

拿一段简单的代码说明一下,例如

<template>
  <span>{{text}}</span>
</template>

<script setup>
import { ref } from 'vue'
const text = ref(1)
setTimeout(() => {
  text.value = 2
}, 1000)
</script>

该效果:从 1 变为 2

而我自始至终只是改变text的值,浏览器渲染出来的视图就跟着变化,这就是数据驱动视图

什么是响应式?

那 Vue 中,是如何实现的呢?

就是响应式

那什么是响应式呢?

以上述例子说明,Vue 在内部封装一个渲染方法 render,初始化时调用 render,而由于 text 初始值 为 1 ,所以渲染出来就是 1 ,只不过 setTimeout 定时器里面的函数把 text 的值变为了 2,此时 Vue 就是再次调用 render 更新成了 2

其,改变 text 值从1 变为 2,触发了响应式,刷新了视图

反过来说,由于 text 是一个具有响应式的对象,所以更新 text 的值才能触发视图更新

如何实现响应式?

那响应式在 Vue 是如何实现的呢?

以 ref 来说明一下(本文基于 Vue 3.2.47 进行分析)

收集依赖

初始化

从 Vue3.x 源码上深入理解响应式原理

从调用堆栈(Call Stack)能看出来应该是符合预期的

从 Vue3.x 源码上深入理解响应式原理

其中关键代码:ref -> createRef -> new RefImpl

// 代码在 /core-3.2.47/packages/reactivity/src/ref.ts 第 82 行
export function ref(value?: unknown) {
  return createRef(value, false)
}

// 代码在 /core-3.2.47/packages/reactivity/src/ref.ts 第 99 行
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}


// 代码在 /core-3.2.47/packages/reactivity/src/ref.ts 第 106 行
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) {
    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)
    }
  }
}

mountComponent 执行完 setupComponent 后将会执行 setupRenderEffect

// 代码在 /core-3.2.47/packages/runtime-core/src/renderer.ts 第 1182 行
const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
  
    // 省略部分代码...
    if (!(__COMPAT__ && compatMountInstance)) {
      if (__DEV__) {
        startMeasure(instance, `init`)
      }
      setupComponent(instance)
      if (__DEV__) {
        endMeasure(instance, `init`)
      }
    }
    
    // 省略部分代码...
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )
  }

从 Vue3.x 源码上深入理解响应式原理

关键是:如何生成副作用代码,添加到响应式数据中?

从 Vue3.x 源码上深入理解响应式原理

// 代码在 /core-3.2.47/packages/runtime-core/src/renderer.ts 第 1292 行
const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
  // 省略部分代码...
  // 代码在 /core-3.2.47/packages/runtime-core/src/renderer.ts 第 1545 行
  // 创建响应式数据和更新视图的依赖关系
  // create reactive effect for rendering
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(update),
      instance.scope // track it in component's effect scope
    ))

    const update: SchedulerJob = (instance.update = () => effect.run())
  // 省略部分代码...    
}  

关键就是使用 ReactiveEffectactiveEffect

只需要关注两点即可:

  • ReactiveEffect 成员变量 deps(收集依赖)、fn(副作用函数)、scheduler(调度器)
  • ReactiveEffect 中的 run 方法会将 this 赋值给activeEffect
// 代码在 /core-3.2.47/packages/reactivity/src/effect.ts 第 48 行
export let activeEffect: ReactiveEffect | undefined

// 代码在 /core-3.2.47/packages/reactivity/src/effect.ts 第 53 行
export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined

  /**
   * Can be attached after creation
   * @internal
   */
  computed?: ComputedRefImpl<T>
  /**
   * @internal
   */
  allowRecurse?: boolean
  /**
   * @internal
   */
  private deferStop?: boolean

  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope)
  }

  run() {
    if (!this.active) {
      return this.fn()
    }
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    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

      if (this.deferStop) {
        this.stop()
      }
    }
  }

  stop() {
    // stopped while running itself - defer the cleanup
    if (activeEffect === this) {
      this.deferStop = true
    } else if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

之后就是主动触发 get value

从 Vue3.x 源码上深入理解响应式原理

trackRefValue -> trackEffects

其中由 createDep 生成 set 传入 trackEffects

此时的 set 赋值给 RefImpldep

activeEffect( ReactiveEffect 的 this )添加到 Set

从 Vue3.x 源码上深入理解响应式原理

ps:此时的 depset 类型

// 代码在 /core-3.2.47/packages/reactivity/src/ref.ts 第 40 行
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()))
    }
  }
}

// 代码在 /core-3.2.47/packages/reactivity/src/dep.ts 第 21 行
export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set<ReactiveEffect>(effects) as Dep
  dep.w = 0
  dep.n = 0
  return dep
}

// 代码在 /core-3.2.47/packages/reactivity/src/effect.ts 第 232 行
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!
      })
    }
  }
}

从 Vue3.x 源码上深入理解响应式原理

好了,这就是收集依赖的过程。

派发更新

派发更新,相对与收集依赖会简单很多

当定时器 setTimeout 中的 text.value = 2时,将会 set value

从 Vue3.x 源码上深入理解响应式原理

从 Vue3.x 源码上深入理解响应式原理

// 代码在 /core-3.2.47/packages/reactivity/src/effect.ts 第 347 行
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)
    }
  }
}

// 代码在 /core-3.2.47/packages/reactivity/src/effect.ts 第 365 行
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()
    }
  }
}

triggerEffects 触发的便是 RefImpl 中的 dep,即 ReactiveEffect set 集合

从 Vue3.x 源码上深入理解响应式原理

ReactiveEffect 中有调度器 scheduler 就优先执行调度器,没有就使用 run 方法(运行fn

从 Vue3.x 源码上深入理解响应式原理

总结

所谓的响应式,说白了,就是先进行收集,收集当前的数据会影响什么方法,或者说收集当前的数据需要再次触发什么方法?当数据改变时进行派发,将之前的收集的方法运行一遍即可。

同理,在Vue中,他把改变视图的方法作为响应式数据对象的副作用,当改变数据时会触发改变视图的方法

感谢阅读