侧边栏壁纸
博主头像
前端自习室博主等级

折腾是进步的阶梯

  • 累计撰写 30 篇文章
  • 累计创建 0 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录
vue

Vue3 入门参考(三):effect

lumozx
2023-10-25 / 0 评论 / 0 点赞 / 16 阅读 / 66589 字

注:本文使用vue版本为3.3.4

前文我们了解了渲染流程,整个流程是基于effect函数的依赖变动,从而不断触发patch,保持页面为数据的最新渲染,那么我们这次来了解一下他的依赖是怎么建立起来的。

或者说vue的响应式是什么。众所周知,vue的响应式数据包括ref, reactive, computed,我们一个一个来看。

ref、shallowRef

ref方法和shallowRef都可以创建一个响应式数据,这个响应式数据的value就是我们使用的数据,对这个value进行修改都会触发响应式逻辑。

他们之中不同点是

  • ref可以进行深层次转换

  • shallowRef不能进行深层次的转换,这里需要注意的是,value本身也算一层。所以,对value整体的赋值,才会触发shallowRef的响应式。

我们直接看看他们的源码。

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

可以看到,他们都调用了createRef,不同的是第二个参数不同,可以推断出第二个参数就是控制shallow的开关。

那么我们进入createRef看一下

function createRef(rawValue: unknown, shallow: boolean) {
  // 已经是响应式了,那么原值返回
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

可以看到,createRef尽可能拦截多层响应式数据的ref嵌套,如果已经是一个响应式数据,那么将原值返回,否则,进入new RefImpl逻辑。这里返回new的结果,我们可以大胆猜测,new RefImpl的结果就是那个包含value的响应式对象。

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
  ) {
    // 递归获取原始值,如果是shallowRef就不递归,直接返回传入值
    this._rawValue = __v_isShallow ? value : toRaw(value)
    // 没有value字段的响应式数据,如果是shallowRef,就不对他使用响应式处理
    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)
    }
  }
}

这里可以看到,我们平时常说的vue3的响应式是基于proxy是不完全正确的,准确说是对象基于proxy,而非对象类型,是用的基于对象的get 和 set实现响应式,换句话说,是对vue2的Object.defineProperty的简化应用,vue2的Object.defineProperty是需要遍历key值,而vue3是直接挂载到对象的value字段下面,直接用set和get就实现了劫持。

在set的时候,会对新值进行比较,如果不相同才会赋值,并且判断新值是否响应式,比如当前ref本身是shallow,或者新值是一个shallow,或者新值是只读属性,那么不进行响应式化。

那么,这就是非对象类型,基于对象实现的响应式逻辑。

那么如果是对象类型呢?

换句话说,toReactive的实现逻辑呢?

export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

简单明了,如果是对象类型,调用reactive,也就是ref的对象类型,是对reactive的一层封装。

我们直接看看reactive的源码。

reactive、shallowReactive

reactive和shallowReactive都可以创建响应式对象,区别同ref一样,带有shallow的只能创建浅层响应式数据。

export function reactive(target: object) {
  // readonly原路返回
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

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

看起来核心函数就是createReactiveObject。而第二个参数,就是shallow的开关。

function createReactiveObject(
  target: Target, // 目标对象,需要被代理的对象
  isReadonly: boolean, // 是否为只读
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any> //缓存
) {
  // 如果目标对象不是一个对象,则无法创建响应式代理
  if (!isObject(target)) {
    return target
  }
  // 如果目标对象已经是一个代理对象,并且不是只读代理,直接返回它
  // 例外:在一个响应式对象上调用readonly()方法
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // 如果目标对象已经有对应的代理对象,返回该代理对象
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 只有特定类型的值可以被观察
  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
}

注释写的很详细,整个核心流程就是首先经过一系列判断,当前对象是否符合要求,包括了入参的类型、是否是响应式的、是否已经被定义过了,以及是否是符合要求的类型这些步骤,最后执行的是 new Proxy() 这样的一个响应式代理 ,在执行的时候,还需要根据类型挂载不同的handler。

我们目光来到handler上,调用createReactiveObject的handler出现了多个,我们分情况讨论下

reactive

shallowReactive

readonly

shallowReadonly

baseHandlers

mutableHandlers

shallowReactiveHandlers

readonlyHandlers

shallowReadonlyHandlers

collectionHandlers

mutableCollectionHandlers

shallowCollectionHandlers

readonlyCollectionHandlers

shallowReadonlyCollectionHandlers

这里大家会产生疑问,现在不是讨论reactive和shallowReactive吗?怎么多出来readonly、shallowReadonly,实际上在解析createReactiveObject的时候,我们注意到判断isReadonly的时候,会判断入参是不是响应式数据,也就是说readonly和shallowReadonly其实也是响应式数据,只是他们是只读的,或者只有根层数是只读的。他们都调用了createReactiveObject,创建了一个代理对象,并挂载不同的代理行为。

baseHandlers

export const mutableHandlers: ProxyHandler<object> =
  /*#__PURE__*/ new MutableReactiveHandler()

export const readonlyHandlers: ProxyHandler<object> =
  /*#__PURE__*/ new ReadonlyReactiveHandler()

export const shallowReactiveHandlers = /*#__PURE__*/ new MutableReactiveHandler(
  true
)
export const shallowReadonlyHandlers =
  /*#__PURE__*/ new ReadonlyReactiveHandler(true)

看起来他们还是分开的,reactive和shallowReactiveHandlers是使用同一个类——MutableReactiveHandler,而readonly和shallowReadonly使用同一个类——ReadonlyReactiveHandler。

但他们都继承了同一个类BaseReactiveHandler,我们直接看看BaseReactiveHandler实现了什么功能。

get

class BaseReactiveHandler implements ProxyHandler<Target> {
  constructor(
    protected readonly _isReadonly = false, // 是否为只读
    protected readonly _shallow = false // 是否为浅层
  ) {}

  get(target: Target, key: string | symbol, receiver: object) {
    const isReadonly = this._isReadonly,
      shallow = this._shallow;

    // 如果 key 是 ReactiveFlags 中的特殊标记,返回相应的值
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly;
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly;
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return shallow;
    } else if (
      // 如果 key 是 RAW,且 receiver 是对应的 Map 中的代理对象,则返回原始目标对象
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target;
    }

    const targetIsArray = isArray(target);

    // 如果不是只读,且 key 是数组内置方法或者 hasOwnProperty 方法,则返回相应的值
    if (!isReadonly) {
        // 处理数组
      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 类型或者是不可追踪的key,则直接返回属性值
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res;
    }

    // 如果不是只读,追踪目标对象的属性访问操作
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key);
    }

    // 如果是浅层,直接返回属性值
    if (shallow) {
      return res;
    }

    // 如果属性值是 ref 对象,且 key 是数组的整数索引,则返回 ref 对象的值,否则返回 ref 对象本身
    if (isRef(res)) {
      // ref unwrapping - 跳过对数组 + 整数键的拆包。
      return targetIsArray && isIntegerKey(key) ? res : res.value;
    }

    // 如果属性值是对象,则将返回值也转换为代理对象。在这里进行 isObject 检查是为了避免无效值警告。
    return isReadonly ? readonly(res) : reactive(res);
  }
}

整个逻辑比较清晰,首先对 key 属于 ReactiveFlags 的部分做了特殊处理,从代码上看起来,vue3并没有将ReactiveFlags挂载到数据上,而是在get上面做了劫持,这也是为什么在 createReactiveObject 函数中判断响应式对象是否存在 ReactiveFlags.RAW 属性,如果存在就返回这个响应式对象本身。同时,也因为响应式对象扩展了各种key, 让代理对象有了不同的表现逻辑。

然后如果target是数组,数组用到了arrayInstrumentations,逻辑是返回arrayInstrumentations中对应的值,arrayInstrumentations是什么?

arrayInstrumentations是createArrayInstrumentations创建来的,是他的返回值,我们看看createArrayInstrumentations实现了什么逻辑。

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(); // 停止依赖收集
      const res = (toRaw(this) as any)[key].apply(this, args); // 获取原始数组并调用相应的方法
      resetTracking(); // 恢复依赖收集
      return res; // 返回方法的结果
    };
  });

  return instrumentations; // 返回处理后的数组方法
}

也就是说,arrayInstrumentations劫持了数组的target,对上面的方法进行了包装,对于includes、 indexOf、 lastIndexOf这些查询方法,数组是可以进行依赖收集的,而针对push、 pop、 shift、 unshift、 splice,这些方法会修改数组的length,但这样会在effect中导致死循环。具体来说,是在effect中,如果发现数组长度变化, 就自增数组,结果导致了数组疯狂自增,这个原因是数组的length也是依赖收集的一环,结果数组长度变化触发了effect,然后effect的自增行为又触发了effect,可以看这个Issue

按照之前的逻辑往下走,我们发现了收集依赖函数——track,在这之前我们已经见到trackRefValue了,现在补上之前留下的坑。

// 本质是还原ref获取原始值,然后调用trackEffects
export function trackRefValue(ref: RefBase<any>) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
	// 如果ref上有dep就用,没有就创建一个,dep是一个Set
   trackEffects(ref.dep || (ref.dep = createDep()))
  }
}

// 是否应该收集依赖
let shouldTrack = true
// 当前正在收集依赖的effect
let activeEffect

const targetMap = new WeakMap()


export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 如果需要追踪(即 shouldTrack 为 true)并且存在活跃的effect函数
  if (shouldTrack && activeEffect) {
    // 获取目标对象的依赖映射
    let depsMap = targetMap.get(target)
    // 如果不存在依赖映射,创建一个新的 Map 对象
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    // 获取属性的依赖集合
    let dep = depsMap.get(key)
    // 如果不存在依赖集合,创建一个新的依赖对象(dep)
    if (!dep) {
      depsMap.set(key, (dep = createDep()))
    }

    // 将当前effect函数追加到依赖集合中
    trackEffects(dep)
  }
}


export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // 用于判断是否应该追踪该依赖对象
  let shouldTrack = false

  // 如果效果函数的追踪深度小于等于最大标记位数
  if (effectTrackDepth <= maxMarkerBits) {
    // 如果该依赖对象尚未被新追踪,将其标记为新追踪
    if (!newTracked(dep)) {
      dep.n |= trackOpBit // 设置为新追踪
      shouldTrack = !wasTracked(dep) // 判断是否应该追踪该依赖对象
    }
  } else {
    // 只有没收集的依赖才能收集
    shouldTrack = !dep.has(activeEffect!)
  }

  // 如果应该追踪该依赖对象
  if (shouldTrack) {
    // 将当前的effect函数添加到依赖对象中
    dep.add(activeEffect!)
    // 将依赖对象添加到当前的effect函数的依赖集合中
    activeEffect!.deps.push(dep)
  }
}

我们在这里补了前面挖的坑,trackRefValue和track其实都是触发trackEffects的包装,这里有一个全局的targetMap,他的key就是target,value是depsMap。

depsMap的key是target的key,而value就是dep的集合,dep就是副作用函数。这个类似Vue2 Observer的dep。

在trackEffects中,响应式数据(ref,effect,computed等)的dep会传入进来,供trackEffects进行处理,里面会遇到新旧依赖的逻辑,这个之后会讲,在收集依赖的逻辑里面,会讲当前的activeEffect视同add加入dep中,也就是收集到了依赖,明确说明一点是activeEffect依赖当前正在trackEffects处理的响应式数据,当这个响应式数据变化的时候,会触发dep里面的当时推入的effect的不同逻辑。

同时activeEffect还有自己的逻辑,他本身还有deps,同时把dep推入自己的deps数组里面。

也就是说,deps的是当前effect依赖的响应式数据所被依赖的合集。比如说

import {ref, computed} from 'vue'
const a = ref(1)
const b = computed(() => a.value + 1)
const c = computed(() => a.value + 2)
// 捕获依赖
b.value 
c.value

此时,a的dep就是一个Set里面有两个value,分别是b 和 c的effect。

而b的effect的deps,此时数组只有一个元素,是长度为2的Set,value分别的b的effect和c的effect。

c同理。

我们回到BaseReactiveHandler,处理完各种特殊情况的时候,逻辑来到了shallow判断。

如果shallow为true,那么直接返回res,也就是结果,为false。

接下来处理了数组展开的情况。

然后关键的地方来了,如果是对象,只读的话递归只读逻辑,非只读返回reactive处理后的。

我们来复习一下Proxy,使用Proxy代理对象的时候,天生只会代理第一层对象,也就是说Proxy默认shallow为true,那么shallow为false 的功能怎么实现呢?

那就是使用了get,当get到非根级对象的时候,会先把对象响应式化,再返回。

这样就实现了对象多级响应式化的功能。而非Vue2在定义的时候就使用更加耗费性能的遍历递归。

也就是说,BaseReactiveHandler起到了一个公共get的作用,在get使用lazy的方式,对用到的数据进行响应式化,而set等则在子类中实现。

set、deleteProperty...

我们看一下readonly和shallowReadonly使用的ReadonlyReactiveHandler。

class ReadonlyReactiveHandler extends BaseReactiveHandler {
  constructor(shallow = false) {
    super(true, shallow)
  }

  set(target: object, key: string | symbol) {
    return true
  }

  deleteProperty(target: object, key: string | symbol) {
    return true
  }
}

简单粗暴,就是继承了BaseReactiveHandler,然后把set和deleteProperty屏蔽了,起到了readonly的效果,唯一区别是shallow的值不同。isReadonly默认是true,对应到BaseReactiveHandler的逻辑,isReadonly对应判断逻辑是在shallow判断之后,如果shallow 是 true,那么不进行递归,如果是false,并且取到的值是对象,那么对取到的值进行递归readonly化。而递归的readonly又把之前的流程走一遍,调用createReactiveObject,构造代理对象,然后再次挂载readonlyHandlers(因为shallowReadonly不会递归,所以递归的都使用readonly的逻辑)。

总结一下,ReadonlyReactiveHandler通过屏蔽set和deleteProperty,实现只读的功能,通过对shallow开关的控制,来决定是否对非根层级的对象节点实现只读的功能。

那么mutableHandlers和shallowReactiveHandlers使用的MutableReactiveHandler呢?我们再次看看源码

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

  set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]; // 获取目标对象的旧值
    // 如果旧值是只读且是 ref 对象,且新值不是 ref 对象,直接返回false,禁止修改
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false;
    }
    if (!this._shallow) {
      // 如果不是浅层代理,且新值和旧值都不是浅层响应式对象,将它们转换为原始值
      if (!isShallow(value) && !isReadonly(value)) {
        oldValue = toRaw(oldValue);
        value = toRaw(value);
      }
      // 如果目标对象不是数组,旧值是 ref 对象,且新值不是 ref 对象,更新 ref 对象的值并返回true
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value;
        return true;
      }
    } else {
      // 在浅层代理模式下,无论是否是响应式对象,都直接将对象设置为新值

    }

    // 检查 key 是否存在于目标对象中
    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) {
        //  触发trigger
        trigger(target, TriggerOpTypes.ADD, key, value); // 触发添加操作
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue); // 触发更新操作
      }
    }
    return result; // 返回操作结果
  }

  deleteProperty(target: object, key: string | symbol): boolean {
    const hadKey = hasOwn(target, key); // 检查 key 是否存在于目标对象中
    const oldValue = (target as any)[key]; // 获取旧值
    const result = Reflect.deleteProperty(target, key); // 删除目标对象的属性
    // 如果删除成功且 key 存在于目标对象中,触发删除操作
    if (result && hadKey) {
      //  触发trigger
      trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);
    }
    return result; // 返回删除操作结果
  }

  has(target: object, key: string | symbol): boolean {
    const result = Reflect.has(target, key); // 检查 key 是否存在于目标对象中
    // 如果 key 不是 Symbol 类型或者不是内置 Symbol,则进行追踪
    if (!isSymbol(key) || !builtInSymbols.has(key)) {
      track(target, TrackOpTypes.HAS, key);
    }
    return result; // 返回检查结果
  }

  ownKeys(target: object): (string | symbol)[] {
    //  触发trigger
    track(
      target,
      TrackOpTypes.ITERATE,
      isArray(target) ? 'length' : ITERATE_KEY
    ); // 追踪迭代操作
    return Reflect.ownKeys(target);
}

除了set以外,其他的操作实现原理都很简单,就是使用反射触发原有逻辑,然后触发一下trigger,trigger是什么之后会讲。

在set中,如果旧值readonly是ref并且新值不是ref的话,那么不允许赋值,如果非浅层,会通过toRaw获取新值和旧值的原始值,如果target非数组,旧值是ref但是新值不是,新值就赋值给旧值的value下,触发前文提到的ref中set的更新。

其他情况,通过 Reflect.set 设置值,最后触发一下trigger。

需要注意一下,这里通过判断了target是否等于receiver的原始值,如果相等,说明更改的非原型链上的值,才会正常触发trigger,反之如果更改的原型链的的属性就不触发,因为如果这里触发就会触两次trigger了——原型链上的先触发,这里再触发。

有人奇怪会是什么情况,这个情况确实不常见,但的确存在一种边界情况。我们引入一个例子

const obj0 = {a:1}
const obj1 = reactive(obj0)
const obj2 = Object.create(obj1)
const obj3 = reactive(obj2)

obj3或者obj2上,并不存在a属性,a属性是存在于原型链上的。因此打印a属性还是可以出来的。

那么当我尝试修改obj3的a属性,根据常识,我们并不可以修改原型链,因此我们实际结果是在obj3或者obj2上新建了一个a属性,并赋值为我们更改修改的值。

也就是说是set会触发了两次, Reflect.set的返回值两次都是true——即使原型链的并没有被修改。

  1. 第一次target是原型链上的obj0,receiver是obj3

  2. 第二次target是obj2,receiver是obj3

target天生跟代理对象,也就是receiver不相等,但receiver是由toRaw包裹的,而toRaw是递归获取代理对象的ReactiveFlags.RAW,根据get属性中的逻辑,ReactiveFlags.RAW又被转到对象建立响应式时候的target中。

也就是toRaw(receiver)后,结果是obj2。

也就是第二次的时候,会走进if语句中。

在if语句中,判断key之前存不存在,存在的话就走修改trigger,不存在就触发增加trigger,判断是否存在使用的hasOwn,就是Object.prototype.hasOwnProperty.call的封装。

好了,说了那么多,我们来看看trigger是什么东西

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
      // 没有依赖列表,返回
    return
  }

  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // 触发这个对象所有的副作用
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    // 如果是数组,触发类型是length 收集关于length的依赖以及index大于新值的所有依赖
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= newLength) {
        deps.push(dep)
      }
    })
  } else {
    // 触发key对应副作用
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }

    // 分情况触发对应副租用
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
            // 新增且对象情况下,ITERATE_KEY也需要收集触发
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
              // 新增且map情况下,MAP_KEY_ITERATE_KEY
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // 新增指定key的情况下,length也被依赖收集
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
            //删除情况下,非数组触发ITERATE_KEY
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
              // 删除情况下 map 为MAP_KEY_ITERATE_KEY
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        // 如果是map,修改的话,触发ITERATE_KEY
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

// 如果依赖为1
  if (deps.length === 1) {
      // 第一个存在
    if (deps[0]) {
        // 直接触发
       triggerEffects(deps[0])
    }
  } else {
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        // 过滤无效值
        effects.push(...dep)
      }
    }
    // 使用createDep包装后,createDep实际是Set,再用triggerEffects触发
     triggerEffects(createDep(effects))
  }
}

trigger就是依赖收集,因为修改了一个值后,并非只有当前值需要变动,相关数据也需要变动,而依赖那些数据的依赖也需要变动。

比如数组,当我push数组的时候,数组变化,会触发依赖数组元素的依赖,那么对数组length的依赖呢?也需要收集触发。

const arr = reactive([]);

effect(() => {
  console.log(arr[1])
});

// 此时数组是[0],无事发生
arr.push(0);

// 此时数组是[0,1] ,触发 effect,打印出arr[1]
arr.push(1);

上面的代码中,我们第一次访问了 state[1], 所以,对 state[1] 进行了依赖收集,而第一次的 state.push(0) 设置的是 state 的第 0 个元素,所以不会触发响应式更新。而第二次的 push 触发了对 state[1] 的更新。但依照这个逻辑,数组的map等遍历方法就无法使用响应式触发了,这反而很反直觉,那么实际上呢?

const arr = reactive([]);

// 观测变化
effect(() => {
    console.log(arr.map(item => item))
}
arr.push(1) //触发

实际上是触发的,这里面的原因就在依赖捕获上,如果直接访问数组元素,那么触发的get的ke只会是目标元素,但如果数组的方法会触发别的key,我们直接看一下。

const arr = []
const arr1= new Proxy(arr, {
  get(target, key,receiver) {
    console.log('get', key)
    return Reflect.get(...arguments)
  },
  set(target, key, value,receiver) {
    console.log('set', key)
    return Reflect.set(...arguments)
  }
})

arr1.map(v => v)

// get map
// get length
// get constructor

可以看到,触发map方法会触发get三次,分别是map,length,以及constructor,正是因为这个访问,才收集到了length的依赖,而我们push数据的时候改变了length,从而触发了对应的effect。

那么接着发散一下,对象呢?对象也是可迭代数据,但没有length啊,比如Object.keys,回忆一下MutableReactiveHandler之中,是不是有个ownKeys,是的,他是收集key的访问的,当使用对象迭代的时候,会触发这个拦截器。

这拦截器里面调用了track,关于track这个我们前面讲过了,就有依赖触发,没依赖就设置一个,然后触发。

那么我们接着看,最后的依赖会被triggerEffects触发

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
 //不是数组变成数组,注意变成数组的时候使用了rest,此时dep可能是Set,使用[...Set]会将Set内容转化成数组
  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)
    }
  }
}

我们看到实际上是对dep进行循环,如果dep不是数组,那么可能是Set,所以使用[...Set]把他转成数组,循环的时候先计算属性,再非计算属性,这个状态提升我们之后再讲。循环的单个dep 进入triggerEffect函数。

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

这个函数也很简单,就是有scheduler执行scheduler,否则执行run。当然这里这里也判断了effect是不是正在触发,防止多次同时触发。而effect是怎么来的,我们之后再说。

collectionHandlers

我们来看看collectionHandlers,collectionHandlers也分为四种

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}

export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, true)
}

export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(true, false)
}

export const shallowReadonlyCollectionHandlers: ProxyHandler<CollectionTypes> =
  {
    get: /*#__PURE__*/ createInstrumentationGetter(true, true)
  }

但这里就开始奇怪了,虽然他们都是使用同一个函数,怎么只有get。

难道没有set吗?

肯定不是,他们的实现逻辑同Vue2的数组类似,是通过拦截对应的方法,来实现响应式的。同时为了性能提升,并没有Vue2定义数据的时候,进行数据劫持,而是在访问的时候,也就是触发get的时候,进行数据劫持,这里的get并非map的get,而是代理的get。

我们看看createInstrumentationGetter是怎么实现的。

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
    // isReadonly shallow 两两组合出四个代理target
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
    ? readonlyInstrumentations
    : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
      // 处理ReactiveFlags,不挂载具体数据,通过get实现
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW) {
      return target
    }

    return Reflect.get(
    // 访问的key是不是在当前属性或者原型链上,是的话劫持,不是的话使用原target
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

由于Proxy限制,拦截map等数据结构不是很轻松,但依然有办法,当访问map.set或者map.get或者map.size等内置方法的时候,一定会触发代理的get,这个时候通过isReadonly和shallow,组合出合适的target,来替换之前的target,因为receiver存在,所以保证了this的指向是没问题,也可以通过this来获取原始数据,进行依赖收集。

instrumentations是createInstrumentations方法创建的,我们看看createInstrumentations的逻辑。

function createInstrumentations() {
  const mutableInstrumentations: Record<string, Function | number> = {
    get(this: MapTypes, key: unknown) {
    //依赖收集,如果是对象,响应化
      return get(this, key)
    },
    get size() {
      return size(this as unknown as IterableCollections)
    },
    has,
    add,
    set,
    delete: deleteEntry,
    clear,
    // 依赖收集,如果是对象,响应化
    forEach: createForEach(false, false)
  }

  const shallowInstrumentations: Record<string, Function | number> = {
    get(this: MapTypes, key: unknown) {
    // 依赖收集,不对获取到的数据响应式化
      return get(this, key, false, true)
    },
    get size() {
      return size(this as unknown as IterableCollections)
    },
    has,
    add,
    set,
    delete: deleteEntry,
    clear,
     // 依赖收集,不对获取到的数据响应式化
    forEach: createForEach(false, true)
  }

  const readonlyInstrumentations: Record<string, Function | number> = {
    // 依赖收集,如果是对象,只读化      
    get(this: MapTypes, key: unknown) {
    // readonly是true,不用get收集依赖
      return get(this, key, true)
    },
    get size() {
        // readonly是true,不用size收集依赖
      return size(this as unknown as IterableCollections, true)
    },
     // readonly是true,不用has收集依赖
    has(this: MapTypes, key: unknown) {
      return has.call(this, key, true)
    },
    add: createReadonlyMethod(TriggerOpTypes.ADD),
    set: createReadonlyMethod(TriggerOpTypes.SET),
    delete: createReadonlyMethod(TriggerOpTypes.DELETE),
    clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
    // 依赖收集,如果是对象,响应化
    forEach: createForEach(true, false)
  }

  const shallowReadonlyInstrumentations: Record<string, Function | number> = {
    // 不收集依赖,不对数据响应式化
    get(this: MapTypes, key: unknown) {
      return get(this, key, true, true)
    },
    // 不收集依赖
    get size() {
      return size(this as unknown as IterableCollections, true)
    },
     // readonly是true,不用has收集依赖
    has(this: MapTypes, key: unknown) {
      return has.call(this, key, true)
    },
    add: createReadonlyMethod(TriggerOpTypes.ADD),
    set: createReadonlyMethod(TriggerOpTypes.SET),
    delete: createReadonlyMethod(TriggerOpTypes.DELETE),
    clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
     // 不依赖收集,不对获取到的数据响应式化
    forEach: createForEach(true, true)
  }

  const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
  iteratorMethods.forEach(method => {
    mutableInstrumentations[method as string] = createIterableMethod(
      method,
      false,
      false
    )
    readonlyInstrumentations[method as string] = createIterableMethod(
      method,
      true,
      false
    )
    shallowInstrumentations[method as string] = createIterableMethod(
      method,
      false,
      true
    )
    shallowReadonlyInstrumentations[method as string] = createIterableMethod(
      method,
      true,
      true
    )
  })

  return [
    mutableInstrumentations,
    readonlyInstrumentations,
    shallowInstrumentations,
    shallowReadonlyInstrumentations
  ]
}

从上面可以看到,基本是对get、has、add、set、delete、clear、forEach的方法劫持,使用反射调用原来的逻辑,但在此之前,根据shallow和readonly来定义数据是否响应式化,是否收集依赖。

但接下来,对keys、values、entries、Symbol.iterator做了特殊处理,他们通过createIterableMethod方法进行拦截。

function createIterableMethod(
  method: string | symbol,
  isReadonly: boolean,
  isShallow: boolean
) {
  return function (
    this: IterableCollections,
    ...args: unknown[]
  ): Iterable & Iterator {
    const target = (this as any)[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    const targetIsMap = isMap(rawTarget)
    const isPair =
      method === 'entries' || (method === Symbol.iterator && targetIsMap)
    const isKeyOnly = method === 'keys' && targetIsMap
    const innerIterator = target[method](...args) // 调用目标对象的迭代器方法,并传入参数
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    !isReadonly &&
    // 如果非只读,进行依赖收集
      track(
        rawTarget,
        TrackOpTypes.ITERATE,
        isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
      )
    // 返回一个包装后的迭代器,该迭代器返回目标迭代器方法的观察版本
    return {
      // iterator protocol
      next() {
        const { value, done } = innerIterator.next()
        return done
          ? { value, done }
          : {
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
              done
            }
      },
      // iterable protocol
      [Symbol.iterator]() {
        return this
      }
    }
  }
}

也就是说,针对keys、values、entries、Symbol.iterator做的特殊处理,就是如果只读就不收集依赖,如果shallow就不对新结果响应式化。

Method Map.prototype.set called on incompatible receiver

如果我们模仿上文中对Map等数据结构代理,大概率会报这个错误。

const map = new Proxy(new Map([['name', 'lumozx']]), {
  get(target, p, receiver) {
    console.log('get');
    return Reflect.get(target, p, receiver);
  },
  set(target, p, value, receiver) {
    console.log('set');
    return Reflect.set(target, p, value, receiver);
  },
});
console.log(map.set('age', 20));

这里我用了反射,但为什么还不行呢?答案是Internal slots引起的。

Map、WeakMap等数据结构,会将数据保存在Internal slots(内部插槽)中,所以只有Map、WeakMap等这些数据对象在内部可以通过this访问到数据,但是代理Proxy是不存在这样的插槽的,当Map等这些数据对象被Proxy包装了之后,this就变成了Proxy对象,所以this自然就访问不到数据了。

解决办法是将this强制更正为原始数据对象,也就是bind target。

const map = new Proxy(new Map([['name', 'lumozx']]), {
  get(target, p, receiver) {
    console.log('get');
    return Reflect.get(target, p, receiver).bind(target);
  },
  set(target, p, value, receiver) {
    console.log('set');
    return Reflect.set(target, p, value, receiver).bind(target);
  },
});
console.log(map.set('age', 20));

这样就可以访问到了。

但问题是Vue3并没有使用bind,他们用了什么方法呢?回忆一下,他们确实没有bind target,但是他们自己造了一个target。

在这个target中,vue3使用了getProto,也就是Reflect.getPrototypeOf,获取了原始数据的原型,然后直接在原始数据的原型上调用对应的方法。

需要注意的是,这里获取到的this实际上是代理对象直接使用,会导致无限循环。反复触发代理对象对应拦截器

所以vue3中,使用toRaw还原了this,使用的代理之前的原始对象。然后在原始对象的原型上调用对应的方法。从而实现方法劫持。

effect

我们从第一节就讲effect,到现在前置知识已经补完,是应该了解一下这个是什么东西,或者是怎么来的。

我们从上文中了解到收集依赖,但是依赖是怎么来的呢?

我们回忆一下第一节的内容,在setupRenderEffect中,调用了 new ReactiveEffect

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())
update()

当前实例会挂载一个effect属性,那么我们看看ReactiveEffect是怎么处理的,怎么当依赖变动的时候,会触发执行componentUpdateFn。

//  初始化追踪深度为0
let effectTrackDepth = 0
// 定义追踪操作位为1,这个是二进制位
export left trackOpBit = 1
// 最大嵌套层数
const maxMarkerBits = 30
// 当前活跃的 effect
let activeEffect;

export class ReactiveEffect<T = any> {
  active = true; // 是否激活的标志
  deps: Dep[] = []; // 依赖数组
  parent: ReactiveEffect | undefined = undefined; // 父级effect

  computed?: ComputedRefImpl<T>; // 计算属性引用
  allowRecurse?: boolean; // 是否允许递归
  private deferStop?: boolean; // 是否延迟停止

  onStop?: () => void; // 停止时执行的回调函数
  constructor(
    public fn: () => T, // 响应式函数
    public scheduler: EffectScheduler | null = null, // 调度器,可为空
    scope?: EffectScope // effect的作用域
  ) {
    recordEffectScope(this, scope); // 记录effect的作用域
  }

  run() {
    if (!this.active) {
      return this.fn(); // 如果effect处于非激活状态,直接返回函数的执行结果,不进行依赖相关逻辑
    }
    let parent: ReactiveEffect | undefined = activeEffect; // 保存当前活动的effect
    let lastShouldTrack = shouldTrack; // 保存当前的追踪状态
    while (parent) {
      if (parent === this) {
        return; // 如果当前effect的父级是自身,则直接返回,避免无限递归
      }
      parent = parent.parent; // 否则,继续向上查找父级响应式效果
    }
    try {
      this.parent = activeEffect; // 将当前活动的effect设置为当前effect父级
      activeEffect = this; // 当前活动的effect为当前effect
      shouldTrack = true; // 设置追踪状态为true

      trackOpBit = 1 << ++effectTrackDepth; // 更新追踪操作位

      if (effectTrackDepth <= maxMarkerBits) {
        initDepMarkers(this); // 如果追踪深度不超过最大标记位数,初始化依赖标记
      } else {
        cleanupEffect(this); // 否则,清理响应式效果
      }
      return this.fn(); // 执行响应式函数
    } finally {
      if (effectTrackDepth <= maxMarkerBits) {
        finalizeDepMarkers(this); // 如果追踪深度不超过最大标记位数,完成依赖标记
      }

      trackOpBit = 1 << --effectdeferStopTrackDepth; // 恢复追踪操作位
      activeEffect = this.parent; // 恢复活动的 effect 为当前父级,当前父级可能是undefined,所以当所有effect执行完毕后,activeEffect肯定是undefined
      shouldTrack = lastShouldTrack; // 恢复追踪状态
      this.parent = undefined; // 清空父级,因为在effect中父级是不定的,没必要记载

      if (this.deferStop) {
        this.stop(); // 如果延迟停止标志为true,执行停止操作
      }
    }
  }

  stop() {
    // 在运行自身时停止 - 延迟清理
    if (activeEffect === this) {
      this.deferStop = true; // 如果活动的effect是自身,设置延迟停止标志为true
    } else if (this.active) {
      cleanupEffect(this); // 否则,清理响应式效果
      if (this.onStop) {
        this.onStop(); // 如果有停止时的回调函数,执行回调函数
      }
      this.active = false; // 将effect的激活状态设置为false
    }
  }
}

看一下,当new ReactiveEffect的时候,fun会保存为传入的函数,当使用run的时候,会查询当前effect与activeEffect是否是循环依赖,如果不是,那么当前的父依赖就是现在的activeEffect,然后将activeEffect赋值为此effect。也就是说让当前activeEffect就是此effect了。

此时再执行fun,fun获取到的响应式数据,都是在activeEffect为此effect的前提下进行依赖收集的。一旦触发响应式数据的get,就会触发track,从而触发trackEffects

我们回忆一下trackEffects的关键逻辑。

if (shouldTrack) {
   //收集依赖
   dep.add(activeEffect!)
   activeEffect!.deps.push(dep)
}

activeEffect 就是上文的effect。

当执行完trackEffects后,会接着执行ReactiveEffect.run中的finally,在finally中会执行依赖清理逻辑,如果有多个effect嵌套,就会逐个一层一层还原activeEffect,直至undefined。让activeEffect的指针有来有回,而不是让activeEffect固定在最深的依赖,这就是this.parent的逻辑。

这里需要注意一下清理依赖的逻辑。

依赖清理

假如说有这样一段代码。

const state = reactive({
  count: 0,
  isActive: true,
});

effect(() => {
  console.log('effect触发') 
  if (state.isActive) {
    console.log(`Count is: ${state.count}`);
  }
});

state.count++;
state.isActive = false;
state.count++; // 不会触发effect

显而易见,在第一次加载执行的时候,是没有问题的,然后执行 state.count++是会触发effect的,然后state.isActive = false,依然执行了effect。

但是,之后执行的state.count++却不会触发effect,虽然内部有非响应式数据,但对于响应式数据来说,这时候执行effect是没有意义的。因此从性能角度来说,需要删除effect的副作用函数。

不过在了解清理依赖之前,需要知道vue3对依赖做了什么样的标记,这样我们才能再必要的时候清理依赖。

//  当前追踪深度为0
let effectTrackDepth = 0
// 定义追踪操作位为1,这个是二进制位
export left trackOpBit = 1
// 最大嵌套层数
const maxMarkerBits = 30

在上文的逻辑中,因为没有超过最大层数,会在run中执行initDepMarkers。

export const initDepMarkers = ({ deps }: ReactiveEffect) => {
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].w |= trackOpBit
    }
  }
}

此时因为是初始化,deps是空数组,所以if逻辑不会执行。

然后执行this.fun,从而触发get收集依赖。然后进入trackEffects函数。我们回忆下trackEffects函数,主要看这串逻辑。

// 如果该依赖对象尚未被新追踪,将其标记为新追踪
if (!newTracked(dep)) {
   dep.n |= trackOpBit // 设置为新追踪
   shouldTrack = !wasTracked(dep) // 判断是否应该追踪该依赖对象
}
if (shouldTrack) {
   dep.add(activeEffect!)
   activeEffect!..push(dep)
}

这里面有newTracked和wasTracked两个方法

export const wasTracked = dep => (dep.w & trackOpBit) > 0

export const newTracked = dep => (dep.n & trackOpBit) > 0

实际上是进行位运算。其中的w是已经被收集过的。n是新收集的依赖。

当初始化的时候。state.isActive是true,的w和n都是false,所以会收集到依赖中。

然后函数结尾,activeEffect.dep会是下面的数据。

[
  {"w":0,"n": 00000000000000000000000000000010, [effect]},
  {"w":0,"n": 00000000000000000000000000000010, [effect]}
]

然后在finally执行finalizeDepMarkers逻辑

export const finalizeDepMarkers = (effect: ReactiveEffect) => {
  const { deps } = effect
  if (deps.length) {
    let ptr = 0
    for (let i = 0; i < deps.length; i++) {
      const dep = deps[i]
      // 如果依赖项之前被追踪,但新的追踪操作不再依赖它
      if (wasTracked(dep) && !newTracked(dep)) {
        dep.delete(effect)
      } else {
         // 如果依赖项是有效的,则保留在 deps 数组中,并更新指针位置
        deps[ptr++] = dep
      }
      // 清除标记位,确保下一轮追踪不受上一轮的影响
      dep.w &= ~trackOpBit
      dep.n &= ~trackOpBit
    }
    // 更新 deps 数组,截断无效的依赖项
    deps.length = ptr
  }
}

根据这些逻辑,如果新收集到的依赖里面没有已经收集的依赖,说明那些依赖是无效的,因此会移出,重新更新deps,也就是依赖列表。此时w和n会被重置为0。

也就是如下

[{"w":0, "n":0, [effect]},{"w":0, "n":0, [effect]}]

然后执行state.isActive = false 的时候,再次触发effect,然后走到 initDepMarkers,因为dep长度非0,已经收集了依赖,所以会进入isActive的依赖。然后执行trackEffects,此时的 newTracked = false,然后跟之前一样执行,但由于isAvtive是false,所以没有触发count的依赖收集。此时deps如下

[
  {
    "w": 00000000000000000000000000000010,
    "n": 00000000000000000000000000000010,
    [effect]
  },
  {
    "w": 00000000000000000000000000000010, 
    "n": 0,
    [effect]
  }
]

然后执行finalizeDepMarkers ,触发了delete删除,然后执行 state.coiunt++ 的操作,但是因为依赖已经没有count 了不会执行副作用函数。

如果当依赖层级大于30的时候,会触发cleanupEffect

function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

其实就是一个一个删除,然后下次收集的时候重新添加。

computed

那么计算属性是什么逻辑呢?

我们看看计算属性的源码

export function computed<T>(
  getter: ComputedGetter<T>,
  debugOptions?: DebuggerOptions
): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
 // 如果是函数类型,是只有get
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
      // 将函数赋值getter
    getter = getterOrOptions
    setter = NOOP
  } else {
    // 对象类类型,get赋值给getter
    getter = getterOrOptions.get
    // 对象类类型,set赋值给setter
    setter = getterOrOptions.set
  }
  // 创建 ComputedRefImpl 并返回
  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
  return cRef as any
}

从入参可以看出,计算属性可以接受函数类型,也接受对象类型,这个在文档里已经说过了,不过对于函数类型,他只是赋值给了getter,最后他们都会传入ComputedRefImpl,我们已经从文档中得知,new ComputedRefImpl的返回值就是一个ref响应式对象。那么我们看看ComputedRefImpl做了什么。

export class ComputedRefImpl<T> {
  public dep?: Dep = undefined; // 依赖对象

  private _value!: T; // 计算属性的值
  public readonly effect: ReactiveEffect<T>; // 对应的effect
  public readonly __v_isRef = true; // 标记为响应式引用
  public readonly [ReactiveFlags.IS_READONLY]: boolean = false; // 是否为只读,如果是函数就是true

  public _dirty = true; // 标志计算属性是否脏,是否需要重新计算
  public _cacheable: boolean; // 是否可以缓存计算结果

  constructor(
    getter: ComputedGetter<T>, // 计算属性的getter
    private readonly _setter: ComputedSetter<T>, // 计算属性的seter
    isReadonly: boolean, // 是否为只读,如果是函数就是true
    isSSR: boolean 
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true; // 如果不是脏的,设置为脏,并触发引用值的更新
        triggerRefValue(this);
      }
    });
    this.effect.computed = this; // 将当前计算属性与响应式效果关联
    this.effect.active = this._cacheable = !isSSR; // 计算属性在非SSR环境下默认为激活状态且可缓存
    this[ReactiveFlags.IS_READONLY] = isReadonly; // 设置只读标志
  }

  get value() {
    // 计算属性的值被其他代理对象(例如readonly())包裹可能性很高,见 #3376
    const self = toRaw(this); // 获取原始的计算属性对象
    trackRefValue(self); // 收集依赖
    if (self._dirty || !self._cacheable) {
      self._dirty = false; // 如果是脏的或者不可缓存,标记为非脏
      self._value = self.effect.run()!; // 重新计算计算属性的值
    }
    return self._value; // 返回计算属性的值,如果上面的if不通过。这个值可能是缓存的
  }

  set value(newValue: T) {
    this._setter(newValue); // 设置计算属性的值,触发设置函数
  }
}

整个逻辑还是比较简单的,在constructor的时候,创建了一个effect函数,在这里,设置了ReactiveEffect第二个参数——scheduler,在这里,我们先回忆一下,effect是如何触发的?我们知道effect中有一个run,触发effect的函数执行实际上就是执行run,但是run是谁触发的呢?

前面已经说的很明白了,在各种响应式上逻辑上,是ref set -> triggerRefValue / reactive track -> triggerEffects -> triggerEffect

如果是ref的话,会在set触发triggerRefValue,获取到dep,然后把dep传入triggerEffects,而reactive会在各个地方的set触发track,然后根据targetMap获取dep, 然后把dep传入triggerEffects。

这样triggerEffects就获取到了dep,然后遍历dep,每个都是effect,分情况先后使用triggerEffect触发effect,前面也讲过,effect有scheduler就执行scheduler,有run就执行run。

是的,我们可以说effect是triggerEffect触发的,但不一定触发run,因为有scheduler就触发scheduler。

这时候有人就问了,定义componentUpdateFn的effect的地方,也传了第二个参数,那么按理说依赖变动的时候,应该执行第二个参数,而不是componentUpdateFn。

很好,看的很仔细,但不够仔细,我们看看第二个参数是什么。

 () => queueJob(update),

是一个调度函数包装的update,最终会调用的update函数。那么update函数大家估计有印象了,我贴一下代码

const update: SchedulerJob = (instance.update = () => effect.run())
// ...
update()

也就是说componentUpdateFn的effect,经常调用的是scheduler,而scheduler其实是包装过的run。本质还是run,而run 最后执行了 componentUpdateFn。

因此,对componentUpdateFn的effect来说,scheduler约等于run,但别的effect不一定。

比如计算属性的这个。

this.effect = new ReactiveEffect(getter, () => {
  if (!this._dirty) {
     this._dirty = true;
     triggerRefValue(this);
   }
});

显而易见,这个effect会执行scheduler,但scheduler不会执行run,而是如果_dirty为false的时候,接着触发这个effect的依赖,换句话说,跳过了计算属性的run,或者说跳过了计算属性getter对应的函数的执行。

那么什么时候触发getter的执行呢?

我们知道计算属性返回包裹着value的对象,而根据代码来看value不是具体的值,而是进行了set和get劫持。

我们看看get value。

  get value() {
  // ...
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

如果_dirty为true,或者_cacheable为false才会执行里面的值,这里说明下,_cacheable是非SSR的情况下才为true,代表能否缓存值,也就是说非SSR情况下, !self._cacheable 一直为false。

那么我们把目光聚焦到前面的self._dirty ,_dirty默认是true。

所以第一次get value必定走这个函数,这里面把_dirty改为了false。然后执行了effect的run。

然后把结果值保存在_value上。

无论走不走if。都会返回缓存的_value,区别在于_value是刚缓存的还是很久之前缓存的。

那么有疑问了,既然 !self._cacheable是false,而刚刚我们把self._dirty也设置了false,那么这个if就永远也走不通了。永远使用第一次执行run缓存下来的值了。

我们别忘了scheduler,如果这个计算属性的依赖有变化,scheduler会执行,然后把_dirty设置为true。然后接着触发依赖这个计算属性的effect。

如果那些effect里面依赖这个计算属性的value,正好触发了get拦截器,然后再次设置_dirty为false,更新缓存值为最新的数据。返回缓存值。

我们捋一下流程

  1. get value触发 -> self._dirty设置为false -> 缓存最新的值 -> 返回最新的值

  2. 计算属性依赖更新 -> 触发scheduler -> ._dirty设置为true -> 触发依赖计算属性的更新 -> 计算属性value的get -> 第1步

  3. 如果计算属性的依赖没有更新 获取计算属性value的get -> 返回缓存的计算属性的值

需要注意的是,整篇文章虽然以【依赖】称呼,但实际上,当前的effect更像是dep的依赖。

当前的effect改变后,才会执行dep,让他使用与effect有关的最新的值。比如上文的计算属性,通过触发triggerRefValue,来让自己的dep获取自己最新的value值。

有的人就会问了,既然计算属性依靠的也是effect,那么岂不是在收集依赖的时候,effect可能存在先于计算属性执行的情况(因为effect是立刻执行)?写在计算属性下面的effect,获取上面的计算属性可能获取的是缓存值?这与vue2的直觉不符——计算属性的计算应该在生命周期非常靠前的。

是这样的,因此在triggerEffects里面用了两次循环,特殊处理了计算属性,优先循环是计算属性的effect,优先执行他的scheduler,让他的_dirty 为false,然后再执行依赖他的effects,然后再优先执行里面的计算属性的effect,当_dirty 为false的时候,这样获取这个计算属性的value的时候,就会执行他的run方法,获取他最新的值。

举个例子

import { ref, effect, computed  } from 'vue'

const num = ref(0)
const add = computed(() => num.value + 1)
effect(() => {
  console.log('num',num.value)
  console.log('add', add.value)
}) 
num.value++ 
// 初始化执行
// num 0
// add 1
// computed改变引起的effect执行
// num 1
// add 2
// num改变引起的effect执行
// num 1
// add 2

此时因为computed优先执行,因此打印的值 add优先变成了2,同时因为computed优先执行,依赖 他的effect也会被优先执行,因此第一次打印出 num 1 add 2的effect,实际上是computed触发的。

Watch

我们一直在讲响应式的基础——effect,那么看到effect的行为,我们想到了什么类似的api吗?答案是watch。而watch的实现其实就是校验了一下参数,然后原封不动调用了doWatch,所以我们直接看看doWatch的源码。

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object, // 监听的数据源,可以是单个数据、数组、响应式对象或watch effect函数
  cb: WatchCallback | null, // 数据变化时触发的回调函数
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ // 配置项,默认为空对象
): WatchStopHandle { // 返回一个用于停止监听的函数句柄

  const instance = getCurrentScope() === currentInstance?.scope ? currentInstance : null // 获取当前组件实例
  let getter: () => any // 获取数据的函数
  let forceTrigger = false // 是否强制触发回调函数
  let isMultiSource = false // 是否监听多个数据源

  // 判断数据源的类型,设置相应的getter函数
  if (isRef(source)) {
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    getter = () => source
    deep = true
  } else if (isArray(source)) {
    isMultiSource = true
    forceTrigger = source.some(s => isReactive(s) || isShallow(s))
    getter = () =>
      source.map(s => {
        if (isRef(s)) {
          return s.value
        } else if (isReactive(s)) {
          return traverse(s)
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
        }
      })
  } else if (isFunction(source)) {
    if (cb) {
      // 带有回调函数的getter
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
      // 没有回调函数的简单effect
      getter = () => {
        if (instance && instance.isUnmounted) {
          return
        }
        if (cleanup) {
          cleanup()
        }
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onCleanup]
        )
      }
    }
  } else {
    getter = NOOP
  }

  // 深度监听时,递归获取数据
  if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }

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

  // 在服务器端渲染时,无需设置实际的effect,应该是空操作,除非是eager或sync flush
  let ssrCleanup: (() => void)[] | undefined
  if (__SSR__ && isInSSRComponentSetup) {
    // 在这种情况下,无需调用invalidate回调(+ runner未设置)
    onCleanup = NOOP
    if (!cb) {
      getter()
    } else if (immediate) {
      callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
        getter(),
        isMultiSource ? [] : undefined,
        onCleanup
      ])
    }
    if (flush === 'sync') {
      const ctx = useSSRContext()!
      ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
    } else {
      return NOOP
    }
  }

  // 初始化oldValue
  let oldValue: any = isMultiSource
    ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
    : INITIAL_WATCHER_VALUE

  // 定义响应函数,用于触发回调函数
  const job: SchedulerJob = () => {
    if (!effect.active) {
      return
    }
    if (cb) {
      // watch(source, cb)
      const newValue = effect.run()
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
          : hasChanged(newValue, oldValue)) ||
        (__COMPAT__ &&
          isArray(newValue) &&
          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
      ) {
        // 在再次运行回调函数之前进行清理
        if (cleanup) {
          cleanup()
        }
        // 调用带有异步错误处理的回调函数
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // 在第一次变化时将旧值传递为undefined
          oldValue === INITIAL_WATCHER_VALUE
            ? undefined
            : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
            ? []
            : oldValue,
          onCleanup
        ])
        oldValue = newValue
      }
    } else {
      // watchEffect
      effect.run()
    }
  }

  // 将该job标记为一个watcher回调函数,以便scheduler知道它可以自触发
  job.allowRecurse = !!cb

  let scheduler: EffectScheduler
  if (flush === 'sync') {
    scheduler = job as any // 直接调用scheduler函数
  } else if (flush === 'post') {
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // 默认为'pre'
    job.pre = true
    if (instance) job.id = instance.uid
    scheduler = () => queueJob(job)
  }

  // 创建ReactiveEffect实例
  const effect = new ReactiveEffect(getter, scheduler)

  // 初始运行
  if (cb) {
    if (immediate) {
      job()
    } else {
      oldValue = effect.run()
    }
  } else if (flush === 'post') {
    queuePostRenderEffect(
      effect.run.bind(effect),
      instance && instance.suspense
    )
  } else {
    effect.run()
  }

  // 返回一个函数,用于停止监听
  const unwatch = () => {
    effect.stop()
    if (instance && instance.scope) {
      remove(instance.scope.effects!, effect)
    }
  }

  // 在服务器端渲染时,将unwatch函数加入到ssrCleanup数组中
  if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
  return unwatch
}

首先,会根据传入的数据源,设置不同的监听逻辑。

  • 如果是ref,那么getter函数返回的就是数据源的value,并且使用isShallow来判断forceTrigger是否是true,从而能否强制触发回调函数

  • 如果是reactive,那么getter函数返回的就是数据源本身,默认是深度监听

  • 如果是非响应式的数组,那么这个数组可能是多个数据源组成,那么就需要监听多个数据源,如果数组有一个是reactive或者shallow,那么forceTrigger就是true,就会强制触发回调函数,getter函数返回的的数组,数组使用map,将每一项都依照上面的逻辑进行处理。如果是reactive ,就使用traverse处理,返回一个set,触发内部所有的get,从而收集到依赖。

  • 如果是函数,那么getter函数返回的就是函数的返回值,没有入参

  • 其他情况,getter就是一个无返回值空函数

在这里遇到了两个没有解释很清楚的逻辑,一个是forceTrigger,一个是traverse。

forceTrigger,上文中我们讲过是用来强制触发回调函数的,那么为什么要强制触发呢?

因为一般来说,watch中只有值变化的时候,才会触发回调函数,也就是新值不等于旧值,但是如果getter函数返回的是对象,或者经过shallow浅层化的响应式数据,那么旧值一定等于新值,因此就不能单纯判断是否相等,所以这里的逻辑是只要发送变化,那么就触发回调函数,而控制这个逻辑的开关,就是forceTrigger。

当然,这里需要补充的是,watch归根结底都是监听的是响应式数据,也就是如果你使用shallow浅层化了一个响应式数据,那么修改未经响应化的深层,也不会触发回调,而修改经过响应式化的数据,由于watch 是基于effect,而effect的触发是比较新旧值的,因此即使是forceTrigger,也会由于effect新旧值比较相同,而不会触发effect的run,从而不会触发回调函数。

针对ref对象,watch默认deep是false,因此构建effect的时候,不会使用traverse,而只是收集到value的浅层依赖 getter = () => source.value。

针对shallowRef,因为只针对value本身被赋值的情况进行响应,因此也无法收集到深层依赖。

针对reactive,默认使用深度监听,也就是使用traverse触发内部所有的get。

那么接下来,需要了解traverse具体实现了

export function traverse(value: unknown, seen?: Set<unknown>) {
  // 如果不是对象或者具有ReactiveFlags.SKIP标志的属性,直接返回该值
  if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
    return value
  }
  
  // 初始化一个Set来存储已经遍历过的对象,避免循环引用
  seen = seen || new Set()
  
  // 如果该对象已经被遍历过,直接返回该值
  if (seen.has(value)) {
    return value
  }
  
  // 将当前对象加入已遍历集合中,避免循环引用
  seen.add(value)
  
  // 如果是响应式引用(Ref对象),递归遍历其内部的值
  if (isRef(value)) {
    traverse(value.value, seen)
  } 
  // 如果是数组,递归遍历数组的每个元素
  else if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen)
    }
  } 
  // 如果是集合(Set对象)或者映射(Map对象),递归遍历集合或者映射的每个元素
  else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => {
      traverse(v, seen)
    })
  } 
  // 如果是普通对象,递归遍历对象的每个属性值
  else if (isPlainObject(value)) {
    for (const key in value) {
      traverse(value[key], seen)
    }
  }
  
  // 返回遍历后的响应式对象
  return value
}

我们看到,所有嵌套的数据结构都会被转换成响应式对象,并且使用了Set,因此不会出现循环依赖的问题,只有可以在数据发生变化时触发相应的更新。也就是我们所说的deep : true。

因此,我们也可以得到跟文档相同的结论。在数组中的响应式对象,也会被深度监听。

接下里我们继续看看函数逻辑,当存在回调且是深度监听的时候,使用traverse包装getter函数,这个时候还不会触发traverse。

接着定义了副作用清理函数onCleanup,副作用清理函数会 传入的回调函数的第三个值。并且接受一个函数。

这个函数将会在下次effect的调度函数触发的时候触发。而下次effect调度函数触发的时候,实际上也是watch回调函数的触发的时候。也就是说,副作用清理函数的参数是一个函数A,每次触发effect,如果上一次触发effect的时候传入了A函数,那么就触发他。

同时onCleanup还会挂载在effect的onStop上,这样销毁的时候也会触发onCleanup(下文会讲)

官方给予的使用场景是

watch(id, async (newId, oldId, onCleanup) => {
  const { response, cancel } = doAsyncWork(newId)
  // 当 `id` 变化时,`cancel` 将被调用,
  // 取消之前的未完成的请求
  onCleanup(cancel)
  data.value = await response
})

如果连续触发回调函数,且以最新的回调函数的结果为准,那么可以将本次回调函数清理副作用逻辑的函数传给onCleanup,那么将会在下次触发回调函数之前,触发他。

再然后省略SSR的逻辑。之后初始化oldValue,也就是旧值。但这里只是初始化一个结构——INITIAL_WATCHER_VALUE,实际是一个空,这里旧值还会分情况初始化,如果是源数据是数组的话,会初始化,相同长度空对象数组。

然后是响应函数的构建。我们之前讲过,类似 () => queueJob(update) 的update,当依赖的响应式数据有变化的时候,会执行这个update。

构建完响应函数,会给予这个函数allowRecurse属性,如果存在回调函数,这个allowRecurse是true,代表可以递归调用。

接下来会根据传入的flush,将构建不同的scheduler,scheduler如同之前的 () => queueJob(update)

如果flush是sync,也就是watchSyncEffect,那么scheduler就是响应函数,不会经过queueJob进行排队。

如果flush是post,也就是watchPostEffect,那么响应函数会通过queuePostRenderEffect进行包装。queuePostRenderEffect在当前渲染周期结束后执行。也就是延后执行。

默认flush是pre,在这里,scheduler会被包装成 () => queueJob(job),默认是渲染更新之前执行。

然后构建effect,依赖收集的函数就是上文的getter。调度函数就是刚刚创建的scheduler。

如果回调函数存在,且immediate为true,就会立即执行响应函数,响应函数会获取当前getter的结果,作为新值,如果旧值依然是INITIAL_WATCHER_VALUE,那么就是undefined,如果旧值是数组,判断的是数组的第一个值是不是INITIAL_WATCHER_VALUE 那么就是[]。然后新值赋值给旧值。

如果immediate不是true。那么就会使用effect.run(),收集依赖的同时,会获取返回值,然后赋值给旧值。

如果flush是post,那么effect.run()的执行会使用bind保存指针,然后延后。

如果没有回调函数,会直接执行effect.run()收集依赖。

最后定义取消监听函数并返回,本质是执行effect的stop中的cleanupEffect(this.active默认为true),清除所有的依赖,然后将当前effect从实例中移除。如果之前注入了onStop,也就是给onCleanup传了函数,还会执行这个函数。

小结

这节我们讲了effect相关的逻辑——包依赖收集、依赖清理、相关函数的实现逻辑,来让我们对整个vue的响应式更进一步了解,当然,这还不够,我们还需要大致了解编译器以及人任务队列相关知识,以及vue内部组件的实现原理,因此需要学习的东西还有很多,我们依然在路上。

0
博主关闭了所有页面的评论