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

 最近学习了下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模块的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容