vue3响应式源码分析

404 阅读27分钟

本文主要从vue源码的角度去分析vue3在响应式模块的实现

vue的响应式在不同的版本实现的方式不一样。在vue2.0的时候主要是通过Object.defineProperties这个api实现。但是vue3中使用的是es6的最新语法Proxy,基于代理的方式实现。不管通过那种方式实现。基本思想都是一样的。即在获取对象属性的时候去记录依赖,当值发生变化的时候在去触发依赖。其实这里就会出现一个问题,vue是怎么知道当这个值发生变化的时候需要执行那个函数呢?也就是说当一个值发生变化的时候怎么去触发和这个值有关联的所有fn呢?这里其实是通过高阶函数的方式去实现的。在vue结构下面对应 ReactiveEffect。这个函数的主要作用就是在执行fn的时候去初始化一些信息,并且把当前的实例赋值给一个全局变量,当在执行fn并且触发key对应的get方法的时候就可以根据这个全局变量知道当前的属性是在哪一个effect函数的下面执行,并且记录下来等待trigger。下面我会依次的分析。

ReactiveEffect

export let activeEffect: ReactiveEffect | undefined

export class ReactiveEffect<T = any> {
  //当前的Effect函数是否是激活状态
  active = true
  //当前Effect函数收集的依赖数组
  deps: Dep[] = []

  /**
   * 脏的状态
   */
  _dirtyLevel = DirtyLevels.Dirty
  /**
   * 本次追踪的id
   */
  _trackId = 0
  _runnings = 0
  // 本次是否应该刷新
  _shouldSchedule = false
  // 当前追踪依赖项的长度
  _depsLength = 0

   // fn我们要进行依赖收集的函数。当里面依赖项发生变化的时候需要重新执行
   // trigger和scheduler都是调度函数。区别在trigger会先触发。
   // scope是当前effect所属的scope作用域
  constructor(
    public fn: () => T,
    public trigger: () => void,
    public scheduler?: EffectScheduler,
    scope?: EffectScope,
  ) {
    recordEffectScope(this, scope)
  }

  public get dirty() {
    ///***
    return this._dirtyLevel >= DirtyLevels.Dirty
  }

  run() {
    this._dirtyLevel = DirtyLevels.NotDirty
    if (!this.active) {
      return this.fn()
    }
    let lastShouldTrack = shouldTrack
    let lastEffect = activeEffect
    try {
      // 初始化变量
      shouldTrack = true
      activeEffect = this
      this._runnings++
      preCleanupEffect(this)
      return this.fn()
    } finally {
      postCleanupEffect(this)
      this._runnings--
      activeEffect = lastEffect
      shouldTrack = lastShouldTrack
    }
  }

  stop() {
    if (this.active) {
      preCleanupEffect(this)
      postCleanupEffect(this)
      this.onStop && this.onStop()
      this.active = false
    }
  }
}

function preCleanupEffect(effect: ReactiveEffect) {
  effect._trackId++
  effect._depsLength = 0
}

function postCleanupEffect(effect: ReactiveEffect) {
  if (effect.deps.length > effect._depsLength) {
    for (let i = effect._depsLength; i < effect.deps.length; i++) {
      cleanupDepEffect(effect.deps[i], effect)
    }
    effect.deps.length = effect._depsLength
  }
}

这里先详细解释下run函数的作用。run函数会在每一次需要执行副作用函数的时候去调用。在执行之前需要做一些初始化的操作。

  1. 判断当前的active是否是激活的状态。如果当前的副作用函数已经失效,就不应该出现在key的依赖数组下面。所以可以直接执行fn并且返回。
  2. lastShouldTrack是当前是否可以进行收集的状态。当fn执行完成之后需要回到这个状态。
  3. lastEffectlastShouldTrack是一样的效果。lastEffect是上一个激活的activeEffect。这两个情况都是为了解决嵌套的情况。如果只有一层effect的话。 其实是没有必要的。当内层执行完成之后需要回退到外层的状态。
  4. shouldTrack设置为true表示可以收集。
  5. activeEffect设置this。也就是当前执行的effect函数。
  6. 当前正在运行的_runnings++。这个_runnings是在执行fn之前+1,当执行完成之后就-1。他的主要作用是为了能在递归执行当前的effect的,并且需要重新触发执行的时候。可以等待所有完成之后在执行。
  7. preCleanupEffect函数的作用重新设置当前收集过程中的一些变量。如_trackId,_depsLength
  8. _trackId的作用是标识当前的追踪id。每次执行的时候这个id都会++。
  9. _depsLength是标识当前fn一共有多少个key需要追踪。每次运行之前都会重置为0。

在finally语句中是做一些后置的操作。如当前正在运行的数量--,activeEffectshouldTrack回到执行之前的状态。postCleanupEffect函数的作用是在当这一次收集的数量和上一次不同的时候。如果第一次小于第二次的时候需要执行对应清理函数,如果有的话。这里会判断effect.deps.lengtheffect._depsLength的大小。在triggerEffects函数的时候会具体说这两个的值为啥会不一样。

总之ReactiveEffect函数的作用就是当执行传递的fn函数的时候。可以把当前effect函数记录到对应响应式变量下面的dep数组。同时在当前的effect函数的deps属性中也会记录一份。当对应的值发生变化的时候就循环这个dep数组。 重新依次执行对应的fn函数。下面是一个他的简单demo

//ref.js 文件
import { ref } from "https://cdn.bootcdn.net/ajax/libs/vue/3.4.27/vue.esm-browser.js";
const data1 = ref({ name: "111" });
const button1 = document.getElementById("button1");
button1.addEventListener("click", () => {
  data1.value.name = "222";
});

export { data1 };

//reactiveEffect.js文件
import { ReactiveEffect } from "https://cdn.bootcdn.net/ajax/libs/vue/3.4.27/vue.esm-browser.js";
import { NOOP } from "./common.js";
import { data1 } from "./ref.js";
console.log(data1);
const fn1 = () => {
  //   console.log("fn1");
  console.log(data1.value.name);
};

const scheduler1 = () => {
  if (effect1.dirty) {
    effect1.run();
  }
};
const effect1 = new ReactiveEffect(fn1, NOOP, scheduler1);
effect1.run();

在ref.js文件当中通过es6的语法引入vue中的ref函数并且定一个ref类型的变量。初始值是111。并且给一个button按钮定义一个点击事件触发的时候把这个值变成222。

在另外一个reactiveEffect文件当中定义一个effect。对应的fn就是读取一下data1.value.name 这个属性。当点击按钮的时候name属性变成222。就重新触发这个fn2函数。见下面的打印结果。

截屏2024-07-08 11.13.48.png

Ref

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

ref函数内部是调用一个createRef去初始化的。value就是ref的初始化值。createRef的第二个参数是是否深层次的响应。

createRef

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

RefImpl

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  // 当前收集的effect
  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(
    value: T,
    public readonly __v_isShallow: boolean,
  ) {
    // this._rawValue的作用是返回当前ref对应的原始类型的值
    this._rawValue = __v_isShallow ? value : toRaw(value)
    // 判断当前是否是shallow,如果不是会吧当前的值转换成响应式数据
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
  // 判断当前使用原始值 还是响应式的值。useDirectValue如果是true的话表示应该去使用传递值的类型去作为新的值。否则就使用原始类型的值。
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      const oldVal = this._rawValue
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, DirtyLevels.Dirty, newVal, oldVal)
    }
  }
}

RefImplconstructor函数。会根据当前是否是shallow,如果为true则_value的值就是传递的原始值比如传递的是reactive就是reactive,否则会把当前的值转换成原始类型的值。如果为false会把当前的值转换成响应式对象。正是因为这样通过shallowRef创建一个响应式对象,如果去修改这个对象中的key的话是不会触发他的依赖,需要手动通过triggerRef的方式去手动触发,这种触发实际上触发的是ref不是对应key。

RefImpl 构造函数的实现是value的getset函数。get函数的作用是调用trackRefValue的方式去收集依赖,并且返回this._value,如果原始值是一个对象会返回一个该对象对应的响应式对象,否则就返回一个普通对象。

set函数的作用是通过比较两次的值是否一样。如果不一样就通过triggerRefValue的方式去触发所有的依赖。从set函数的过程可以看出当给ref去设置值的时候会根据自身的只读或者shallow状态去对应的设置。比如当前ref是一个shallow的状态,当给他设置一个值的时候不会给当前的值设置成一个响应式的状态。但是如果当前的状态不是一个shallow并且newVal的值不是只读或者是shallow的时候, 会把当前的值转换成reactive对象去操作。

总结:设置值的时候如果当前是shallow的就按照设置的类型去设置。不会递归响应式。但是如果是shalow设置的值就是对应的类型,这种情况下设置的值是原始类型就递归响应式。否则就是原对象。递归响应式不是实现一个递归函数,而是只有获取到对应key的value的时候才去设置响应式,不是一开始的时候就把所有的value都递归响应式。

trackRefValue

export function trackRefValue(ref: RefBase<any>) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    trackEffect(
      activeEffect,
      //第二个参数是当前ref对应的dep。初始化的时候需要通过createDep的方法创建一个。
      (ref.dep ??= createDep(
        () => (ref.dep = undefined),
        ref instanceof ComputedRefImpl ? ref : undefined,
      )),
      __DEV__
        ? {
            target: ref,
            type: TrackOpTypes.GET,
            key: 'value',
          }
        : void 0,
    )
  }
}

// createDep 函数的作用就是返回一个Map结构。map的key是effect的追踪id。value是effect函数。同时加入一些清理函数。
export const createDep 函数的作用就是返回一个 = (
  cleanup: () => void,
  computed?: ComputedRefImpl<any>,
): Dep => {
  const dep = new Map() as Dep
  dep.cleanup = cleanup
  dep.computed = computed
  return dep
}

trackRefValue在执行的时候先判断当前执行期间是否有激活的activeEffectactiveEffect的值是在effect函数run的时候设置到一个全局变量上。所以effect函数必须要先执行,对应的fn函数里面的响应式数据才能去收集当前的依赖。shouldTrack是标记当前执行状态下是否应该去追踪。为了解决嵌套的情况,这个地方的判断用了一个队列的方式,队列的数据结构是boolean,每执行一次push进去一个当前的状态,执行完成之后在把上一次的状态取出去赋值给shouldTrack

export let shouldTrack = true
const trackStack: boolean[] = []

/**
 * Temporarily pauses tracking.
 */
export function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}

/**
 * Resets the previous global effect tracking state.
 */
export function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}

可以看到调用pauseTracking的时候是push下当前的状态到trackStack里面,并且把当前的shouldTrack设置为false。resetTracking是获取上一个收集的状态,并且把上一个收集的状态赋值给shouldTrack

如果可以追踪。就调用trackEffect函数去追踪。

trackEffect

export function trackEffect(
  effect: ReactiveEffect,
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
  if (dep.get(effect) !== effect._trackId) {
    dep.set(effect, effect._trackId)
    const oldDep = effect.deps[effect._depsLength]
    if (oldDep !== dep) {
      if (oldDep) {
        cleanupDepEffect(oldDep, effect)
      }
      effect.deps[effect._depsLength++] = dep
    } else {
      effect._depsLength++
    }
    if (__DEV__) {
      // eslint-disable-next-line no-restricted-syntax
      effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
    }
  }
}

trackEffect的参数有三个

  • effect。当前追踪的effect函数。
  • dep。当前追踪的dep属性。每一个响应式key都有一个dep与之对应。没有会在初始化的时候创建。dep是一个weakmap结构。
  • debuggerEventExtraInfo。dev调试参数。
  1. 函数开始的时候会根据当前的Effect的_trackId作为键在dep中找有没有与之对一个的Effect。如果有说明已经收集过了。没有就走下一步。每一次重新执行的时候_trackId都是不一样的,都会在原来的基础上加1。
  2. 如果当前的dep里面没有对应的_trackId。就将_trackId作为key,Effect作为值设置在dep里面。
  3. 获取当前的oldDep。如果oldDep和dep不一样就需要执行清理的动作。
  4. 否则的话就当前的_depsLength++

这里oldDep是通过effect.deps[effect._depsLength]的方式去获取。这里通过render对应的effect函数举例。见下面的例子。

<template>
  <div>
    {{ data.name }}
    <div v-if="isShowAge">
      {{ data.age }}
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const isShowAge = ref(true);
const data = ref({ name: "person", age: 14 });
</script>
<style lang="scss" scoped></style>

最终编译的结果如下:

const _sfc_main = /* @__PURE__ */ _defineComponent({
  __name: "Person",
  setup(__props, { expose: __expose }) {
    __expose();
    const isShowAge = ref(true);
    const data = ref({ name: "person", age: 14 });
    const __returned__ = { isShowAge, data };
    Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
    return __returned__;
  }
});
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode, createTextVNode as _createTextVNode } from "/node_modules/.vite/deps/vue.js?v=5780ee23";
const _hoisted_1 = { key: 0 };
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return _openBlock(), _createElementBlock("div", null, [
    _createTextVNode(
      _toDisplayString($setup.data.name) + " ",
      1
      /* TEXT */
    ),
    $setup.isShowAge ? (_openBlock(), _createElementBlock(
      "div",
      _hoisted_1,
      _toDisplayString($setup.data.age),
      1
      /* TEXT */
    )) : _createCommentVNode("v-if", true)
  ]);
}
import _export_sfc from "/@id/__x00__plugin-vue:export-helper";
export default _export_sfc(_sfc_main, [["render", _sfc_render], ["__file", "/Users/lubinjie/code/fork/my-vue-app/src/components/Person.vue"]]);

这里可以先关注_sfc_render_sfc_render是这个组件最终获取虚拟dom的render函数。

在这里第一次执行这个render函数去获取虚拟dom的时候。其实是有四个key需要收集在render函数对应的deps下面的。分别是data.namedata.ageisShowdata.value,如下图。

截屏2024-07-08 15.12.26.png 其中多出来的一个data.value。因为data是通过ref定义的,所以获取data.value.name的时候首先触发的data.value的get函数。如下:

截屏2024-07-08 15.16.37.png 从右边的执行栈也可以看出来。

当把isShowAgetrue变成false的时候需要重新执行render函数。其实就是Effect对应的fn需要重新执行。所以depLength会从上一次的4变成3。因为isShowAge变成false之后,$setup.isShowAge这个值是不成立的,所以直接走false的逻辑不再创建一个div类型的虚拟dom,而是创建一个v-if类型的注释节点,对应的属性获取不会在触发,data.age对应的dep也不会出现在render下的deps数组里面。

重新执行fn,也就意味需要重新进行依赖的收集,所以收集之前对应的depLength会重置为0。每当收集到一个的时候需要depLength+1。当没有加1之前,通过获取到这个effect.deps[effect._depsLength]是当前位置旧值对应的dep,如果前面的代码没有发生变化,这个地方两个值是一样。如果前面的代码发生变化,这个地方对应的就不是最新的dep,有可能旧的已经删了,也有可能放在后面。但是这两种情况都是不相等的情况,都需要走清除的逻辑,即便他的位置发生了变化,可能在后面并不是删除,也需要把当前位置的清除,当执行到后面的时候就重新的添加。

这里每次收集完成的时候都会有三种情况。

  1. 第一次的收集的数量小于第二次。这种属于是新加的。不需要处理。
  2. 第二种是等于的。这种也不需要额外的处理。
  3. 第三种是小于的。小于需要执行一些清理函数。

这里的收集顺序是按照数组来的。这个其实是和代码的执行的顺序有关系。因为代码执行都是从上到下。从左到右依次执行的。所以每次的重新运行如果没变化这个顺序其实是不会发生变化。

triggerRefValue

triggerRefValue是当前值发生变化的时候需要重新执行effect函数时候调用的。对应如下:

export function triggerRefValue(
  ref: RefBase<any>,
  dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
  newVal?: any,
  oldVal?: any,
) {
  ref = toRaw(ref)
  const dep = ref.dep
  if (dep) {
    triggerEffects(
      dep,
      dirtyLevel,
      __DEV__
        ? {
            target: ref,
            type: TriggerOpTypes.SET,
            key: 'value',
            newValue: newVal,
            oldValue: oldVal,
          }
        : void 0,
    )
  }
}

triggerRefValue一共有四个参数

  1. ref。当前要触发的ref变量
  2. dirtyLevel。触发的dirty等级默认是Dirty。
  3. newVal。触发的新值
  4. oldVal。旧值

这个函数首先获取当前ref对应的dep。根据dep是否有值去触发。触发调用主要的函数是triggerEffects

triggerEffects

export function triggerEffects(
  dep: Dep,
  dirtyLevel: DirtyLevels,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
  // 暂停Scheduling的执行
  pauseScheduling()
  for (const effect of dep.keys()) {
    let tracking: boolean | undefined
    // 当前effect对应的dirty等级要小于触发的等级。
    if (
      effect._dirtyLevel < dirtyLevel &&
      (tracking ??= dep.get(effect) === effect._trackId)
    ) {
      /**
       * _shouldSchedule  用来判断是否更新
       */
      effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
      effect._dirtyLevel = dirtyLevel
    }
    if (
      effect._shouldSchedule &&
      (tracking ??= dep.get(effect) === effect._trackId)
    ) {
    
      // 先触发trigger,在调度scheduler
      effect.trigger()
      if (
        (!effect._runnings || effect.allowRecurse) &&
        effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
      ) {
        effect._shouldSchedule = false
        if (effect.scheduler) {
          queueEffectSchedulers.push(effect.scheduler)
        }
      }
    }
  }
  // 重新启动Scheduling
  resetScheduling()
}

triggerEffects在执行期间需要先暂停scheduler的调度执行。等所有的effect都触发完成之后并且他的父调用都执行完成,其实是当前js执行期间没有在需要触发Effect的函数的时候。在开始调度。做法是通过一个队列。如下:

export let pauseScheduleStack = 0
const queueEffectSchedulers: EffectScheduler[] = []

export function pauseScheduling() {
  pauseScheduleStack++
}

export function resetScheduling() {
  pauseScheduleStack--
  while (!pauseScheduleStack && queueEffectSchedulers.length) {
    queueEffectSchedulers.shift()!()
  }
}

当调用pauseScheduling时候pauseScheduleStack++

resetScheduling时候pauseScheduleStack。请求在当pauseScheduleStack是0,也意味着所有的Effects已经执行完毕。在去queueEffectSchedulers调度队列里面去拿出一个scheduler去执行。

triggerEffects函数的作用是循环deps,获取每一个dep。然后根据当前的dirty等级去决定是触发还是不触发。 effect._shouldSchedule的判断条件是effect._dirtyLevel===DirtyLevels.NotDirty

也就是说当前effect的dirty等级必须是NotDirty。为什么是NotDirty,因为如果一个Effect在正常执行完成之后都是NotDirty。如果是NotDirty是可以触发更新的。可以更新之后就把当前effect的dirty等级设置为参数对应的等级。

如果_shouldSchedule为true表示可以更新。先调用Effect的trigger。然后在调度执行scheduler。 关于dirty的判定可以在另外一篇juejin.cn/post/738057…. 查看。

以上就是关于ref从设置到触发的全过程。

reactive

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap,
  )
}

reactive函数的作用是根据一个原始类型的target,返回一个代理对象类型。

createReactiveObject

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>,
) {
  // 判断当前代理的target是不是一个对象类型。如果不是的话就直接返回。
  if (!isObject(target)) {
    return target
  }
  // 如果当前的target已经是一个reactive对象就直接返回本身。
  // 如果一个对象是只读的,他的ReactiveFlags.RAW属性指向他的原始对象。但是他的ReactiveFlags.IS_REACTIVE属性是false。
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // 如果一个对象已经是响应式的对象。就返回缓存里面的。不在重复创建新的。
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 这里对值是类型是无效的值不进行响应式追踪。如果一个对象通过Object.preventExtensions设置之后他的值就是无效的
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
  )
  // 记录当前的代理对象
  proxyMap.set(target, proxy)
  return proxy
}

function getTargetType(value: Target) {
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}

createReactiveObject的参数有五个

  1. target。要代理的目标对象。
  2. isReadonly。是否是只读的。
  3. baseHandlers。普通拦截处理器。
  4. collectionHandlers。迭代器收集器
  5. proxyMap。缓存集合

这里有一个优化点。如果一个对象中的一个key对应的对象。不希望他被响应式追踪可以通过Object.preventExtensions的方式把对象变成不可以扩展的。

  1. Object.preventExtensions。使对象变得不可扩展,即不能再添加新的属性。
  2. Object.seal。使其不可扩展且所有现有属性不可配置。不能通过Object.defineProperty的方式设置属性描述符。
  3. Object.freeze。使其不可扩展且所有现有属性不可配置并且不能重新设置新的值。

createReactiveObject14行的实现可以看出。只读状态和非只读状态是可以相互转换的,可以把一个响应式对象通过readonly变成只读的。也可以把一个只读的通过reactive转换成响应式的。

shallowReadonly

export function shallowReactive<T extends object>(
  target: T,
): ShallowReactive<T> {
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers,
    shallowReactiveMap,
  )
}

shallowReactive

export function shallowReadonly<T extends object>(target: T): Readonly<T> {
  return createReactiveObject(
    target,
    true,
    shallowReadonlyHandlers,
    shallowReadonlyCollectionHandlers,
    shallowReadonlyMap,
  )
}

vue官方还提供另外两个apishallowReadonlyshallowReactive,这两个api的作用是把一个普通的对象转换成对应的浅只读,浅响应式。如上实现。

通过对createReactiveObject分析可知。浅层次的创建是通过传递proxy的handle不一样,只读是通过参数的形式。所以如果把一个只读的对象传递给shallowReadonly,其实是不能创建成功,会返回原来的对象。同理一个响应式对象也不能通过shallowReactive的方式创建一个浅类型的。下面是几个创建的例子:

import {
  ref,
  reactive,
  readonly,
  shallowReactive,
  shallowReadonly,
} from "https://cdn.bootcdn.net/ajax/libs/vue/3.4.27/vue.esm-browser.js";

const data1 = readonly({ a: 1 });
const data2 = shallowReadonly(data1);

const data3 = reactive({ a: 1 });
const data4 = shallowReactive(data3);

const data5 = shallowReactive(data1);

console.log(data1 === data2, data3 === data4, data5 === data1, "wtf");


截屏2024-07-15 14.57.58.png

第三个true是因为参数只读是false并且target的isReactive也是false,所以直接返回原对象。因为target[ReactiveFlags.IS_REACTIVE]的返回值是当前target的只读状态取反,从BaseReactiveHandler的实现可知。

总结来说如果要创建成功,必须要传递值的只读状态和创建时候传递的只读参数要相反,相同的话必须都是true。否则就会创建失败返回原对象。

mutableHandlers

mutableHandlers是一般的reactive对象的代理处理器。他是通过new MutableReactiveHandler()方式得到的。

MutableReactiveHandler

所有的处理器都会有一个相同get处理函数。所以MutableReactiveHandler这边会继承BaseReactiveHandler

class MutableReactiveHandler extends BaseReactiveHandler {
  constructor(isShallow = false) {
    super(false, isShallow)
  }

  set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object,
  ): boolean {
    let oldValue = (target as any)[key]

    // hadKey当target是数组的时候并且是添加一个属性 如pop的时候是true
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    if (target === toRaw(receiver)) {
      if (!hadKey) {
      // 如果是添加的属性 触发的类型是ADD
        trigger(target, TriggerOpTypes.ADD, key, value)
        // 判断触发的两次的值是否发生变化。如length的时候
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
  deleteProperty(target: object, key: string | symbol): boolean {}
  has(target: object, key: string | symbol): boolean {}
  ownKeys(target: object): (string | symbol)[] {}
    
}
class BaseReactiveHandler implements ProxyHandler<Target> {
  constructor(
    protected readonly _isReadonly = false,
    protected readonly _isShallow = false,
  ) {}

  get(target: Target, key: string | symbol, receiver: object) {
    const isReadonly = this._isReadonly,
      isShallow = this._isShallow
     // 获取vue内部的属性如是否是响应式数据。获取原始值RAW等。
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return isShallow
    } else if (key === ReactiveFlags.RAW) {
        return target
    }

    // 访问的对象是不是数组
    const targetIsArray = isArray(target)

    if (!isReadonly) {
    // 不是只读。并且是获取数组中某几个特殊的属性如pop。需要做一层拦截。
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
        return Reflect.get(arrayInstrumentations, key, receiver)
      }
      if (key === 'hasOwnProperty') {
        return hasOwnProperty
      }
    }

    // 访问的具体的属性值
    const res = Reflect.get(target, key, receiver)

    // 如果访问你的key是否某一个知名符号,如Symbol.iterator,或者是获取对象特殊属性如__proto__这些属性就直接返回,不需要去追踪响应。
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    // 不是只读的就追踪对应的key,需要响应式触发
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    // 如果是浅层次的直接返回不需要递归去追踪
    if (isShallow) {
      return res
    }

    if (isRef(res)) {
    // 如果访问的key是ref类型。直接返回res.value.  外面不需要通过Object[key].value的方式获取
    // 如果一个对象是一个浅层次的就不会进行value的自动解包了。
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }

    if (isObject(res)) {
      // 如果当前不是只读的并且要访问的key是一个对象的话吧对应的value变成响应式对象。
      // 如果要访问的是一个对象类型 会根据当前自身的状态去递归的处理
      // 比如readonly(reactive(obj)) 虽然obj是一个响应式的对象 但是因为readonly就不能改变了,
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

BaseReactiveHandler 类的主要作用是提供一个通用的get函数。 主要功能如下:

  1. 判断是不是获取vue内部的一些属性值如__v_isReactive,__v_raw,__v_isReadonly
  2. 如果要访问的是一个数组中的某一个特殊属性如pop。返回被被劫持之后的函数。
  3. 如果返回的是一个知名的符号或者访问的是一个特殊的属性如原型就直接返回。
  4. 如果当前对象不是只读的。就追踪的对应的key。进行响应式的触发。
  5. 如果当前对象是浅层的。直接返回。不需要深层次处理。
  6. 如果访问的value是一个ref对象。返回res.value。自动解包。shallow就不能自动解包。
  7. 如果访问的是一个对象的并且当前不是只读的,把当前的对象变成响应式的。如果是只读的就递归设置只读。所以vue的响应式对象不是刚开始创建的时候就递归创建。而是获取的时候才去创建。和vue2递归创建不太一样。提高效率。

下面再说如果访问的是数组的情况。数组情况的处理vue加一个劫持。

// 返回类似 {popL:()=>any}
const arrayInstrumentations = createArrayInstrumentations()

function createArrayInstrumentations() {
  const instrumentations: Record<string, Function> = {}
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      const arr = toRaw(this) as any
      for (let i = 0, l = this.length; i < l; i++) {
        track(arr, TrackOpTypes.GET, i + '')
      }
      const res = arr[key](...args)
      if (res === -1 || res === false) {
        return arr[key](...args.map(toRaw))
      } else {
        return res
      }
    }
  })

  ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      pauseTracking()
      pauseScheduling()
      const res = (toRaw(this) as any)[key].apply(this, args)
      resetScheduling()
      resetTracking()
      return res
    }
  })
  return instrumentations
}
let arr = [1, 2, 3, 5, 6];
const arrPro = new Proxy(arr, {
  get(targer, key) {
    console.log(targer, key);
    return targer[key];
  },
  set(targer, key, value) {
    const oldValue = targer[key];
    console.log(targer, key, value, oldValue);
    targer[key] = value;
    return true;
  },
});

用上面的例子验证数组的方法被调用的时候触发的次数。有些情况下他是会触发多次的。但是最终导致的结果都只会触发一次。

push

push方法被调用的时候set被执行两次如下: 截屏2024-07-09 15.26.19.png

后两次都会触发set。但是由于是length触发的时候value和oldValue的值都是6,没有发生变化就没有重新触发。

pop

pop方法被执行的时候会触发一次。触发的key是length

截屏2024-07-09 15.34.58.png

shift,unshift

这两个方法和poppush类似

reverse

截屏2024-07-09 15.37.17.png reverse方法执行的时候set触发的次数是不确定。 他取决当前数组中的个数。他的交换顺序是1和n各交换一次,2和n-1各交换一次。一共交换n-1次。 所以会触发n-1次triggerEffect。但是不会触发n-1次update,因为第一次触发的时候对应的Effect的等级就已经是dirty了。所以第二次再一次触发的时候Effect.shouldUpdate的值就是false,不会重复的触发。 其他的情况都是类似的。

track

track函数的具体实现

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
    // 在vue内部每一个有一个weekMap结果用来存储每一个对象所所有key对应的dep
  ·// 获取当前对象对应的depsMap  如果没有就创建一个。
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    // 获取当前key对应的dep 如果没有就创建一个
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))))
    }
    //用dep去触发Effect
    trackEffect(
      activeEffect,
      dep,
      __DEV__
        ? {
            target,
            type,
            key,
          }
        : void 0,
    )
  }
}

trigger

settriggerEffects之间是通过trigger这个桥梁实现的。下面是具体的实现。

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>,
) {
  // 获取对象的depsMap。没有就返回
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  let deps: (Dep | undefined)[] = []
  //如果type类型是clear。需要当前对象的所有属性的所有key对应的dep都要重新执行。
  if (type === TriggerOpTypes.CLEAR) {
    deps = [...depsMap.values()]
    // 如果改变的是数组的length属性。就需要触发小于length的所有属性。循环的时候需要剔除掉length和symbol属性。
  } else if (key === 'length' && isArray(target)) {
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
        deps.push(dep)
      }
    })
  } else {

    // 正常触发某一个对象的单个key对应的dep
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }

    switch (type) {
      //如果触发类型是add
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  pauseScheduling()
  for (const dep of deps) {
    if (dep) {
      triggerEffects(
        dep,
        DirtyLevels.Dirty,
      )
    }
  }
  resetScheduling()
}

watch

export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>,
): WatchStopHandle {
  return doWatch(source as any, cb, options)
}

watchEffect

export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase,
): WatchStopHandle {
  return doWatch(effect, null, options)
}

watchPostEffect

export function watchPostEffect(
  effect: WatchEffect,
  options?: DebuggerOptions,
) {
  return doWatch(
    effect,
    null,
    __DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' },
  )
}

watchSyncEffect


export function watchSyncEffect(
  effect: WatchEffect,
  options?: DebuggerOptions,
) {
  return doWatch(
    effect,
    null,
    __DEV__ ? extend({}, options as any, { flush: 'sync' }) : { flush: 'sync' },
  )
}

watchwatchEffectwatchPostEffectwatchSyncEffect 这三个api都是通过调用doWatch去实现的。只是传递的参数不一样。

doWatch

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  {
    immediate,
    deep,
    flush,
    once,
    onTrack,
    onTrigger,
  }: WatchOptions = EMPTY_OBJ,
): WatchStopHandle {

// 回调存在并且需要立即执行一次。拦截cb当执行完成之后就结束监视。
  if (cb && once) {
    const _cb = cb
    cb = (...args) => {
      _cb(...args)
      unwatch()
    }
  }

  const instance = currentInstance
  // 如果是深度监视需要递归获取对象中的每一个属性。
  const reactiveGetter = (source: object) =>
    deep === true
      ? source 
      :traverse(source, deep === false ? 1 : undefined)

  // 生成Effect中的fn函数。 参数归一化统一处理成功函数的形式。
  let getter: () => any
  // 是否是强制触发。对于shallow类型的source。
  let forceTrigger = false
  // 是否有多个数据源
  let isMultiSource = false

  // 当前source是ref类型
  if (isRef(source)) {
    getter = () => source.value
    forceTrigger = isShallow(source)
    // 当前source是reactive类型
  } else if (isReactive(source)) {
    // reactiveGetter函数的作用是如果是深度监听就获取第一层的数据。
    getter = () => reactiveGetter(source)
    forceTrigger = true
    // 监控的数据源是多个
  } else if (isArray(source)) {
    isMultiSource = true
    // 如果其中有一个类型是shallow。就需要强制触发。
    forceTrigger = source.some(s => isReactive(s) || isShallow(s))
    //。循环处理数组中的每一个数据类型。
    getter = () =>
      source.map(s => {
        if (isRef(s)) {
          return s.value
        } else if (isReactive(s)) {
          return reactiveGetter(s)
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
        } else {
          __DEV__ && warnInvalidSource(s)
        }
      })
    // 数据源是函数类型并且有cb参数。是指watch并且数据源是一个函数。没有cb参数是指watchEffect
  } else if (isFunction(source)) {
    if (cb) {
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
      getter = () => {
        if (cleanup) {
          cleanup()
        }
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onCleanup],
        )
      }
    }
  } else {
    getter = NOOP
  }

  // 如果有cb参数。并且是深度监听。需要递归获取每一个key
  if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }

  let cleanup: (() => void) | undefined
  // 清理函数
  let onCleanup: OnCleanup = (fn: () => void) => {
    cleanup = effect.onStop = () => {
      callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
      cleanup = effect.onStop = undefined
    }
  }

  let oldValue: any = isMultiSource
    ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
    : INITIAL_WATCHER_VALUE
  // scheduler
  const job: SchedulerJob = () => {
    // 当前effect是否已经暂停。当前effect的dirty的等级是否是dirty
    if (!effect.active || !effect.dirty) {
      return
    }
    // 如果是watch(source, cb)
    if (cb) {
      // 重新运行fn获取最新的值
      const newValue = effect.run()
      // 重新运行运行的条件是deep为true。或者deep为false的时候。两次是否有变化。
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
          : hasChanged(newValue, oldValue))
      ) {
        // 重新运行之前先执行上一次的清理函数
        if (cleanup) {
          cleanup()
        }
        // 调用cb,参数是新值,旧值,和清理函数
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          oldValue === INITIAL_WATCHER_VALUE
            ? undefined
            : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
              ? []
              : oldValue,
          onCleanup,
        ])
        oldValue = newValue
      }
    } else {
      // watchEffect
      effect.run()
    }
  }

  job.allowRecurse = !!cb

  let scheduler: EffectScheduler
  // 如果是sync运行的话。会在当前effect执行的时候同步执行
  if (flush === 'sync') {
    scheduler = job as any 
  } else if (flush === 'post') {
    // 渲染完成之后。 异步执行
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    job.pre = true
    // 提供当前instance的uid。 会在当前的update函数执行之后同步执行
    if (instance) job.id = instance.uid
    scheduler = () => queueJob(job)
  }

  const effect = new ReactiveEffect(getter, NOOP, scheduler)

  const scope = getCurrentScope()
  const unwatch = () => {
    effect.stop()
    if (scope) {
      remove(scope.effects, effect)
    }
  }

  // initial run
  if (cb) {
    // 如果是立即触发 调用job的回调。否则调用run获取值
    if (immediate) {
      job()
    } else {
      oldValue = effect.run()
    }
    // 否则就是watchEffect 除了post需要调度执行。其他都是执行。
  } else if (flush === 'post') {
    queuePostRenderEffect(
      effect.run.bind(effect),
      instance && instance.suspense,
    )
  } else {
    effect.run()
  }
  return unwatch
}

doWatch的三个参数

  1. source。要监控的数据源
  2. cb。触发的回调函数
  3. options。触发的配置选项

doWatchsource来源一共有四种

  1. WatchSource。ref和ComputedRef两种类型。或者是一个返回ref类型的函数。
  2. WatchSource[]。WatchSource的数组的形式。
  3. WatchEffect(onCleanup: OnCleanup) => void。是一个函数并且有一个清理函数。一般是watchEffect传递的回调函数。
  4. object。一般的对象。

前两种类型是watch函数的第一个参数。表示监视的数据源。 第三种类型是watchEffect的回调函数。

doWatchoptions参数是配置选项,如下

export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
   // 是否立即触发
  immediate?: Immediate
  // 是否深度监听
  deep?: boolean
  // 是否立即执行
  once?: boolean
}

export interface WatchOptionsBase extends DebuggerOptions {
// 触发的时机
  flush?: 'pre' | 'post' | 'sync'
}

doWatch的触发时机一共有三种

  1. sync。同步触发
  2. post。组件渲染完成以后异步触发
  3. pre。默认值。 组件update函数处理完成之后同步触发。

syncpre的方式在执行的时候当前组件的update函数还没有执行。所以dom还没有更新。 post的方式是所有的组件更新执行之后。所以他执行的时候 dom已经更新。 post有点类似react的useLayoutEffect。sync类似useEffectnextTick函数就类似post方式。

总结: doWatch函数watch的原理是创建一个ReactiveEffect。 需要执行的fn是监视的数据源。通过参数归一化的方式生成一个函数fn,当执行fn的时候就会收集里面的依赖,并且在依赖更新的时候重新触发,触发的方式根据flush的不同会有不同的触发。具体可查看我的另外一篇文章juejin.cn/post/739081…

nextTick

export function nextTick<T = void, R = void>(
  this: T,
  fn?: (this: T) => R,
): Promise<Awaited<R>> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}

nextTick函数本身的实现是很简单的。是在currentFlushPromise这个promise给注册一个then方法的回调。

vue在内部维护了两个队列。 一个是用于更新的队列quene。另外一个是用来处理更新过程中出现的cb队列pendingPostFlushCbs

const resolvedPromise =  Promise.resolve()

export function queueJob(job: SchedulerJob) {
  if (
    !queue.length ||
    !queue.includes(
      job,
      isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex,
    )
  ) {
  // 当给队列添加一个的时候会根据id给插入到给定id的后面。
    if (job.id == null) {
      queue.push(job)
    } else {
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    queueFlush()
  }
}

function queueFlush() {
  // 如果正在执行队列中的方法。或者刷新队列对应的promise在pending直接返回。
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

function flushJobs(seen?: CountMap) {
  isFlushPending = false
  isFlushing = true
  if (__DEV__) {
    seen = seen || new Map()
  }
  queue.sort(comparator)
minifiers
  const check = __DEV__
    ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
    : NOOP

  try {
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job && job.active !== false) {
        if (__DEV__ && check(job)) {
          continue
        }
        // 取出任务执行pendingPostFlushCbs队列里面的人物。
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    flushIndex = 0
    queue.length = 0
    // 执行完成之后在执行
    flushPostFlushCbs(seen)

    isFlushing = false
    currentFlushPromise = null
    if (queue.length || pendingPostFlushCbs.length) {
      flushJobs(seen)
    }
  }
}

quene队列的执行时间是在下一次事件循环的微任务队列当中。pendingPostFlushCbs是在quene队列执行完毕之后。所以两个队列执行的时机都是在下一次事件循环。