vue3响应式原理

vue3响应式原理

在这里插入图片描述

简单回顾vue2

  • vue2响应式的一些问题:
    • 响应化过程需要遍历data,props等,消耗较大
    • 不支持Set/MapClass、数组等类型
    • 新加的或者删除属性无法监听
    • 数组响应化需要额外实现
    • 对应的修改语法有限制

vue3响应式处理

  • vue3响应式原理:使用ES6proxy来解决这些问题

vue3的响应式流程并没有多大变化:依然是vue2的那一套(数据劫持+收集依赖+派发更新),只不过vue3更换了api,做了一些优化

effect

effectvue3响应式系统的核心,而响应式又是vue3的核心,所以从effect开始讲。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
   
  if (isEffect(fn)) {
   
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
   
    effect()
  }
  return effect
}

参数:

fn: 传入的回调函数

options: 可选配置参数,说明如下表

参数 含义
lazy 是否延迟触发 effect
computed 是否为计算属性
scheduler 调度函数
onTrack 追踪时触发
onTrigger 触发回调时触发
onStop 停止监听时触发
export interface ReactiveEffectOptions {
   
  lazy?: boolean
  computed?: boolean
  scheduler?: (job: ReactiveEffect) => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  onStop?: () => void
}

介绍完可配置参数options后,接着讲回调函数。当传入的回调函数是effect时,取出effect中的原始值然后调用createReactiveEffect创建新的effect;如果传入的optionslazy不为true,则立即调用我们刚刚创建的 effect, 最后返回刚刚创建的 effect

createReactiveEffect是怎么创建effect

function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
   
  const effect = function reactiveEffect(...args: unknown[]): unknown {
   
    if (!effect.active) {
   
      return options.scheduler ? undefined : fn(...args)
    }
    if (!effectStack.includes(effect)) {
   
        
      // 清空之前的依赖
      cleanup(effect)
      try {
   
        // 开启跟踪
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn(...args)
      } finally {
   
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

reactiveEffect 如果 effect 不是激活状态,这种情况发生在我们调用了 effect 中的 stop 方法之后,那么先前没有传入调用 scheduler 函数的话,直接调用原始方法fn,否则直接返回。首先判断是否当前 effect 是否在 effectStack 当中,如果在,则不进行调用,这个主要是为了避免死循环。

effect挂载属性

effect 挂载属性 含义
id 自增id, 唯一标识effect
_isEffect 用于标识方法是否是effect
active effect 是否激活
raw 创建effect是传入的fn
deps 持有当前 effect 的dep 数组
options 创建effect是传入的options

接着是清除依赖,每次 effect 运行都会重新收集依赖, deps 是持有 effect 的依赖数组,其中里面的每个 dep 是对应对象某个 key 的 全部依赖,我们在这里需要做的就是首先把 effectdep 中删除,最后把 deps 数组清空。

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

清除完依赖,就开始重新收集依赖。首先开启依赖收集,把当前 effect 放入 effectStack 中,然后将 activeEffect 设置为当前的 effectactiveEffect 主要为了在收集依赖的时候使用(在下面会很快讲到),然后调用 fn 并且返回值,当这一切完成的时候,finally 阶段,会把当前 effect 弹出,恢复原来的收集依赖的状态,还有恢复原来的 activeEffect

 try {
   
    enableTracking()
    effectStack.push(effect)
    activeEffect = effect
    return fn(...args)
  } finally {
   
    effectStack.pop()
    resetTracking()
    activeEffect = effectStack[effectStack.length - 1]
  }

effect 是怎么收集依赖的呢?vue3 利用 proxy 劫持对象,在上面运行 effect 中读取对象的时候,当前对象的 key 的依赖 set集合 会把 effect 收集进去。

export function track(target: object, type: TrackOpTypes, key: unknown) {
   
  ...
}

vue3reactive 中触发 track 函数,reactive 会在单独的章节讲。触发 track 的参数中,object 表示触发 track 的对象, type 代表触发 track 类型,而 key 则是 触发 trackobjectkey。在下面可以看到三种类型的读取对象会触发 track,分别是 gethasiterate

export const enum TrackOpTypes {
   
  GET = 'get',
  HAS = 'has',
  ITERATE = 'iterate'
}

回到 track 内部,如果 shouldTrackfalse 或者 activeEffect 为空,则不进行依赖收集。接着 targetMap 里面有没有该对象,没有新建 map,然后再看这个 map 有没有这个对象的对应 key 的 依赖 set 集合,没有则新建一个。 如果对象对应的 key 的 依赖 set 集合也没有当前 activeEffect, 则把 activeEffect 加到 set 里面,同时把 当前 set 塞到 activeEffectdeps 数组。最后如果是开发环境而且传入了 onTrack 函数,则触发 onTrack。 所以 deps 就是 effect 中所依赖的 key 对应的 set 集合数组, 毕竟一般来说,effect 中不止依赖一个对象或者不止依赖一个对象的一个key,而且 一个对象可以能不止被一个 effect 使用,所以是 set 集合数组。

if (!shouldTrack || activeEffect === undefined) {
   
    return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
   
    targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
   
    depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
   
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
   
        activeEffect.options.onTrack({
   
            effect: activeEffect,
            target,
            type,
            key
        })
    }
}

依赖都收集完毕了,接下来就是触发依赖。如果 targetMap 为空,说明这个对象没有被追踪,直接return

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) {
   
    // never been tracked
    return
  }
  ...
}

其中触发的 type, 包括了 setadddeleteclear

export const enum TriggerOpTypes {
   
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear'
}

接下来对 key 收集的依赖进行分组,computedRunners 具有更高的优先级,会触发下游的 effects 重新收集依赖。

const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()

add 方法是将 effect 添加进不同分组的函数,其中 effect !== activeEffect 这个是为了避免死循环,在下面的注释也写的很清楚,避免出现 foo.value++ 这种情况。至于为什么是 set 呢,要避免 effect 多次运行。就好像循环中,set 触发了 trigger ,那么 ITERATE 和 当前 key 可能都属于同个 effect,这样就可以避免多次运行了。

const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
   
    if (effectsToAdd) {
   
      effectsToAdd.forEach(effect => {
   
        if (effect !== activeEffect || !shouldTrack) {
   
          if (effect.options.computed) {
   
            computedRunners.add(effect)
          } else {
   
            effects.add(effect)
          }
        } else {
   
          // the effect mutated its own dependency during its execution.
          // this can be caused by operations like foo.value++
          // do not trigger or we end in an infinite loop
        }
      })
    }
}

下面根据触发 key 类型的不同进行 effect 的处理。如果是 clear 类型,则触发这个对象所有的 effect。如果 keylength , 而且 target 是数组,则会触发 keylengtheffects ,以及 key 大于等于新 lengtheffects, 因为这些此时数组长度变化了。

if (type === TriggerOpTypes.CLEAR) {
   
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
   
    depsMap.forEach((dep, key) => {
   
      if (key === 'length' || key >= (newValue as number)) {
   
        add(dep)
      }
    })
} 

下面则是对正常的新增、修改、删除进行 effect 的分组, isAddOrDelete 表示新增 或者不是数组的删除,这为了对迭代 key的 effect 进行触发,如果 isAddOrDeletetrue 或者是 map 对象的设值,则触发 isArray(target) ? 'length' : ITERATE_KEY 的 effect ,如果 isAddOrDeletetrue 且 对象为 map, 则触发 MAP_KEY_ITERATE_KEYeffect

else {
   
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
   
      add(depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE | Map.SET
    const isAddOrDelete =
      type === TriggerOpTypes.ADD ||
      (type === TriggerOpTypes.DELETE && !isArray(target))
    if (
      isAddOrDelete ||
      (type === TriggerOpTypes.SET && target instanceof Map)
    ) {
   
      add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
    }
    if (isAddOrDelete && target instanceof Map) {
   
      add(depsMap.get(MAP_KEY_ITERATE_KEY))
    }
}

最后是运行 effect, 像上面所说的,computed effects 会优先运行,因为 computed effects 在运行过程中,第一次会触发上游把cumputed effect收集进去,再把下游 effect 收集起来。

还有一点,就是 effect.options.scheduler,如果传入了调度函数,则通过 scheduler 函数去运行 effect, 但是 scheduler 里面可能不一定使用了 effect,例如 computed 里面,因为 computed 是延迟运行 effect, 这个会在讲 computed 的时候再讲。

const run = (
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值