走进Vue3源码:响应式原理(超详细)

1,488 阅读2分钟

概述

响应式原理主要由 3 个部分构成:1. 初始化;2. 收集依赖;3. 触发依赖
Vue3.0 观测数据提供4个方法:reactive, shallowReactive, readonly, shallowReadonly
下面从 reactive 入手分析响应式原理(Vue 3.0.5 版本)

Demo

<div id="demo">
  <!-- 2. 收集依赖 -->
  <h1 @click="handleAddCount"> Vue.js Count: {{state.count}}</h1>
</div>

<script>
import { reactive } from 'vue'
export default{
  setup() {
    // 1. 初始化
    const state = reactive({ count: 1 })
    
    const handleAddCount = () => {
      // 3. 触发依赖
      state.count ++
    }
    
    return {
      state,
      handleAddCount
    }
  }
}
</script>

1. 初始化阶段

调用代码
const state = reactive({count: 1}) // 传入 Object,返回 Proxy
源码详解: reactive.ts
export function reactive(target: object) {
  // 如果观测一个 readonly proxy 直接返回
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}


function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 1. 非对象不允许观测
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // 2. target 已经是一个 Proxy,直接返回
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // 3.target 已经是一个观测过的对象,直接返回
  // 但是一个对象可以经 reactive, readonly 都观测一次
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 4. 仅白名单类型的对象可以观测
  const targetType = getTargetType(target)
  // targetType === TargetType.INVALID 有如下情况
  // a. 经 markRaw 处理的对象(__v_skip属性为true)
  // b. 禁止扩展的对象(Object.preventExtensions, Object.seal, Object.freeze 处理过)
  // c. 非 Object, Array, Map, Set, WeakMap, WeakSet 的对象(比如 Date 对象)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // 5. 进行观测,普通对象(Object, Array)和 COLLECTION 对象(Map, Set, WeakMap, WeakSet)会有不同的处理,下面着重分析普通对象
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 6. 保存 target 到 Map
  proxyMap.set(target, proxy)
  return proxy
}

2. 收集依赖阶段

调用代码
// 渲染节点时等同于执行下面的代码,renderTemplate 方法模拟渲染 dom 的 render 方法
// 会对 state.count 进行取值,触发了 state 的 get 拦截
watchEffect(() => {
  renderTemplate(
    `<h1 @click="handleAddCount"> Vue.js Count: {{state.count}}</h1>`,
    { state, handleAddCount }
  )
})
源码详解: baseHandlers.ts & effect.ts
// baseHandlers.ts
const get = /*#__PURE__*/ createGetter()

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 1. 处理标识属性的值
    // 1.1 ReactiveFlags.IS_REACTIVE => __v_isReactive 是否是响应式
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    // 1.2 ReactiveFlags.IS_READONLY => __v_isReadonly 是否是只读
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    // 1.3 ReactiveFlags.RAW => __v_raw 原始对象
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)
    // 2. 劫持数组方法(proxy 不能拦截的操作,vue 将进行如下 hack 处理)
    // 2.1 'includes', 'indexOf', 'lastIndexOf' 执行这 3 个方法时遍历收集数组所有元素的依赖
    // 2.2 'push', 'pop', 'shift', 'unshift', 'splice' 获取这些方法时会暂时他停止收集依赖
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    // 3. 返回 key 为 Symbol 类型的数据,不会收集依赖
    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      return res
    }
    // 4. 收集依赖(只读属性不会触发依赖收集,因为永远不会触发 set,也就不用收集依赖)
    if (!isReadonly) {
      // track 方法详解见后面
      track(target, TrackOpTypes.GET, key)
    }
    // 5. 浅观测只收集第一层属性的依赖
    if (shallow) {
      return res
    }
    // 6. 对于属性值为 ref 时,对象会自动解包,数组则不会
    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }
    // 7. 对于深层的对象懒观测,即只有在 get 该值时才会进行观测,有利于提升性能
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

// effect.ts
// targetMap 作为全局依赖,所有被观测的对象的依赖全部保存在这
// 观测粒度为对象的 key,数据结构如下
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

// 2. 依赖收集方法
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 1. 判断是否允许收集
  // shouldTrack:用于标识是否可执行 track,由内部方法 pauseTracking和 enableTracking 切换状态
  // activeEffect:表示当前执行的 effect,即要收集的依赖(本案例中当 watchEffect 传入的函数对应的 effect 运行时就会被标记为 activeEffect)
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // 2. 获取 target 的所有依赖,如果不存在则进行初始化
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 3. 获取 target 对应 key 的所有依赖,如果不存在则进行初始化
  let dep = depsMap.get(key)
  if (dep === void 0) {
    depsMap.set(key, (dep = new Set()))
  }
  // 4. 如果当前依赖不存在则保存
  if (!dep.has(activeEffect)) {
    // 加入依赖
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
  // 依赖收集完毕
}

3. 触发依赖阶段

调用代码
const handleAddCount = () => {
  state.count ++ // 此时触发
}
源码详解: baseHandlers.ts & effect.ts
// baseHandlers.ts
const set = /*#__PURE__*/ createSetter()

// 1. 设置 state.count 触发的 set 拦截
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // 1. 获取对象 key 对应的旧值
    const oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      // 2. 自动解包 ref 对象,设置 value
      // 此时 oldValue.value 又会触发 ref 对象的 set,从而触发其依赖触发
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }
    
    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      // 3. 判断当前 key 是否已存在,分别触发 add 和 set 类型的 trigger
      if (!hadKey) {
        // 新增属性也会触发依赖更新,不像 vue2.0 中必须使用 $set
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 当值有改变时才会触发依赖
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

// effect.ts
// 触发依赖的执行

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // 1. 获取 target 对应的依赖
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }
  // 2. 获取要执行的 effect 队列
  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

  // 3. collection 执行 clear 时,将所有 key 的依赖都添加到队列
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
  // 4. 当修改数组的 length 时,将数组 length 和 大于数组 length 下标的依赖都添加到队列 
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  // 5. 处理常见的 修改,新增,删除 操作
  } else {
    // 5.1 找到 key 对应的依赖添加到队列
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    // 5.2 找到 ITERATE_KEY 的依赖添加到队列(在 ownKeys 拦截中收集)
    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          add(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  // 6. 执行队列中所有依赖
  effects.forEach(run)
}

关于对 Map, Set, WeakMap, WeakSet 的观测

源码位置: collectionHandlers.ts
概述

均只拦截了 get 方法,然后通过代理对象 重写 colection 的 get, set, has, add, set, delete, clear, forEach 来实现依赖收集和触发,原理和对象的拦截类型,有兴趣的同学可以自行了解

总结

响应式原理的核心是收集依赖和触发依赖,通过使用 Proxy 对要观测的对象进行各种拦截设置(get, set, deleteProperty, has, ownKeys),在获取数据时收集依赖,在修改数据时触发依赖。