Vue3源码解读之reactivity

696 阅读24分钟

前言

文件总览

在正式进入源码之前,我们先总览一下src目录下有那些文件

├── baseHandlers.ts
├── collectionHandlers.ts
├── computed.ts
├── effect.ts
├── index.ts // 主入口,暴露所有方法
├── operation.ts // 包含TrackOpTypes和TriggerOpTypes
├── reactive.ts
└── ref.ts

根据这些文件,我们可以把响应式模块分为这几部分:effectreactiverefcomputedhandlers

我们知道Vue3跟Vue2在响应式模块的实现区别就在于:从defineproperty变成了proxy,而handlers就是对应创建proxy对象时传入的handler

注意点

我们知道Vue响应式的核心原理就是:依赖收集和触发更新。通过学习effectreactivehandlers这几部分,你就能明白这大概是怎么回事了。

而在这几个模块中有几个点需要你留心,比如:

  • effect中的tracktrigger,这两个函数是依赖收集和触发更新的核心函数,是重中之重。在一开始看到这两个函数时,你可能会不理解其作用,但当你继续阅读其他模块,你就会恍然大悟。
  • handlers中的getset,在这两个函数的内部做了许多事情,也是依赖收集和触发更新的主要入口

effect

effect

我们平常在使用effect时,都会传入一个回调函数,在函数内部中对响应式数据进行监听,每次当响应式数据更新了,这个回调函数就会执行。如:

const num = ref(1) // 响应式数据
effect(() => console.log(num.value)) // effect

// 当响应式数据更新,则会触发回调函数
num.value = 2 // console.log(2)

其内部是怎么实现的呢,现在让我们来看看effect的源码

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // 如果fn是effect则取出原始值
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options) // 调用createReactiveEffect创建effect

  // 如果不为lazy,则立即执行effect
  if (!options.lazy) {
    effect()
  }
  return effect
}

可以看到该函数接收的参数主要为两个,一个是回调函数(fn),一个是options

// options

export interface ReactiveEffectOptions {
  lazy?: boolean // 是否延迟触发effect,正常是当数据还没更新之前都会触发一次
  scheduler?: (job: ReactiveEffect) => void // 调度函数
  onTrack?: (event: DebuggerEvent) => void // 监听track
  onTrigger?: (event: DebuggerEvent) => void // 监听trigger
  onStop?: () => void // 停止监听时触发
  allowRecurse?: boolean
}

effect内部主要做了这些事:

  • 判断fn,如果以及是effect了,则取出其原始值
  • 调用createReactiveEffect创建effect
  • 如果options中没有设置lazy,则立即执行effect函数,最后返回effect函数

我们可以看到其核心在于调用createReactiveEffect创建effect

现在就让来看看这个函数

function createReactiveEffect<T = any>(
  fn: () => T, // 之前传入的回调函数
  options: ReactiveEffectOptions // 之前的options
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    // 如果 effect 不是激活状态,这种情况发生在我们调用了 effect 中的 stop 方法之后,
    // 那么先前没有传入调用 scheduler 函数的话,直接调用原始方法fn,否则直接返回。
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect) // 清除依赖:当前effect的deps。避免依赖重复收集
      try {
        enableTracking() // 即可以追踪,用于后续触发 track 的判断
        effectStack.push(effect) // 推入栈中,表示处于收集依赖的状态
        activeEffect = effect // 为了收集依赖
        return fn() // 执行回调函数,进行依赖收集
      } finally {
        effectStack.pop() // 弹出栈,退出收集依赖的状态
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1] // 恢复原状
      }
    }
  } as ReactiveEffect

  // 挂载属性
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse // 允许递归
  effect._isEffect = true
  effect.active = true // 是否激活
  effect.raw = fn
  effect.deps = [] // 持有当前 effect 的依赖(dep)数组
  effect.options = options
  return effect
}

createReactiveEffect的参数还是两个,与effect相同。

其内部主要做了这些事:

  • 定义一个effect函数
  • 在这个effect函数上挂载属性
  • 返回这个effect函数,即const effect = createReactiveEffect(fn, options)的左边

让我们先来看看其挂载的属性,再看看这个effect函数内部做了些什么

effect.id = uid++ // uid,标识effect的编号
effect.allowRecurse = !!options.allowRecurse
effect._isEffect = true // 标识是否是effect
effect.active = true // 是否为激活状态
effect.raw = fn // 保存原函数
effect.deps = [] // 持有当前 effect 的依赖(dep)数组
effect.options = options // 保存传入第二个参数options

需要重点留心的是effect.deps,这与接下来要讲的以及track函数中的内容有密切的关系

让我们再回头看看新定义的effect函数

const effect = function reactiveEffect(): unknown {
    // 如果 effect 不是激活状态,这种情况发生在我们调用了 effect 中的 stop 方法之后,
    // 那么先前没有传入调用 scheduler 函数的话,直接调用原始方法fn,否则直接返回。
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect) // 清除依赖:当前effect的deps。避免依赖重复收集
      try {
        enableTracking() // 即可以追踪,用于后续触发 track 的判断
        effectStack.push(effect) // 推入栈中,表示处于收集依赖的状态
        activeEffect = effect // 为了收集依赖
        return fn() // 执行回调函数
      } finally {
        effectStack.pop() // 弹出栈,退出收集依赖的状态
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1] // activeEffect恢复原状
      }
    }
  } as ReactiveEffect

这个函数内部主要做了这些事:

  • 如果当前的effect不是激活状态,即已经被主动stop了,如果之前的options中没有 scheduler 函数,那就直接调用fn(回调函数),否则直接返回。
  • 如果effectStack没有包含当前的effect,就调用cleanup清除之前的依赖,即effect.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
  }
}
  • 然后开始进入准备依赖收集的状态,调用enableTracking、将当前的effect推入effectStack中,并将当前effect设置为activeEffect(这一步是重点,与后面track函数被调用时息息相关),然后调用fn(依赖收集的实际执行,后面到proxy-handler会再提到)
  • 最后将当前的effect出栈,调用resetTrackingactiveEffect恢复原状。

我们看到在上面的步骤中调用了enableTrackingresetTracking,其作用又是什么呢?

let shouldTrack = true
const trackStack: boolean[] = []

export function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}

export function enableTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = true
}

export function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}

我们可以看到这三个函数中都是在对shouldTracktrackStack进行操作,需要注意的是shouldTrack的值也是后面进行依赖收集时的重要因素,很快就会讲到。

总结:

  • effect函数主要工作就是创建一个effect函数,这个函数上挂载了许多属性,比较重要的例如depsactiveoptions。在创建effect函数的过程中,也对一些情况进行了处理,比如设置了lazy来避免一开始就调用;还有将effect函数再次作为回调函数传入。
  • 在其内部定义的effect函数主要都是为了之后进行依赖收集的过程。有一些需要留意的,这些都与依赖收集息息相关:effect.depsactiveEffectshouldTrack

track

在对tracktrigger两个函数进行阅读时,你可能疑惑,这两个函数并没有在effect内部出现过,为什么说是核心呢?确实,这两个函数并没有在effect中出现,其主要调用者是在reactive创建的proxy对象中的handlerref内部等等。这是其他模块与effect进行沟通的渠道。

现在的你在看这些函数的时候可能会一直半解,但当你阅读到proxyhandler,即函数的实际调用时,你就会明白了。

现在废话少说,让我们进入track

首先我们还得知道:在effect模块中,维护了一个targetMap,用于保存各个对象的依赖

const targetMap = new WeakMap<any, KeyToDepMap>()
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 不进行依赖收集
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // 开始依赖收集
  // 获取触发对象的depsmap
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 获取depsmap中对应key的依赖集合
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect) // 将activeEffect加入依赖set中
    activeEffect.deps.push(dep) // 将set加入activeEffect的dep中,即deps为effect的依赖数组
                      
    // 如果为开发环境以及activeEffect有onTrack函数,则执行onTrack
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

先来看看参数(target: object, type: TrackOpTypes, key: unknown),依次为触发对象、track操作(分为get/has/iterate)、触发对象的key(属性)

接下来看看其内部做了些什么:

  • 检查shouldTrackactiveEffect,这两个属性我们已经在effect见到过了。当shouldTrackfalse或没有activeEffect,则不进行依赖收集
  • 开始获取targetdepsMap,如果没有则进行创建
  • depsMap中获取对应key(属性)的依赖集合(dep),如果没有则进行创建
  • 在拥有当前对象的属性的依赖集合后,如果集合中不包含activeEffect,则将activeEffect加入到依赖集合中(dep),再将依赖集合加入activeEffect.deps
  • 最后如果为开发环境且activeEffect.options中设置了onTrack,就对其调用

总结:该函数的主要功能就是将effect加入到目标对象对应属性的依赖集合(dep)之中,以实现依赖收集。

trigger

track函数中,我们知道:依赖都会被收集到targetMap中。当监听的属性更新时,又要怎么对依赖进行通知呢,trigger函数就是实现这一过程的函数。

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) {
    return
  }

  const effects = new Set<ReactiveEffect>() // effects Set
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

  // 根据类型来进行不同的操作-->将符合条件的dep添加到effects中
  // CLEAR
  if (type === TriggerOpTypes.CLEAR) {
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) { 
   	// 如果target是数组,表示数组长度发生变化(变短)
    //添加'length'的和key>=newValue的依赖
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    switch (type) {
      // ADD
      case TriggerOpTypes.ADD:
        // 如果不是数组
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          // 如果是map                                                                       
          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
      // DELETE
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      // SET
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  const run = (effect: ReactiveEffect) => {
    // 如果effect.options中对trigger进行了监听
    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() // 执行effect
    }
  }

  effects.forEach(run) // 把收集到的effects全部执行一遍
}

我们再来看一下函数接收的参数:target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown>

其他没啥好说的,让我们看一下type:TriggerOpTypestrigger操作分为clear/set/delete/addtrigger的操作方法和track不同,它有着至关重要的作用,下面就会介绍到。

让我们来对这个函数的功能做一个大致的介绍,然后再仔细分析:

  • 维护一个effects集合,用于存放要进行派发更新的依赖
  • 根据trigger操作的不同,往effects集合中加入符合条件的effect
  • 遍历effects集合,执行集合内的每一个effect

现在开始仔细分析:

  • 我们知道所有的依赖都被存放再targetMap中,因此一开始,我们肯定要获取target的依赖,如果找不到,就表示没有依赖,也就不用进行派发更新了
const depsMap = targetMap.get(target)
if (!depsMap) {
   return
}
  • 接下来开始维护一个effects集合,还有提供了一个add函数来往集合内添加依赖
const effects = new Set<ReactiveEffect>() // effects Set

const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
   if (effectsToAdd) {
     effectsToAdd.forEach(effect => {
			 // 如果当前effect不是活跃(不是正在收集依赖)或设置了allowRecurse就添加至effects
       if (effect !== activeEffect || effect.allowRecurse) {
         effects.add(effect)
       }
     })
   }
 }
  • 根据不同的操作类型来往effects中添加依赖:
    • typecleartargetdepsMap全部添加
    • target为数组,keylength,表示数组长度发生变化,将lengthkey大于新长度的项添加
    • typeadd,如果target不是数组,将对targetITERATE_KEY的依赖集合加入,并且如果targetmap,就将对targetMAP_KEY_ITERATE_KEY的依赖集合加入。如果target是数组,如果数组长度变长了,就将对targetlength依赖的集合加入effects
    • typedelete,如果target不是数组,将对targetITERATE_KEY的依赖集合加入,并且如果targetmap,就将对targetMAP_KEY_ITERATE_KEY的依赖集合加入。
    • typeset,如果targetmap,将对targetITERATE_KEY的依赖集合加入
  • 在上一步中,已经把需要派发更新的依赖添加完了,现在开始正式派发更新: effects.forEach(run)run函数的逻辑比较简单,就是执行effect

不知道你是否还记得effect的内容是什么,让我们再回顾一下:

const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect) // 清除依赖:当前effect的deps。避免依赖重复收集
      try {
        enableTracking() // 即可以追踪,用于后续触发 track 的判断
        effectStack.push(effect) // 推入栈中,表示处于收集依赖的状态
        activeEffect = effect // 为了收集依赖
        return fn() // 执行回调函数
      } finally {
        effectStack.pop() // 弹出栈,退出收集依赖的状态
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1] // 恢复原状
      }
    }
  } as ReactiveEffect

到这里你可能明白了,在派发更新的时候会重新进行依赖收集fn函数也会再执行一次

reactive

reactive

我们正常使用reactive时是这样的const obj = reactive({name:'obj'}),都是传入一个对象,然后返回一个响应式对象。

现在让我们开始进入源码

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  // 如果对象只读,则直接返回
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers, // 对普通的引用数据类型的劫持(object/array)
    mutableCollectionHandlers // 对集合类型的劫持(set/map/WeakMap/WeakSet)
  )
}

老惯例,看参数:target:object。即传入一个对象。

现在看一下函数内部做了什么:

  • 首先对传入的对象进行了判断,如果对象是只读的,表示不能被代理就直接返回
  • 然后返回调用createReactiveObject函数,传入了四个参数。

可见核心实现都在这个函数中了,让我们看看这个函数到底是干什么的。

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 如果不是对象则直接返回
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  
  // 对象已经是proxy
  // proxy为响应式的(proxy有两种类型,一种为只读,一种为响应式)
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  // 如果对象上已经挂载了proxy(被proxy代理),则返回该proxy
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 对象类型在白名单(object/array/map/set/weakmap/weakset)内才能被劫持
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // 创建proxy
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

看看刚刚传进入的4个参数到底对应什么:

target  ->  target: Target // 原对象
false   ->  isReadonly: boolean // 是否为只读
mutableHandlers  ->  baseHandlers: ProxyHandler<any> // handlers
mutableCollectionHandlers  ->  collectionHandlers: ProxyHandler<any> // handlers

前两个好理解,后面两个handlers是干嘛的?我们知道new proxy(target,handle)语法是这样的。传入的两个handlers即是为了后面创建proxy所要传入的。关于handler的具体内容将在后面提到,这也是关键的一环。

让我们正式进入函数内部:

  • 首先先对target进行判断,如果target不是对象就直接返回

  • 然后如果传入的target已经是proxy,并且不是只读或不是响应式的对象,就直接返回

  • 然后根据函数接受的第二个参数isReadonly: boolean,去对应的Map查看当前对象是否已经缓存过了(是否缓存的意思是:这个target是否已经创建过proxy对象了),如果已经缓存过,就直接返回之前创建的proxy

    export const reactiveMap = new WeakMap<Target, any>() // 正常类型的缓存Map
    export const readonlyMap = new WeakMap<Target, any>() // 只读类型的缓存Map
    
  • 走到这一步,就表示target没有创建过proxy。接下来开始获取target的类型,因为要**根据类型来传入不同的handler**来创建proxy对象,如果类型是不可用则直接返回target。获取类型的函数如下:

    const enum TargetType {
      INVALID = 0, // 不可用
      COMMON = 1, // 普通类型,Object/Array
      COLLECTION = 2 // 集合类型,Map/Set/WeakMap/WeakSet
    }
    
    // 白名单内的对象类型
    function targetTypeMap(rawType: string) {
      switch (rawType) {
        case 'Object':
        case 'Array':
          return TargetType.COMMON
        case 'Map':
        case 'Set':
        case 'WeakMap':
        case 'WeakSet':
          return TargetType.COLLECTION
        default:
          return TargetType.INVALID
      }
    }
    
    // 获取目标对象的类型
    function getTargetType(value: Target) {
      return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
        ? TargetType.INVALID
        : targetTypeMap(toRawType(value))
    }
    
  • 最后一步,就是根据target的类型来传入不同的handlers来创建proxy对象。然后进行缓存,返回proxy对象。

到这里我们直接了解到reactive是怎么创建一个响应式对象的,总结来说就是针对target类型来创建不同的proxy,还有对一些特殊情况进行处理,比如传入的不是对象、传入的对象已经是proxy

在这部分内容中没有提到依赖收集和派发更新相关的东西,因为核心奥秘是在于handlers中。在下一小节中,我们就会揭开handlers的面纱。

proxy-handler

相信你还记得,reacitve在创建proxy对象时,会根据target的不同类型传入相应的handlers。而target主要分为common(包含Object/Array)和collection(包含Map/Set/WeakMap/WeakSet)。因此对应的handlers也分成两大类,分别为baseHandlerscollectionHandlers

baseHandlers

baseHanlers这个大类中一共有4个handler,分别为mutableHandlersreadonlyHandlersshallowReactiveHandlersshallowReadonlyHandlers

我们主要学习的是:mutableHandlers,因为当我们在调用reactive时,其第三个参数传入的就是mutableHanlers。可以返回上一节查看具体代码。

其他三个则是针对reactive中提供的其他API,比如shallowReactivereadonlyshallowReadonly相关的handler

现在让我们来看看mutableHandlers

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

可以看到其内容其实是5个相关的操作函数,对比与defineProperty中只对getset进行重写,proxy拓展了许多功能。

其实其他三个handlers包含的内容也大致相同。区别在于:比如get其实是调用了一个工厂函数来创建的,而不同的handlers中调用工厂函数时,传入的参数不同:

const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)

现在让我们具体看看这5个操作。

get

在上面我们看到,get的创建其实是调用了createGetter这个函数。那就让我们来看看这个函数:

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target) // 判断target是否为数组

    // 如果target是数组且非只读,且key为改写过的数组方法,则调用arrayInstrumentations
    // arrayInstrumentations是对数组方法的重写,对其获取res
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver) 
    }

    const res = Reflect.get(target, key, receiver) // 获取值

    // 如果是symbol类型的属性
    // 然后为symbol本身的属性(在该set之中)或key为__proto__/_v_isRef
    // 直接返回结果(不进行依赖收集)
    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : isNonTrackableKeys(key) // key为__proto__/_v_isRef
    ) {
      return res
    }

    // 非只读情况下,进行依赖收集
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    // 如果是浅响应式,直接返回res
    if (shallow) {
      return res
    }

    // 如果是ref类型的对象,则返回ref.value
    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    // 如果为对象,则递归处理,返回响应式对象
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

首先看一下参数,(isReadonly = false, shallow = false),这两个参数即标记只读浅响应式。在上面提到的,主要是不同handlers在创建get的时候传入的这两个参数的不同。然后再看一下返回值,当调用这个工厂函数时就会返回一个get函数。

现在让我们重点看看get函数内部做了些什么。

  • 首先对特殊的key进行了处理。当要访问的target对象的keyReactiveFlags.IS_REACTIVE时返回!isReadonly;是ReactiveFlags.IS_READONLY时返回isReadonly;或者是ReactiveFlags.RAW,且receivermap(来源于reactive)存储中的target对象的proxy时,则返回target

  • 然后判断target是否为数组。如果是数组,且访问的key是数组方法时,则从arrayInstrumentations中获取对应的方法并调用(Reflect.get(arrayInstrumentations, key, receiver) )。我们来看看arrayInstrumentations是怎么做到数组方法改写的。

    const arrayInstrumentations: Record<string, Function> = {} // 一个对象,存放改写的数组方法
    
    // 通过遍历,将方法重写并添加到arrayInstrumentations中。重写的方法内部会触发依赖收集。
    ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
      const method = Array.prototype[key] as any // 保存原方法
      // 重写方法,并添加到arrayInstrumentations中
      arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
        const arr = toRaw(this) // 获取调用该方法的源数组
        for (let i = 0, l = this.length; i < l; i++) {
          track(arr, TrackOpTypes.GET, i + '') // 对数组的每一项都进行依赖收集
        }
      
        const res = method.apply(arr, args) // 调用原方法
        if (res === -1 || res === false) {
          return method.apply(arr, args.map(toRaw)) // 如果调用失败,使用原始值进行重新调用
        } else {
          return res
        }
      }
    })
    
    ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
      const method = Array.prototype[key] as any // 保存原方法
      
      // 重写方法,并添加到arrayInstrumentations中
      arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
        pauseTracking() // 更改 shouldTrack状态和trackStack,来源于effect
        const res = method.apply(this, args) // 调用原方法
        resetTracking() // 重置 shouldTrack状态和trackStack,来源于effect
        return res
      }
    })
    
  • 继续返回流程。如果当target不是数组,且key不是改写的方法列表中。则进行普通的获取值,const res = Reflect.get(target, key, receiver)

  • 当获取到值之后,先处理一下特殊情况:如果key是一个symbol且为symbol本身的属性或者key__proto__,__v_isRef,__isVue,直接返回值。因为当符合这两种情况时,是不需要进行依赖收集的。

  • 然后开始针对工厂函数传入的参数来进行操作。如果是非只读时,则调用track进行依赖收集。然后如果是浅响应式时,则直接返回值。

  • 最后根据res的类型来进行处理:当res是一个ref时,则返回ref.value;当res是一个对象时,则对其进行使用reactive/readonly包装,并返回(这是实现深度监听的关键);如果不是这两种情况,则正常的返回res

触发时机

让我们再回顾一下effectreactive的正常配合使用。

let obj = reactive({name:'obj'}) // 使用reactive创建一个响应式对象
effect(() => console.log(obj.name)) // 对这个响应式对象添加依赖

我们知道reactive的根本就是创建一个proxy对象。当对这个对象进行不同的操作时,则会触发不同的handle

让我们再回顾一下effect中的核心代码:

const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect) 
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn() // 执行回调函数
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect

简述一下当调用该函数时,会先清理之前的依赖。然后开启依赖收集的准备工作,然后就调用回调函数fn,最后恢复原状态。

而且我们知道:在创建响应式对象的时候是没有触发依赖收集的,需要当对响应式的属性进行访问(执行get)操作时才会触发。而effect内部中也没有看到有调用track的地方。那到底是怎么进行依赖收集的呢?

其实重点就在于effectfn函数:上面的示例中调用effect时传入的回调函数是() => console.log(obj.name),在这个函数内部对响应式对象的属性(obj.name)进行了访问,当effect中执行fn时,就触发了响应式对象的对应keyget操作。而在get的内部中又会调用了track。因此就这样进行了依赖收集。

总结一下:当调用effect传入的回调函数中与响应式对象的属性有关系时,那么当effect函数被执行时,回调函数在其内部也会执行,因此触发了响应式对象的get操作,而在get操作内部会调用track。因此就进行了依赖收集。因此实际effect中的依赖收集是在fn的调用。

set

set也和get一样,都是由一个工厂函数创建的。这个工厂函数为createSetter

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key] // 获取旧值

    // 非浅响应式,即正常情况下
    if (!shallow) {
      value = toRaw(value)
      // 如果target不是数组且旧值为ref类型,新值不为ref类型
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value // 将新值赋给旧值(ref类型)的value,让旧值来处理trigger
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    // 如果target为数组且key为数字类型,则通过下标判断是否有该key,或则通过hasOwn函数
    // hadKey:为了后续的触发更新操作,判断是新增或修改
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : 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
    // 当target不是原型链上的值,此时触发trigger。
    // 因为当target是原型链上的值时,设置值的操作起作用的是receiver而不是target,因此不应该对target触发更新
    if (target === toRaw(receiver)) {
      // 如果target中没有该属性(key),则调用trigger触发add,即新增
      // 或则调用trigger触发set操作,即修改
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value) // add操作
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue) // set操作
      }
    }
    return result
  }
}

老规矩,先看看参数(shallow = false),跟createGetter一样,这个参数主要是为了不同handlers创建get时能有区别。同样,返回值是一个get函数。

现在看看其内部做了哪些事:

  • 首先对旧值进行了保存。
  • 当为非浅响应式时,如果target不为数组,然后旧值为ref类型但新值不为ref类型时。就将新值赋给旧值(oldValue.value = value)。相当于让ref来实现派发更新。
  • 然后判断target上是否拥有key来定义hadKey。这是为了确认操作是为新增还是修改。以便于调用trigger派发更新时,能够传入正确的操作类型。
  • 通过const result = Reflect.set(target, key, value, receiver)设置值。
  • 接下来开始派发更新。当target不是原型链上的值时,根据之前获取hadKey来进行不同的trigger。当为新增时:trigger(target, TriggerOpTypes.ADD, key, value)当为修改时:trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  • 返回result

deleteProperty

function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key) // 是否为对象本身的属性
  const oldValue = (target as any)[key] // 保存旧值
  const result = Reflect.deleteProperty(target, key)
  // 触发更新
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

get/set不同的是,deleteProperty不是通过工厂函数生成的。因此我们直接看看它内部做了些什么:

  • 先判断key是否是存在于target
  • 保存旧值
  • 调用const result = Reflect.deleteProperty(target, key)进行正式的deleteProperty操作,并储存是否删除成功
  • 当删除成功且keytarget上的属性时,进行派发更新trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  • 最后返回删除结果

has

function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  // 若属性不是symbol或symbol本身的属性,进行依赖收集
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}

如上,直接看看函数内部:

  • 直接调用const result = Reflect.has(target, key)并保存查询结果
  • 如果属性不是symbol或者symbol本身的属性时,调用track进行依赖收集
  • 返回结果

ownKeys

function ownKeys(target: object): (string | number | symbol)[] {
  // 依赖收集
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

这个函数很简单,先track进行依赖收集,然后调用Reflect.ownKeys(target)返回结果

collectionHandlers

ref

ref

reactive一样,ref方法也是用来创建一个响应式对象。但区别在于,ref传入的值一般为基本数据类型而不是引用数据类型。且在访问通过ref创建的响应式对象时,都要通过.value

现在我们就来深入源码,看看它的内部奥秘

export function ref(value?: unknown) {
  return createRef(value)
}

reactive一样,其内部很简单,都是调用了另外一个函数来创建对象。让我们来看看createRef

function createRef(rawValue: unknown, shallow = false) {
  // 如果value已经是ref了,直接返回
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

这个函数也比较简单,首先先对传入的值进行了判断,如果传入的值已经是ref类型了,就直接返回,否则就调用new RefImpl()来创建一个新的ref对象

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, public readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue) // 非shallow,调用convert,并传入value
  }

  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value') // 依赖收集
    return this._value
  }

  set value(newVal) {
    // 判断值是否更新了
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal) // 触发更新
    }
  }
}

首先我们来看看它的构造函数:

constructor(private _rawValue: T, public readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue) 
}

构造函数中接受了两个参数:(_rawVlaue, _shallow),即初始值和是否为浅响应式。然后构造函数主要就是将传入的初始值赋值给_value。当为浅响应式时,直接赋值,否则将调用convert(_rawValue)然后再将返回值赋值给_value

const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val // 如果是对象则用reactive代理

convert比较简单,就是当传入的值是对象时返回用reactive包裹的值,否则直接返回val。所以当调用ref传入对象时,其实内部还是调用了reactive

接下来看看另外的两个方法:

get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value') // 依赖收集
    return this._value
  }

set value(newVal) {
    // 判断值是否更新了
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal) // 触发更新
    }
}

可以看到都是对value的操作,现在你知道为什么方法ref类型的值时都需要通过.value的原因了吧。

  • get的内部很简单,就是调用track进行依赖收集,然后返回值
  • set则是当值进行更新时,将新值赋值给原始值,再重复构造函数中的操作this._value = this._shallow ? newVal : convert(newVal)。最后调用trigger进行派发更新。

ref讲完了,可以知道跟reactive的区别除了之前提到的那些之外。还有这些区别:

  • reactive创建的对象是一个proxy,而ref不是。
  • reactive进行依赖收集和派发更新的位置是在handlers中代理的方法,而ref则是在get/set之中。

computed

在正式进入源码之前,我们有必要先看看它的使用方法

const state = reactive({name:'obj',age:18})

const computedAge = computed(() => state.age + 10)
const computedName = computed({
  get() {
     return state.name + '123'
  },
  set(val) {
     state.name = val 
  }
})

在平常使用中,我们一般是传入一个回调函数,但其实computed也接受一个包含getset的对象。

现在我们进入源码一探究竟:

export function computed<T>(
  (
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  // 根据传入的函数/包含get和set的对象生成getter和setter
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions // getter赋值为函数
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  // 创建ComputedRefImpl
  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set // 当传入的是函数时为true
  ) as any
}

首先看参数:(getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>),我们在上面提到了,可以接受一个函数或者一个带着getset的对象。

函数内部主要做了两件事:

  • 根据传入的参数来生成getter/setter。当传入的是函数时,则将函数赋值给getter;当传入的是对象时,则对应的将get/set赋值给getter/setter
  • 调用ComputedRefImpl实例化一个对象并返回。

重点在于computedRefImpl这个类:

class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true // 标记是否缓存

  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true; // 标识为是ref对象
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    // 调用effect方法对传入的getter进行响应式包装
    // 后面的对象是options
    this.effect = effect(getter, {
      lazy: true,
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value') // 触发更新,所有computed的依赖都会进行更新
        }
      }
    })

    this[ReactiveFlags.IS_READONLY] = isReadonly // 设置flag
  }

  get value() {
    // 当依赖发生改变时
    if (this._dirty) {
      this._value = this.effect() // 调用effect函数重写获取值
      this._dirty = false
    }
    track(toRaw(this), TrackOpTypes.GET, 'value') // 对computed依赖进行收集
    return this._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

我们先来看看包含的属性:

private _value!: T // 值
private _dirty = true // 标记是否缓存

public readonly effect: ReactiveEffect<T> // 依赖

public readonly __v_isRef = true; // 标识为是ref对象
public readonly [ReactiveFlags.IS_READONLY]: boolean // reactive Flag

可以看到属性中出现了我们十分熟悉的:effect。其实computed的核心就是在其内部使用了effect来添加依赖,因此也具有和effect相同的效果。还有一个**_dirty属性,这是computed进行缓存更新的关键之一**。

接下来我们继续看看构造函数:

constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
) {
    // 调用effect方法对传入的getter进行响应式包装
    // 后面的对象是options
   this.effect = effect(getter, {
      lazy: true,
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value') // 触发更新
        }
      }
   })

  this[ReactiveFlags.IS_READONLY] = isReadonly // 设置flag
}

参数一共有三个,分别为gettersettterisReadonly。我们在刚刚的computed函数中也看到了实际传入的会有两种情况:

  • 当传入的computed的是一个函数时,则此时传入ComputedRefImpl构造函数的参数分别对应:回调函数、一个提示函数以及true
  • 当传入的是一个带有get/set的对象时,传入构造函数的实参则是分别对应:对象中的get、对象中的setfalse

构造函数做的事很简单:就是创建一个effect,并赋值给this.effect,进行缓存(这也是跟effect的区别之一,computed会进行缓存)。重点在于创建effect时,传入的参数。第一个参数是getter,第二个参数我们知道是一个options对象,这里包含了lazy和一个scheduler调度函数。

function effect<T = any>(): ReactiveEffect<T> {
  const effect = createReactiveEffect(fn, options) // 创建effect
  // 如果不为lazy,则立即执行effect
  if (!options.lazy) {
    effect()
  }
  return effect
}

lazy的效果我们可以从effect的源码中知道。就是创建完effect后不会立即执行。而scheduler则是在trigger的中的run函数会被调用。

export function trigger() {
  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()
    }
  }

  effects.forEach(run)
}

当每次trigger之后,就会调用scheduler函数。而在此处设置的scheduler中内部是主要是对_dirty进行了设置还有触发trigger函数(trigger函数会对computed的所有依赖进行派发更新)

现在我们来看看 get value()/set value()

get value() {
    // 当依赖发生改变时
    if (this._dirty) {
      this._value = this.effect() // 调用effect函数获取值
      this._dirty = false
    }
    track(toRaw(this), TrackOpTypes.GET, 'value') // 依赖收集
    return this._value
}

set value(newValue: T) {
	this._setter(newValue)
}
  • set:当对computed直接设置值时,则会调用setter。如果调。用computed传入的是一个函数时,则会调用设置的警告函数。否则则调用传入对象的set
  • get中,首先会对_dirty属性进行判断。如果_dirtytrue,则表示依赖发生改变,因此需要调用this.effect获取最新值,并修改_dirty。然后调用track进行依赖收集并返回值

可见重点在于this._dirty的变化。当getter依赖的响应式数据更新时,会调用到设置好的scheduler调度函数,_dirty会被设置为true,表示数据发生改变。然后当再次访问computed的值时,会重新调用this.effect来获取新值,并将_dirty的值恢复成false