vue3 源码阅读- ref 创建响应式对象

544 阅读3分钟

如题,这篇文章是我在阅读源码的过程中做的笔记,发布到掘金有两个目的:

  1. 记录自己的学习过程,方便以后查阅;
  2. 通过与掘友们讨论,反查自己对框架实现原理理解是否到位,查漏补缺。

vue2和vue3的响应式实现核心都使用了发布订阅设计模式

下面是ref实现响应式对象的流程介绍 image.png

下面开始分析源码

ref 函数入口

export function ref(value?: unknown) {
  // 调用createRef创建响应式对象
  return createRef(value, false)
}

function createRef(rawValue: unknown, shallow: boolean) {
  // 判断value是否已经是一个响应式对象了,如果是,则直接返回
  if (isRef(rawValue)) return rawValue

  // 实例化 RefImpl 创建响应式对象
  return new RefImpl(rawValue, shallow)
} 

从上面代码可以看出创建响应式对象的核心在于 class RefImpl ,下面开始分析这个 class

创建响应式对象


class RefImpl<T> {
  // 储存 ref 函数创建的值
  private _value: T
  // 没有被代理的原始数据
  private _rawValue: T
  
  // 是一个Set集合,这个 ref 所有的订阅者都会存在这个集合里面
  public dep?: Dep = undefined
  
  // 一个标记
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {
    // 根据一个 Vue 创建的代理返回其原始对象。
    this._rawValue = __v_isShallow ? value : toRaw(value)
    // toReactive 内部会通过 reactive 将 value 转换成一个响应式对象。
    this._value = __v_isShallow ? value : toReactive(value)
  }
   
  get value() {
    // 添加订阅者
    trackRefValue(this)
    
    // 返回对应ref变量的值
    return this._value
  }

  set value(newVal) {
    newVal = this.__v_isShallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
    
      // 更新 value
      this._rawValue = newVal
      this._value = this.__v_isShallow ? newVal : toReactive(newVal)
      
      // 通知订阅者更新
      triggerRefValue(this, newVal)
    }
  }
}

  • get value: 我们在 template 上面使用 {{ xxx }} 或者在外部(比如 setup 内部)使用 xxx.value 都会通过它来取值,同时会在这方法里面添加订阅者。
  • set value: 在执行 xxx.value = yyy 时触发,除了在这个方法内部更新value,还会通知对应订阅者更新。

下面开始分析 RefImpl 是怎么添加订阅和通知订阅更新的

添加订阅(也叫做依赖收集)

export function trackRefValue(ref: RefBase<any>) {

  // 需要订阅并且存在订阅者
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    trackEffects(ref.dep || (ref.dep = createDep()))
  }
}
  • shouldTrack 用来判断是否需要添加订阅,初始化时为 true
  • activeEffect 订阅者对象(会在组件渲染、计算属性、watch监听这三种情况下创建)。
// ref.dep 内部结构
export type Dep = Set<ReactiveEffect> & {w:number,n:number}

// 订阅者层级的标记
let effectTrackDepth = 0

// 当前
let trackOpBit = 1

// 最大标记的比特位
const maxMarkerBits = 30

export function trackEffects(
  dep: Dep,
  // debug 配置,不需要关心
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  
  // 判断是否超出依赖存储上限,一般情况下不会超过这个值
  if (effectTrackDepth <= maxMarkerBits) {
  
    // 性能优化,是否需要添加这个新的依赖
    if (!newTracked(dep)) {
      dep.n |= trackOpBit // set newly tracked
      shouldTrack = !wasTracked(dep)
    }
  } else {
    shouldTrack = !dep.has(activeEffect!)
  }
    
  // 添加依赖
  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
  }
}

变量介绍:

  • dep 用来添加订阅者的集合。
    • 关于 dep.n dep.w 源码中的解释是: wasTracked 和 newTracked 维护多个级别的效果跟踪递归的状态。 每个级别使用一位来定义是否跟踪依赖项。
  • sholudTrack 是否需要添加订阅者。

通知订阅者(触发更新)

触发更新阶段相对于依赖收集结点逻辑会清晰一些,主要就是遍历 ref.dep 通知对应的订阅者执行对应的代码。

set value(newVal) {
    newVal = this.__v_isShallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
    
      // 更新 value
      this._rawValue = newVal
      this._value = this.__v_isShallow ? newVal : toReactive(newVal)
      
      // 通知订阅者更新
      triggerRefValue(this, newVal)
    }
  }

从上面代码可以看出来,set value 首先会更新 ref 对象内部的值,紧接着会去通知订ref的订阅者们更新。

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  
  // 将订阅者集合转换成数组
  const effects = isArray(dep) ? dep : [...dep]
  
  
  // 下面两个 for 循环用来通知每个订阅者更新
  
  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 (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

总结:

        通过上面的分析我们能看出来,ref 实现响应式对象的核心就是发布订阅模式,所以只要我们能够好好的去理解,学习vue的原理也不是那么困难了。
        第一次把自己阅读源码的理解以这种形式记录下来,还是没什么经验,如果在文中某些点我的表达或者理解有误,请掘友们不吝赐教,我会积极更正错误
        附上我很喜欢的一句诗:长风破浪会有时,直挂云帆济沧海