reactivity

这个 Q 详细研究了其中 reactivity 部分源码实现:

Vue 3.0 版响应式系统支持 基本对象/readonly/collectionMap(Set/Map) 的响应式处理。

以基本对象为例:

// demolet value1, value2
const target = { num: 0 }
const state = reactive(target)
const cValue = computed(() => state.num + 1)

let reactiveEffect1 = effect(() => {
  value1 = state.num  // 依赖收集
})
let reactiveEffect2 = effect(() => {
  value2 = state.num  // 依赖收集
})
console.log(value1, value2, cValue.value) // 0, 0, 1
state.num++  // 触发依赖更新console.log(value1, value2, cValue.value) // 1, 1, 2

我们可以看到其中使用到的几个核心函数 reactivecomputedeffect

简单介绍一下这几个函数,以下代码经过大量精简。

reactive

const targetMap = new WeakMap()  // 依赖收集的核心存储位置
const rawToReactive = new WeakMap()  // 标记已 proxy 的对象
const reactiveToRaw = new WeakMap()  // 标记已 proxy 的对象
const effectStack = []  // 存储正在运行的 effect 实例
function reactive(target) {
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers
  )
}
function createReactiveObject(target, toReactive, toRaw, baseHandlers) {
  let observed = new Proxy(target, baseHandlers)
  toReactive.set(target, observed)
  toRaw.set(observed, target)
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

可以看到,reactive 函数本质返回的是个 Proxy 实例,用以定义 target 对象的响应式行为,同时会标记该对象,避免重复 proxy,具体行为 baseHandlers 会在下面介绍。

baseHandlers

以 get、set 为例。

// baseHandlers: {
//   track: get, has, ownKeys
//   trigger: set, deleteProperty
// }
function get(target, key, receiver) {
  const res = Reflect.get(target, key, receiver)
  track(target, 'get', key)
  // 嵌套对象的响应式,触发属性访问时才实现,优化了 Vue2.x 递归实现。
  return isObject(res) ? reactive(res) : res
}

function set(target, key, value, receiver) {
  value = toRaw(value)
  const hadKey = hasOwn(target, key)
  const oldValue = target[key]
  const result = Reflect.set(target, key, value, receiver)
  // 如果 target 在原始原型链的上游,不触发
  // 举例:Object.setPrototypeOf(someObj, target); someObj.foo = 'bar';
  if (target === toRaw(receiver)) {
    const extraInfo = { oldValue, newValue: value }
    if (!hadKey) {
      trigger(target, 'add', key, extraInfo)
    } else if (hasChanged(value, oldValue)) {
      trigger(target, 'set', key, extraInfo)
    }
  }
  return result
}

这里值得注意的是对于多层嵌套对象,Vue 2.x 实现的响应式,会在初始化时便对 target 递归调用 defineProperty,Vue 3.0 只会在属性访问时触发 reactive,节省了性能。此外,属性修改新增都会触发 set 行为,这里也通过 hasOwnProperty 做了判断区分。

baseHandlers 定义的其它行为方法有:

effect

effect 函数是触发依赖收集的关键,所以还是得先于 track 函数介绍。

function effect(fn, options = EMPTY_OBJ) {
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect() // !!!
  }
  return effect
}
function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect(...args) {
    return run(effect, fn, args) // !!!
  } // !!!
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}
function run(effect, fn, args) {
  if (!effect.active) {
  return fn(...args)
  }
  if (!effectStack.includes(effect)) {
    cleanup(effect)
    try {
      effectStack.push(effect)
      // 回调函数执行前 reactiveEffect 入栈了
      return fn(...args) // !!!
    } finally {
      effectStack.pop()
    }
  }
}

// 回顾之前的 effect 调用
let activeEffect1 = effect(() => {
  value1 = state.num  // 依赖收集
})

简单概括下:effect 函数执行返回了一个 effect 实例 reactiveEffect,在非 lazy 状态下,会同步调用 run 函数,当前 reactiveEffect 实例会入栈 effectStack,同时调用传入 effect 的回调函数,回调函数内部 state.num 触发了之前 baseHandlers.get 行为,至此,依赖收集正式开始了!!

track

function track(target, type, key) {
  if (!shouldTrack || effectStack.length === 0) {
    return
  }
  const effect = effectStack[effectStack.length - 1]
  // 触发前置条件:effect 回调内触发 baseHandlers 中 track 行为的方法
  if (effect) {
    if (type === 'iterate' /* ITERATE */) {
      key = ITERATE_KEY
    }
    // 下面干的事就是在 targetMap 中设置相应 key 和依赖函数
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (dep === void 0) {
      depsMap.set(key, (dep = new Set()))
    }
    if (!dep.has(effect)) {
      dep.add(effect)
      effect.deps.push(dep)
      if (effect.onTrack) {
        effect.onTrack({
          effect,
          target,
          type,
          key
        })
      }
    }
  }
}

// 回顾之前的代码let value1, value2
const target = { num: 0 }
const state = reactive(target)

let reactiveEffect1 = effect(() => {
  value1 = state.num  // 依赖收集
})
let reactiveEffect2 = effect(() => {
  value2 = state.num  // 依赖收集
})

可以看到,依赖收集有个前置条件:拿到 effectStack 栈顶的 reactiveEffect 实例。所以如果你不在 effect 回调内访问 state.num 是不会触发依赖收集的。

依赖收集 主要做的就是建立并保存 keyreactiveEffect 之间的联系,这种联系全部保存在 targetMap 中。

targetMap: {
  target: depsMap { num: dep=Set(reactiveEffect1, reactiveEffect2) }
}

⚠️ 看着很眼熟不是?Vue 3.0 响应式的核心就是发布/订阅模式,它的订阅Proxy get(track) 实现,发布Proxy set(trigger) 实现。

trigger

function trigger(target, type, key, extraInfo) {
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    // never been trackedreturn
  }
  const effects = new Set()
  const computedRunners = new Set()

  // schedule runs for SET | ADD | DELETE
  if (key !== void 0) {
    // addRunners 主要判断 effect.computed 来考虑 effect 属于 effects/computedRunners
    addRunners(effects, computedRunners, depsMap.get(key))
  }
  // ADD | DELETE 操作,也会触发 track 中的迭代(ownKeys)操作。
  if (type === 'add' /* ADD */ || type === 'delete' /* DELETE */) {
    const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
    addRunners(effects, computedRunners, depsMap.get(iterationKey))
  }

  const run = effect => {
    scheduleRun(effect, target, type, key, extraInfo)
  }
  // set 值时要确保 computed 相关的 ReactiveEffect 先执行
  computedRunners.forEach(run)
  effects.forEach(run)
}

function scheduleRun(effect, target, type, key, extraInfo) {
  if ( effect.options.onTrigger) {
    const event = {
      effect,
      target,
      key,
      type
    };
    effect.options.onTrigger(extraInfo ? extend(event, extraInfo) : event);
  }
  if (effect.options.scheduler !== void 0) {
    effect.options.scheduler(effect);
  }
  else {
    effect();
  }
}

// 回顾之前的代码
state.num++  // 触发依赖更新

此处触发依赖更新的 key 是 num,这时我们就会从 targetMap 中找到 num 对应的 reactiveEffect 实例(上面依赖收集建立了联系),即 reactiveEffect1,reactiveEffect2,随后会遍历调用。

这里会有一种情况是某个 reactiveEffect 内可能会依赖着计算属性,对于计算属性的更新会始终会先于普通的 reactiveEffect,确保取值正确性。

computed

// 本质就是传入lazy/computed: true 的 effect 函数
function computed(getterOrOptions) {
  let getter
  let setter
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = () => {
      console.warn('Write operation failed: computed value is readonly')
    }
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  let dirty = true
  let value
  const runner = effect(getter, {
    lazy: true,
    // mark effect as computed so that it gets priority during trigger
    computed: true,
    scheduler: () => {
      dirty = true
    }
  })
  return {
    _isRef: true,
    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      // When computed effects are accessed in a parent effect, the parent
      // should track all the dependencies the computed property has tracked.
      // This should also apply for chained computed properties.
      trackChildRun(runner)
      return value
    },
    set value(newValue) {
      setter(newValue)
    }
  }
}

// 回顾之前的代码const cValue = computed(() => state.num + 1)
console.log(value1, value2, cValue.value)

computed 函数返回一个标准对象,封装了 effect 函数调用。

可以看到内部 runner 函数就是 effect 函数调用,即 computedReactiveEffect,这个东东也被依赖收集进入了 targetMap 对应 num 中。(因为访问 state.num 触发了依赖收集)

看看这个 effect 传入的额外属性:

dirty: true,决定 computedReactiveEffect 调用获取计算值,可以看到第一次访问 value 时会触发调用,之后改为 false,依赖属性更新也会重新设置为 true;

dirty: false,决定此后访问都会直接返回之前的 value 值,达到缓存效果。