vue3响应式分析

vue3响应式分析

  1. 首先对vue3响应式解析之前,需要前置知识ProxyReflect有所了解,,关于这两个知识我推荐看阮一峰老师的ES6入门教程
  2. vue3的响应式我是以reactive为入口进行梳理,流程如下图
    vue3响应式分析
    文章图片
  3. 源码位置:reactivity/src/...,分四部分解析
    • reactive文件:目标对象转化为proxy实例
    • baseHandler文件:基本类型处理器
    • collectionHandlers文件:Map、Set处理器
    • effect文件:收集触发依赖
  4. 如果不想看源码解析,可以直接看总结
reactive
  • reactive:将一个JS对象转为具有响应式的proxy实例
    export function reactive(target: object) { // 如果是只读数据,就直接返回 if (target && (target as Target)[ReactiveFlags.IS_READONLY]) { return target } return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap ) }

  • createReactiveObject
    function createReactiveObject( target: Target, // 源对象 isReadonly: boolean, // 是否只读 baseHandlers: ProxyHandler, // 基本类型的handlers collectionHandlers: ProxyHandler, // 主要针对(set、map、weakSet、weakMap)的handlers proxyMap: WeakMap ) { // 如果不是一个对象,直接返回,并且在开发环境发出警告 if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target } // 如果已经是响应式,直接返回 if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) { return 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 }// 根据不同的类型值赋予不同的handlers,就是我之前图上画的分开处理 /* 把set、Map这种数据与基础数据分开处理,是因为Map、Set中存储的数据必须通过this进行访问 但是被proxy劫持后,this就变成了proxy, 所以需要特殊处理,把劫持方法进行重写 */ const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ) proxyMap.set(target, proxy) return proxy }

baseHandler
baseHandler主要分析reactive的处理器对象mutableHandlers
// 对get set delete has onwKeys做了拦截处理 export const mutableHandlers: ProxyHandler = { get, set, deleteProperty, has, ownKeys }
get
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 ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap ).get(target) ) { return target }const targetIsArray = isArray(target)// 如果target是数组并且key属于一些数组的原始方法,即触发拦截 if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) }const res = Reflect.get(target, key, receiver)// 如果key是symbol的内置方法,或者是原型对象,直接返回 if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res }// 只读对象不收集依赖,因为不会触发依赖更新 if (!isReadonly) { track(target, TrackOpTypes.GET, key) }// 浅层响应立即返回,不递归转化 if (shallow) { return res }// 如果是ref对象(数组除外),返回真正的值, if (isRef(res)) { const shouldUnwrap = !targetIsArray || !isIntegerKey(key) return shouldUnwrap ? res.value : res }if (isObject(res)) { // 由于proxy只能代理一层,所以target[key]的值如果是对象,就继续对其进行代理 return isReadonly ? readonly(res) : reactive(res) } return res } }

数组方法拦截: 对数组的两种原生方法进行了拦截
  • 遍历查找的方法:includes、indexOf、lastIndexOf
  • 改变数组长度的方法:push、pop、shift、unshift、splice
    function createArrayInstrumentations() { const instrumentations: Record = {} ; (['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => { const method = Array.prototype[key] as any instrumentations[key] = function(this: unknown[], ...args: unknown[]) { // 这一步是为了取原始实例,因为当前的this是receiver 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 } } }) /* 因为改变数组长度的方法,执行期间会触发length的get和set 就回导致无限循环track和trigger 所以就用pauseTracking()禁用依赖收集,触发方法后, 再用resetTracking()恢复track */ ; (['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => { const method = Array.prototype[key] as any instrumentations[key] = function(this: unknown[], ...args: unknown[]) { pauseTracking() const res = method.apply(this, args) resetTracking() return res } }) return instrumentations }

set
function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { let oldValue = https://www.it610.com/article/(target as any)[key] if (!shallow) { value = toRaw(value) oldValue = toRaw(oldValue) // 如果原来的值是ref,但新的值不是,则将新的值赋给oldValue.value 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 = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key) const result = Reflect.set(target, key, value, receiver)// 判断receiver是当前对象的proxy实例,防止原型链上的proxy触发更新 if (target === toRaw(receiver)) { // 判断新增属性还是修改属性 if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result } }

deleteProperty、has...
  • deleteProperty、has、ownKeys的源码就不贴了,都是判断key的属性,然后选择触发或者收集依赖
collectionHandlers
  • collection主要分析reactive的重写方法对象mutableInstrumentations
    // 主要对以下原生api进行了改写 const mutableInstrumentations: Record = { get(this: MapTypes, key: unknown) { return get(this, key) }, get size() { return size((this as unknown) as IterableCollections) }, has, add, set, delete: deleteEntry, clear, forEach: createForEach(false, false) }

    get
    function get( target: MapTypes, key: unknown, isReadonly = false, isShallow = false ) { // 获取原始值重新赋值给target target = (target as any)[ReactiveFlags.RAW]// 对target源对象和key进一步获取原始值 const rawTarget = toRaw(target) const rawKey = toRaw(key)// 当前key和原始key均进行依赖收集(track) if (key !== rawKey) { !isReadonly && track(rawTarget, TrackOpTypes.GET, key) } !isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey) const { has } = getProto(rawTarget)// 获取对应的转换函数 const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive /* 如果源对象有key对应的属性,就通过原生get方法取到值, 并对该值进行响应式转换,返回转换后的响应式对象, 如果没有,就去key原始值中去查找 */ if (has.call(rawTarget, key)) { return wrap(target.get(key)) } else if (has.call(rawTarget, rawKey)) { return wrap(target.get(rawKey)) } else if (target !== rawTarget) { target.get(key) } }

size
// 对size属性做get拦截 function size(target: IterableCollections, isReadonly = false) { target = (target as any)[ReactiveFlags.RAW] // 获取size和获取数组的length类似,都用专门的key做依赖收集 !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) return Reflect.get(target, 'size', target) }

set
function set(this: MapTypes, key: unknown, value: unknown) { // 获取value和this上下文的原始值 value = https://www.it610.com/article/toRaw(value) const target = toRaw(this) const { has, get } = getProto(target)/* 判断源对象是否已经存在对应的key 1. 首先查找源对象是否已有key对应的属性 2. 如果没有,再查找key对应的原始值在源对象的属性是否存在 */ let hadKey = has.call(target, key) if (!hadKey) { key = toRaw(key) hadKey = has.call(target, key) } else if (__DEV__) { checkIdentityKeys(target, has, key) }const oldValue = get.call(target, key) target.set(key, value)// 触发依赖,新增属性和修改属性分开进行trigger if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue) } return this }

has、clear...
  • 其余重写方法我就不上代码了,不同点是单个属性触发单个的依赖,如果是遍历所有属性的方法就触发所有依赖
effect
  • effect文件作为响应式的核心,主要负责收集依赖,触发依赖
effect
  • effect函数主要是生成收集依赖所需的依赖函数
    export function effect( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect { // 如果已经是effect函数,取得原来的fn if (isEffect(fn)) { fn = fn.raw } const effect = createReactiveEffect(fn, options)// 如果lazy为false,立即执行一次 if (!options.lazy) { effect() } return effect }

  • createReactiveEffect:生成effect对象
    function createReactiveEffect( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect { const effect = function reactiveEffect(): unknown { // 没有激活,已经调用stop函数停止监听 if (!effect.active) { return fn() } /** * effectStack是一个全局的effect栈结构 * 设计为栈结构是因为如果effect是嵌套时,为了防止内层副作用函数覆盖外层副作用函数,在收集时只收集栈顶的,这样就不会收集到错误的副作用函数 */ if (!effectStack.includes(effect)) { /** * 为了保证当前effect的dep是最新,因为在一些判断处理中,可能会导致一些无效的副作用函数 * 所以为了取消这些不必要的更新,就要清除effect依赖 */ cleanup(effect) try { enableTracking() // 重新收集依赖 effectStack.push(effect) activeEffect = effect return fn() } finally { /* track将依赖函数activeEffect添加到对应的dep中, 然后将activeEffect重置为上一个effect的值 */ effectStack.pop() resetTracking() activeEffect = effectStack[effectStack.length - 1] } } } as ReactiveEffect effect.id = uid++ // 自增ID effect.allowRecurse = !!options.allowRecurse // 递归状态 effect._isEffect = true // 用于标识方法是不是effect effect.active = true // 用于判断当前effect是否激活,有一个stop()来将它设为false effect.raw = fn // effect的执行函数 effect.deps = [] // 用于收集依赖 effect.options = options // 创建effect传入的options return effect }

  • activeEffect就是标记track所需的依赖函数
track
  • track就是baseHandler和collectionHandlers文件中频繁使用的收集依赖函数
  • 首先需要看一个关键变量targetMap
    // targetMap是依赖管理中心,收集依赖和触发依赖都依托于这个Map数据 // 下面是targetMap的定义(target -> key -> dep) // target: 监听的对象源 // key: 监听的键值 // dep:依赖函数 type Dep = Set type KeyToDepMap = Map const targetMap = new WeakMap() // 格式大致为 targetMap = { target: { key1: { fn1, fn2 } key2: { fn1, fn2 } } }

    /** * @param {target} 目标对象 * @param {type} 收集类型 * @param {key} 需要收集依赖的key */ export function track(target: object, type: TrackOpTypes, key: unknown) { // activeEffect为空,就表示当前没有依赖,就没必要做依赖收集了 if (!shouldTrack || activeEffect === undefined) { return } // 获取当前依赖数据 let depsMap = targetMap.get(target)if (!depsMap) { targetMap.set(target, (depsMap = new Map())) }// 如果当前数据中没有所属的依赖key,就重新设置一个 let dep = depsMap.get(key) if (!dep) { depsMap.set(key, (dep = new Set())) } // 添加依赖函数 if (!dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) if (__DEV__ && activeEffect.options.onTrack) { activeEffect.options.onTrack({ effect: activeEffect, target, type, key }) } } }

trigger
// 触发依赖 export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map | Set ) { const depsMap = targetMap.get(target) // 如果没有收集过依赖,直接返回 if (!depsMap) { return }const effects = new Set() const add = (effectsToAdd: Set | undefined) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { // 避免循环触发依赖 类似`effect(() => obj.foo++)` if (effect !== activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }if (type === TriggerOpTypes.CLEAR) { // 在清空前,将对应的依赖全部添加到局部Set depsMap.forEach(add) } 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 // 通过不同的TriggerOpTypes将depsMap的数据取出,添加到effects 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) { // 如果有调度属性,就通过scheduler处理执行 effect.options.scheduler(effect) } else { effect() } }effects.forEach(run) }

总结
  • vue3响应式是通过数据劫持和发布订阅的模式进行处理,首先vue3中的数据是通过proxy做了一层代理,然后处理proxyhandler,基本类型的handler是通过baseHandlers,特殊类型(map,set)的handler是通过collectHandlers
  • handler中获取属性的操作通过track进行依赖收集,修改属性的操作通过trigger进行依赖触发,依赖的收集与触发是通过依赖管理中心targetMap保存的
  • track进行收集时,他收集的是activeEffect,这个变量存储的就是正在触发的副作用函数,activeEffect通过effect()方法进行收集
  • 【vue3响应式分析】effect()常用的三个地方
    • 组件副作用函数
    • watch
    • computed

    推荐阅读