系列文章目录

  1. 目录分析
  2. 初始化流程
  3. 响应式系统
  4. shared工具函数


前言

无论在vue2或者vue3中,实现一个vue的响应式系统(改变数据,自动渲染组件),大致分为两部分:

  • 定义响应式数据
  • 依赖处理:收集和派发

packages/reactivity文件夹下的src中就是实现响应式逻辑的源码


一、定义响应式数据

  • vue2中,只需要在组件的data中定义数据,这些数据自动就会变成响应式的数据。原理是在组件初始化时,遍历data中的属性,依次利用Object.defineProperty中的getter setter进行拦截操作。
  • 而从vue3的setup API可知,我们需要调用 reactive 方法,手动定义一个响应式数据。
    如以下代码示例:
<template>
    <p>{{obj.count}}</p>
</template>

import {reactive} from 'vue'
const obj = reactive({count: 0})
obj.count++      // obj.count --> 1

reactive 方法背后的实现原理是什么呢???下面通过分析源码看下~~

1、reactive(target)

reactive方法主要作用:接收一个普通对象target然后返回该普通对象的响应式代理
定位到[runtime-core/reactivity/reactive.ts]源码:

export function reactive(target: object) {
  // 如果target是一个只读的响应式数据,则直接返回。因为已经是响应式了
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  // 创建一个响应式对象
  return createReactiveObject(
    target,   // 传入的响应式对象
    false,   // 是否只读
    mutableHandlers, // proxy handle
    mutableCollectionHandlers,  // 集合数据(Map,WeakMap,Set,WeakSet)的 proxy handle
    reactiveMap  // 全局缓存的一个map结构,存储所有的proxy
  )
}

reactive作为定义响应式数据的入口API,将一个target普通对象包装成响应式的对象,里面核心实现方法是:createReactiveObject(如下)

2、createReactiveObject

方法主要作用:创建一个响应式数据对象

/**
 * 创建响应式对象
 */
function createReactiveObject(
  target: Target,
  isReadonly: boolean, 
  baseHandlers: ProxyHandler<any>, 
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any> // 一个缓存weakmap,key是 target,value是响应式对象
) {
  // 判断一:若不是对象,则直接返回,特别注意null也直接返回, reactive(null) => null
  if (!isObject(target)) { 
    return target
  }
  
  // 判断二:若已经是proxy对象,并且。。。
  if (target[ReactiveFlags.RAW] &&!(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
    return target
  }

  // 判断三:从proxyMap数据中拿到key为target的响应式对象,若存在,则直接返回已创建过的响应式对象
  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
  }

 // 通过所有判断,用有效的tagert创建new Proxy代理对象
  const proxy = new Proxy( 
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 存入缓存map 中
  proxyMap.set(target, proxy)
  return proxy
}

2.1 入参

  • target 传入的要被创建响应式的对象,同reactive(见上)的target
  • isReadonly 是否只读 ,通过reactive创建的为false非只读(见上)
  • baseHandlers 普通对象(Object,Array)的proxy handler(get,set,deleteProperty,has, ownKeys)。通过reactive创建的为mutableHandlers(见[baseHandlers.ts]),为之后new Proxy(target, handlers)传入
// proxy handlers
export const mutableHandlers: ProxyHandler<object> = { 
  get, //  拦截对象属性的读取,指向了方法 createGetter, 创建 get 劫持
  set, // 拦截对象属性的设置
  deleteProperty,  // 拦截delete proxy[propKey]操作
  has, // 拦截propKey in proxy操作
  ownKeys // 拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
}
  • collectionHandlers !!!集合数据(Map,WeakMap,Set,WeakSet)的proxy handler 。通过reactive创建的为mutableCollectionHandlers(见[collectionHandlers.ts]),为之后new Proxy(target, handlers)传入

  • proxyMap 存储所有的proxy的WeakMap(Map与WeakMap区别可自行百度)结构数据,key是 target对象, value是响应式对象。reactive方法传入的是reactiveMap

// reactiveMap全局对象,存储所有的proxy对象
export const reactiveMap = new WeakMap<Target, any>()

2.2 响应式创建过程

首先,判断入参target是否为有效参数

  • isObject判断target是否为为对象,若不是对象,直接返回target
  • 若target已经是proxy对象,直接返回target
  • existingProxy = proxyMap.get(target)若从全局proxyMap对象中能拿到该对象的代理对象,则直接返回拿到的代理对象
  • 检查target的类型,若不是object array map set weakmap weakset 中的,则直接返回target

接着,创建proxy对象

  • 通过new Proxy(target, handlers)为target创建代理
  • 并且根据target类型,选择合适的handers:TargetType.COLLECTION ? collectionHandlers : baseHandlers
  • Map,Set,WeakMap,WeakSet 集合(TargetType.COLLECTION)类型,在proxy中使用的是collectionHandlers
  • Object,Array在proxy中使用的是 baseHandlers,下面具体看一下

最后,全局存储target对应的Proxy对象: proxyMap.set(target, proxy),并返回:return proxy

2.3 proxy handler

从上可知,reactive方法创建响应式对象时,传入的baseHandlers具体为mutableHandlers
mutableHandlers是从packages/src/reactivity/baseHandlers.ts文件引入而来,baseHandlers.ts文件定义了所有不同的baseHandlers,对应不同的数据处理

import {
  mutableHandlers,
  readonlyHandlers,
  ...
} from './baseHandlers'

baseHandlers.ts中的 mutableHandlers为例:new Proxy(target, mutableHandlers)

export const mutableHandlers: ProxyHandler<object> = {
  get, //  拦截对象属性的读取,指向了方法 createGetter, 创建 get 劫持
  set, // 拦截对象属性的设置
  deleteProperty,  // 拦截delete proxy[propKey]操作
  has, // 拦截propKey in proxy操作
  ownKeys // 拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
}

通过以上分析可知,vue3将一个普通对象变为响应式对象,核心是通过new Proxy(ES6语法)创建代理,拦截set,get,deleteProperty,has,ownKeys操作监听。

2.4 get 做了什么?

访问对象属性会触发get函数

const get = createGetter()
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // ....
    // 当获取target自身属性时,receiver --》 reactiveMap.get(target)拿到代理对象
    if (key === ReactiveFlags.RAW && receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
            ? shallowReactiveMap
            : reactiveMap
        ).get(target)
    ) {
      return target
    }

    // 数组操作
    const targetIsArray = isArray(target)
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      // 关键代码
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    // 非数组操作:关键代码-利用 Reflect 反射来获取原始值  
    const res = Reflect.get(target, key, receiver)

	// 关键代码2:调用track,收集依赖。若只读,不会变更,无需追踪
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key) 
    }
    //返回target属性值
    return res
  }
}

以上可知,get函数大致做了2件事:

  • track依赖收集(类似于vue2的getter中的dep.depend())。把组件渲染期间 依赖的property记录下来,之后触发依赖项setter时,会通知更新,使得组件重新渲染
  • get最终就是通过 Reflect.get 要拿到代理对象属性值res

思考为什么用 Reflect.get,而不是直接 target[key] 返回呢?

Reflect.get(target, key, receiver)
  • target (取值的目标对象)、 key (获取的值的键值)、receiver(如果target对象中指定了getter,receiver则为getter调用时的this值)。
  • 返回值:属性的值
var target = {
  foo: 1,
  get baz() {
    return this.foo;
  },
};
const observed = reactive(target)

此时,如果用target[key]取值,那么 this.foo 中的 this 就指向的是 target,而不是 observed,此时 this.foo 就不能收集到 foo 的依赖了,如果 observed.foo = 20 改变了 foo 的值,那么是无法触发依赖回调的,所以需要利用 Reflect.get 中的第三个参数receiver 将 getter 里的 this 指向代理对象observed。

2.5 set 做了什么?

设置对象属性会触发set函数

const set =  createSetter()
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
  
    let oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      // 非数组类型,若旧值是ref类型,新值不是ref类型,那么直接复制给oldValue.value。ref数据在set value时就已经trigger依赖了,所以直接return
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    }

    // 关键代码,设置值
    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)) {
    // 对象上没有这个key,trigger add
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
     // 对象上有这个key且value发生了变化,trigger set
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    
    return result
  }
}

以上可知,set函数大致做了2件事:

  • set就是通过 Reflect.set 要设置代理对象属性值
  • trigger 派发更新,使得组件重新渲染。

二、依赖收集与派发更新

1、依赖收集——track

track方法在 reactivity模块中effect.ts模块有定义,如下:

export function track(target: object, type: TrackOpTypes, key: unknown) {
  // activeEffect不存在,直接return
  if (!shouldTrack || activeEffect === undefined) {
    return
  }

  // targetMap 依赖管理中心,用于收集依赖和触发依赖
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // target 在 targetMap 对应的值是 depsMap    targetMap(key:target, value:depsMap(key:key, value:dep(activeEffect)))
    // set结构防止重复
    targetMap.set(target, (depsMap = new Map()))
  }

  //此时经过上面的判断,depsMap 必定有值了。然后尝试在 depsMap 中获取 key
  let dep = depsMap.get(key)
  if (!dep) {  // 判断有无当前key对应的dep,没有则创建
    //如果没有获取到dep,说明 target.key 并没有被追踪,此时就在 depsMap 中塞一个值
    depsMap.set(key, (dep = new Set()))  // 执行了这句后,targetMap.get(target) 的值也会相应的改变
  }
  
  // 这个 activeEffect 就是在 effect执行的时候的那个 activeEffect
  if (!dep.has(activeEffect)) {  
    dep.add(activeEffect)  // 将effect放到dep里面
    activeEffect.deps.push(dep)  // 双向存储
  }
}
  • 以上涉及到了2个关键对象:activeEffecttargetMap。最终将activeEffect收集到targetMap全局Map结构中,同时将dep放到activeEffect下的deps数组。
  • 在vue2中,做依赖收集时,收集的是watcher,而vue3已经没有了watcher概念,取而代之的是effect(副作用函数)。
  • targetMap 是一个全局WeakMap对象,作为一个依赖收集容器,用于存储target[key]相应的dep依赖。targetMap.get(target) 获取target对应的depsMap,depsMap内部又是一个Map,key为target中的属性,depsMap.get(key)则为Set结构存储的target[key]对应的dep,dep中则是存储了所有依赖的effects。
  • 方便在修改数据触发set时,找到对应的effect依赖并执行。
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

2、activeEffect

activeEffect同样在 reactivity模块中effect.ts模块有定义,表示正在运行中的effect:

let activeEffect: ReactiveEffect | undefined  // 当前正在运行的effect

3、effect

effect 作为 reactive 的核心,主要负责收集依赖,更新依赖。

3.1 effect如何被定义

看下源码方法定义:

export function effect<T = any>(
  fn: () => T,  // 接收一个回调函数
  options: ReactiveEffectOptions = EMPTY_OBJ  // 配置{lazy:false,...}
): ReactiveEffect<T> {
  if (isEffect(fn)) {  // 若fn已经是一个effect对象
    fn = fn.raw  // effect对象中:effect.raw = fn
  }
  // 创建`effect`,执行createReactiveEffect后得到的是一个 function 即 reactiveEffect
  const effect = createReactiveEffect(fn, options)

  // 非lazy时,立即执行effect()。computed effect为lazy模块
  if (!options.lazy) {
    effect()
  }
  return effect
}
  • isEffect(fn)判定fn是否已经是一个effect,若是fn.raw就是原始的回调函数,fn = fn.raw (effect.raw = fn)
  • createReactiveEffect(fn, options)返回一个effect函数
  • options.lazy 若为false,非惰性更新,则立即执行一次。computed effect为true

下面看看effect如何被创建的:

function createReactiveEffect<T = any>(
  fn: () => T,  // 回调函数
  options: ReactiveEffectOptions  // 传入的配置
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    // 没有激活,说明调用了effect stop函数
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect)  // 清除effect依赖
      try {  
        enableTracking() // 重新进行依赖收集
        effectStack.push(effect)  // 将当前effect压入effectStack栈
        activeEffect = effect  // 当前正在运行的effect
        return fn()  // 执行回调
      } finally { // 最后将effect出栈,恢复之前的状态
        effectStack.pop() 
        resetTracking()  // 重置依赖
        activeEffect = effectStack[effectStack.length - 1]   // 重置activeEffect
      }
    }
  } as ReactiveEffect
  effect.id = uid++  // 自增ID,唯一标识
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true  // 用于标识方法是否经历过effect,是否是effect
  effect.active = true  // effect是否激活,默认false。调用stop函数之后,修改为false
  effect.raw = fn   // 传入的fn
  effect.deps = []  // 持有当前effect的dep数组,在track时收集dep,dep就是追踪列表中对应的key,即 targetMap.get(target).get(key)
  effect.options = options
  return effect
}
  • 生成一个挂载了一系列属性:id,allowRecurse,_isEffect,active,raw,deps, options的reactiveEffect方法。
  • effectStack是一个作为全局变量的effect栈,类似于vue2中存储watcher的栈:const effectStack: ReactiveEffect[] = []
  • 在执行effect()时,若effectStack没有当前effect,首先清空effect.deps数组中的依赖,每次effect 运行时都会重新收集依赖。(在上面track那里,曾给deps数组塞值:activeEffect.deps.push(dep))
  • effectStack.push(effect) ,将当前effect压入effectStack栈
  • activeEffect = effect , 给activeEffect赋值,表示当前正在运行的effect,activeEffect 主要为了在收集依赖的时候使用
  • 执行回调fn(),最后将effect出栈,恢复之前的状态,重置activeEffect

3.2 effect都在什么时候创建呢?

1、mountComponent
2、computed
3、watcher、watchEffect

3、依赖更新派发——trigger

依赖收集完毕,接下来target的属性值修改会触发trigger,拿到相应的依赖并执行effect

export function trigger(
  target: object,
  type: TriggerOpTypes, // set | add | delete | clear
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)  //targetMap上面讲到,是全局的依赖收集器
  if (!depsMap) {  /* targetMap中没有该值,说明没有收集该effect,无需追踪*/
    return
  }

  const effects = new Set<ReactiveEffect>()
  
  /* 将合规的effect添加进effects set集合中*/
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

  if (type === TriggerOpTypes.CLEAR) { // 若是clear
    depsMap.forEach(add)  // 触发对象所有的effect
  } else if (key === 'length' && isArray(target)) {  // 若数组的length发生变化
    depsMap.forEach((dep, key) => { 
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else { // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    // 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) => {
    ...
    // 如果 scheduler 存在则调用 scheduler,计算属性拥有 scheduler
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  // 关键代码,所有的effects会执行内部run方法
  effects.forEach(run)
}
  • 首先校验一下target有没有被收集依赖,若没有收集依赖,则return
  • 根据不同的操作clear add delete set,将合规的effect加入到effects set集合中
  • 遍历effects set集合,执行effect函数

以上,整个响应式系统已经形成了一个基本的闭环。

三、reactivity模块其他API

上一篇大致也介绍了vue源码各目录存放的API,那响应式模块都在runtime-core/reactive目录中。
打开packages/reactivity/src/index.ts,可看到导出的API
主要是四个部分:

  • ref 响应式的关键入口,作用同reactive
  • reactive 响应式的关键入口 (本文重点讲述)
  • computed 同vue2 的computed选项
  • effect 作用同vue2的watcher(Vue3中已经没有了watcher概念,由effect取而代之)
export {
  ref,
  shallowRef,
  isRef,
  toRef,
  toRefs,
  unref,
  proxyRefs,
  customRef,
  triggerRef,
  Ref,
  ToRefs,
  UnwrapRef,
  ShallowUnwrapRef,
  RefUnwrapBailTypes
} from './ref'
export {
  reactive,           // --> 创建响应式对象的入口方法
  readonly,
  isReactive,
  isReadonly,
  isProxy,
  shallowReactive,
  shallowReadonly,
  markRaw,
  toRaw,
  ReactiveFlags,
  DeepReadonly,
  UnwrapNestedRefs
} from './reactive'
export {
  computed,
  ComputedRef,
  WritableComputedRef,
  WritableComputedOptions,
  ComputedGetter,
  ComputedSetter
} from './computed'
export {
  effect,
  stop,
  trigger,
  track,
  enableTracking,
  pauseTracking,
  resetTracking,
  ITERATE_KEY,
  ReactiveEffect,
  ReactiveEffectOptions,
  DebuggerEvent
} from './effect'
export { TrackOpTypes, TriggerOpTypes } from './operations'

总结

vue2响应式原理:

  • 在 Vue2 里内部通过 Object.defineProperty API 劫持数据的变化,深度遍历 data 函数里的对象,给对象里每一个属性设置 getter、setter。
  • 触发 getter 会通过 Dep 类做依赖收集操作,收集当前 Dep.target, 也就是 watcher。
  • 触发 setter,执行 dep.notify 通知收集到的各类 watcher 更新,如 computed watcher、user watcher 、渲染 watcher。

Vue3响应式原理:

  • Vue3 用 ES6的Proxy 重构了响应式,new Proxy(target, handler)
  • Proxy 的 get handle 里 执行track() 用来跟踪收集依赖(收集 activeEffect,也就是 effect )
  • Proxy 的 set handle 里执行 trigger() 用来触发响应(执行收集的 effect)
  • effect 副作用函数 代替了 watcher
    在这里插入图片描述
Logo

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。

更多推荐