美文网首页让前端飞Web前端之路
Vue3源码--响应式原理1(effect)

Vue3源码--响应式原理1(effect)

作者: 勤奋的大鱼 | 来源:发表于2020-03-07 22:56 被阅读0次

 最近学习了下Vue3的源码,抽空写一些自己对3.x源码的解读,同时算是学习的一个总结吧,也能加深自己的印象。
 就先从3.x的响应式系统说起吧。

回忆

 首先大概回忆一下2.x的响应式系统,主要由这几个模块组成,Observer,Watcher,Dep。
Observer负责通过defineProperty劫持Data,每个Data都各自在闭包中维护一个Dep的实例,用于收集依赖着它的Watcher。Dep维护一个公共的Target属性,用于保存当前的需要被收集依赖的Watcher。每次Data被劫持的getter执行的时候,如果Dep.Target!==undefine, dep和Watcher实例就互相收集对方~
 2.x的响应式系统其实是围绕着Watcher,也可以说围绕着watch API的,包括render是一个renderWatcher,computed是通过lazyWatcher实现。这并不是一个好的设计模式,不符合六个设计原则的(单一职责原则,开闭原则)。而响应式系统也无法独立出来。

对比

 那么3.x是怎样实现这一块的内容的呢。
 首先3.x响应式系统相关的代码在packages/reactivity/src里。3.x的响应式系统的核心由两个模块构成: effect, reactive。
 reactive模块的功能比较简单,就是给数据设置代理,类似于2.x的Observer,不同的点在于是用的Proxy去做代理。
 effect模块,传入一个函数,然后让这个函数需要被响应式数据影响,目前具体在3.x中包括,watch API,computed API,还有组件的更新都是依赖effect实现的,但是这个模块没有暴露在Vue对象上面。所以说effect模块是一个偏向于底层只有基础功能的模块,相比2.x,这明显是一个较好的设计模式。

Effect

 关于effect模块,最主要的是里面的effect,track,trigger三个方法。
 effect方法是一个高阶函数,或者也可以说是工厂方法,接收一个函数作为参数,返回一个effect实例方法,它使这个函数中的响应式数据可追踪到这个effect实例,如果有响应式数据发生了改变,就会再次执行这个effect,可以参照源码中调用这个方法的三个地方computed.ts,apiWatch.ts,renderer.ts
 首先来看看track:以下是track方法的主要逻辑以及注释,track方法按字面的解释就是追踪,会在数据Proxy的get代理中调用,track这个数据本身。其实简单说就做了一件事情,把当前的active effect收集到响应式数据的depsMap里面。
其实并不复杂,这里和2.x不同的是,2.x是每个数据各自都在闭包中维护deps对象,这里是用一个全局的Store去保存响应式数据影响的effects,实现了模块的解耦。

// target为传入的响应式数据对象,type为操作类型,key为target上被追踪的key
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 如果shouldTrack为false 或者 当前没有活动中的effect,不需要执行追踪的逻辑
  // shouldTrack为依赖追踪提供一个全局的开关,可以很方便暂停/开启,比如用于setup以及生命周期执行的时候
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // 所有响应式数据都是被封装的对象,所以用一个Map来保存更方便,Map的key为响应式数据的对象
  let depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 同样为每个响应式数据按key建立一个Set,用来保存target[key]所影响的effects
  let dep = depsMap.get(key)
  if (dep === void 0) {
    // 用一个Set去保存effects,省去了去重的判断
    depsMap.set(key, (dep = new Set()))
  }
  // 如果target[key]下面没有当前活动中的effect,就把这个effect加入到这个deps中
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

 看完track方法的逻辑之后,effect方法的主要逻辑其实就呼之欲出了,那就是启动响应式追踪---设置shouldTrack为true,设置activeEffect为当前的effect,然后再调用传入的方法并追踪依赖,最后返回一个封装后的实例effect方法。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // createReactiveEffect是一个工厂方法,返回一个函数实例
  const effect = createReactiveEffect(fn, options)
  // 如果不是lazy effect(lazy effect主要用于computed),立即执行这个effect
  if (!options.lazy) {
    effect()
  }
  return effect
}

// createReactiveEffect是一个工厂方法,返回一个函数实例
function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    return run(effect, fn, args)
  } as ReactiveEffect
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}
function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  // 如果effect.active为false,跳过追踪直接调用传入的函数
  if (!effect.active) {
    return fn(...args)
  }
  if (!effectStack.includes(effect)) {
    // 清除effect中之前记录的deps
    cleanup(effect)
    try {
      // 设置shouldTrack为true
      enableTracking()
      // 设置activeEffect为当前的effect,另外把当前的effect入栈(比如渲染子组件的时候,这个栈就起作用了)
      effectStack.push(effect)
      activeEffect = effect
      // 执行传入effect的函数
      return fn(...args)
    } finally {
      effectStack.pop()
      // 设置shouldTrack为上一次的shouldTrack(注:和effect一样,shouldTrack也有一个栈)
      resetTracking()
      // 设置activeEffect为上一个activeEffect
      activeEffect = effectStack[effectStack.length - 1]
    }
  }
}

 最后来看一下trigger方法,trigger方法的调用在Proxy的set代理中,作用就是在修改一个响应式数据的时候,执行这个响应式对象的depsMap中所有的effect。

// target为修改的响应式数据对象,type为操作类型,key为target上具体修改的参数
// newValue,oldValue, oldTarget都很好理解
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 === void 0) {
    // never been tracked
    return
  }
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  // 如果操作类型是CLEAR,说明数据类型是Map,或者Set(注意,3.x的响应式系统是支持Map和Set的)
  // CLEAR操作需要触发集合上的所有属性的effects
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(dep => {
      // addRunners功能其实很简单,就是区分这个effect是普通的effect还是一个computed effect
      addRunners(effects, computedRunners, dep)
    })
  // 如果是更改length长度,说明是个数组,只需要触发key在这个新的length之后的数据
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        addRunners(effects, computedRunners, dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      // 大部分的情况,触发这个key下面的effets
      addRunners(effects, computedRunners, depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE | Map.SET
    if (
      type === TriggerOpTypes.ADD ||
      type === TriggerOpTypes.DELETE ||
      (type === TriggerOpTypes.SET && target instanceof Map)
    ) {
      // 如果是添加/删除数组里的项,或者Set,Map的add,delete,set几个方法,同时也会改变length或者size,
      // 在Map和Set里面,受size影响的一些方法(比如size,forEach,entries,keys,values),都会把effect收集到ITERATE_KEY里面。
      // 具体可参考packages/reactivity/src/collectionHandler.ts里面的实现
      const iterationKey = isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }
  const run = (effect: ReactiveEffect) => {
    scheduleRun(
      effect,
      target,
      type,
      key,
      __DEV__
        ? {
            newValue,
            oldValue,
            oldTarget
          }
        : undefined
    )
  }
  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  // run每个effect
  computedRunners.forEach(run)
  effects.forEach(run)
}

// addRunners功能其实很简单,就是区分这个effect是普通的effect还是一个computed effect
// 普通的effect存在effects里面,computed effect存在computedRunners里面
function addRunners(
  effects: Set<ReactiveEffect>,
  computedRunners: Set<ReactiveEffect>,
  effectsToAdd: Set<ReactiveEffect> | undefined
) {
// 省略
}
// 调度将要执行的effect,是否传入effect.options.scheduler决定了执行的方式
// 若没有传入,就立即同步执行,若有,则执行调度方法,传入effect
// 3.x中关于异步调度方法的实现可以查看packages/runtime-core/src/scheduler.ts中的queueJob方法
function scheduleRun(
  effect: ReactiveEffect,
  target: object,
  type: TriggerOpTypes,
  key: unknown,
  extraInfo?: DebuggerEventExtraInfo
) {
  if (effect.options.scheduler !== void 0) {
    effect.options.scheduler(effect)
  } else {
    effect()
  }
}

 以上源码都是基于 vue-next-alpha8 版本。
 effect模块相关的内容就这些,下一篇是关于reactive模块的。

相关文章

  • Vue3源码--响应式原理1(effect)

     最近学习了下Vue3的源码,抽空写一些自己对3.x源码的解读,同时算是学习的一个总结吧,也能加深自己的印象。 就...

  • 面试总结之基础(2)

    Vue2响应式原理 Vue3响应式原理

  • 【浅谈Vue3 effect】

    Vue3 中引入了 proxy进行数据劫持,而effect是响应式系统的核心,而响应式系统又是 vue3 中的核心...

  • 2021-07-23 vue2与vue3的响应式原理

    vue2的响应式原理 无法响应对象的新增与删除 vue3的响应式原理

  • Vue3

    Vue3启程 关键字:创建vue实例、响应式、响应式原理、组合式API 1. 初始Vue3 Vue2存在一些缺陷,...

  • Vue

    vue双向绑定原理及实现从零带你手把手实现Vue3响应式原理-上从零带你手把手实现Vue3响应式原理-下为什么说 ...

  • 36.响应式原理

    手写响应式原理-vue3 手写响应式原理-vue2 非常感谢王红元[https://link.juejin.cn/...

  • Vue3响应式原理傻瓜式教程(二)——Proxy & Refle

    上一节我们学到了响应式的简单原理:Vue3响应式原理傻瓜式教程(一)——Reactive - 简书 (jiansh...

  • 🥤 简述:Vue2和Vue3开发区别

    响应式原理api的改变Vue2响应式原理采用的是defineProperty,而vue3选用的是proxy。这两者...

  • Vue数据劫持原理-源码分析

    响应式原理分析 原理流程图概览image.png 响应式相关的源码在(源码位于instance/index.js)...

网友评论

    本文标题:Vue3源码--响应式原理1(effect)

    本文链接:https://www.haomeiwen.com/subject/owldlhtx.html