关于前端:Vue3最啰嗦的Reactivity数据响应式原理解析

206次阅读

共计 12532 个字符,预计需要花费 32 分钟才能阅读完成。

根本差不多了,图有点小丑,也能够看他人比拟全的图。@mxin

前言

良久没有接触 Vue 了,在前几天观看尤大的直播时议论对于 看源码 的一些认识,是为了更好的上手vue? 还是想要学习外部的框架思维?

国内前端:面试,面试会问。

在大环境下仿佛曾经卷到了只有你是开发者,那么必然须要去学习源码,不管你是实习生,还是应届生,或者是多年教训的老前端。

如果你停滞下来,不跟着卷,那么突然之间带来的压力就会将你冲垮,以至于你可能很难在内卷的环境下生存上来,哪怕你是对的。

有趣味的话能够浏览一下 @掘金泥石流大佬的写的程序员焦虑水平自测表。

仿佛讲了太多的题外话,与其发牢骚不如静下心来,一起学习一下 Reactivity 的一些基本原理吧,置信浏览完文章的你会对 vue 3 数据响应式有更加粗浅的了解。

而之所以抉择 Reactivity 模块来说,是因为其耦合度较低,且是 vue3.0 外围模块之一,性价比老本十分高。

根底篇

在开始之前,如果不理解 ES6 呈现的一些 高阶 api,如,Proxy, Reflect, WeakMap, WeakSet,Map, Set等等能够自行翻阅到资源章节,先理解前置知识点在从新观看为最佳。

Proxy

@vue/reactivity 中,Proxy是整个调度的基石。

通过 Proxy 代理对象,才可能在 get, set 办法中实现后续的事件,比方 依赖收集 effecttrack, trigger 等等操作,在这里就不具体开展,后续会具体开展叙述。

如果有同学急不可待,加上天资痴呆,ES6有肯定根底,能够间接跳转到原理篇进行观看和思考。

先来手写一个简略的 Proxy。在其中handleCallback 中写了了 set, get 两个办法,又来拦挡以后属性值变动的数据监听。先上代码:

const user = {
  name: 'wangly19',
  age: 22,
  description: '一名掉头发微不足道的前端小哥。'
}

const userProxy = new Proxy(user, {get(target, key) {console.log(`userProxy: 以后获取 key 为 ${key}`)
    if (target.hasOwnProperty(key)) return target[key]
    return {}},
  set(target, key, value) {console.log(`userProxy: 以后设置值 key 为 ${key},value 为 ${value}`)
    let isWriteSuccess = false
    if (target.hasOwnProperty(key)) {target[key] = value
      isWriteSuccess = true
    }
    return isWriteSuccess
  }
})

console.log('myNaame', userProxy.name)

userProxy.age = 23

当咱们在对值去进行赋值批改和打印的时候,别离触发了以后的 setget办法。

这一点十分重要,对于其余的一些属性和应用办法在这里就不过多的赘述,

Reflect

Reflect并不是一个类,是一个内置的对象。这一点呢大家要知悉,不要间接 实例化 (new) 应用,它的性能比拟和 Proxyhandles有点相似,在这一点根底上又增加了很多 Object 的办法。

在这里咱们不去深究 Reflect, 如果想要深刻理解性能的同学,能够在后续资源中找到对应地址进行学习。在本章次要介绍了通过Reflect 平安的操作对象。

以下是对 user 对象的一些 批改 操作的实例,能够参考一下,在后续可能会用到。

const user = {
  name: 'wangly19',
  age: 22,
  description: '一名掉头发微不足道的前端小哥。'
}

console.log('change age before' , Reflect.get(user, 'age'))

const hasChange = Reflect.set(user, 'age', 23)
console.log('set user age is done?', hasChange ? 'yes' : 'no')

console.log('change age after' , Reflect.get(user, 'age'))

const hasDelete = Reflect.deleteProperty(user, 'age')

console.log('delete user age is done?', hasDelete ? 'yes' : 'none')

console.log('delete age after' , Reflect.get(user, 'age'))

原理篇

当理解了前置的一些常识后,就要开始 @vue/reactivity 的源码解析篇章了。上面开始会以简略的思路来实现一个根底的 reactivity,当你理解其本质原理后,你会对@vue/reactivity依赖收集 (track)触发更新 (trigger),以及 副作用 (effect) 到底是什么工作。

reactive

reactivevue3 中用于生成 援用类型 api

const user = reactive({
  name: 'wangly19',
  age: 22,
  description: '一名掉头发微不足道的前端小哥。'
})

那么往函数外部看看,reactive办法到底做了什么?

在外部,对传入的对象进行了一个 target 的只读判断,如果你传入的 target 是一个只读代理的话,会间接返回掉。对于失常进行 reactive 的话则是返回了 createReactiveObject 办法的值。

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,
    mutableCollectionHandlers,
    reactiveMap
  )
}

createReactiveObject

createReactiveObject 中,做的事件就是为 target 增加一个 proxy 代理。这是其外围,reactive最终拿到的是一个 proxy 代理,参考 Proxy 章节的简略事例就能够晓得 reactive 是如何进行工作的了,那么在来看下 createReactiveObject 做了一些什么事件。

首先先判断以后 target 的类型,如果不符合要求,间接抛出正告并且返回原来的值。

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}

对于这些判断代码来说,浏览起来并不是很艰难,留神 if () 中判断的条件,看看它做了一些什么动作即可。而 createReactiveObject 做的最重要的事件就是创立 targetproxy, 并将其放到 Map 中记录。

而比拟有意思的是其中对传入的 target 调用了不同的 proxy handle。那么就一起来看看handles 中到底干了一些什么吧。

const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy

handles 的类型

在对象类型中,将 ObjectArrayMap,Set, WeakMap,WeakSet 辨别开来了。它们调用的是不同的Proxy Handle

  • baseHandlers.tsObject & Array会调用此文件下的 mutableHandlers 对象作为Proxy Handle
  • collectionHandlers.tsMap,Set, WeakMap,WeakSet会调用此文件下的 mutableCollectionHandlers 对象作为Proxy Handle
/**
 * 对象类型判断
 * @lineNumber 41
 */
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
  }
}

会在 new Proxy 的依据返回的 targetType 判断。

const proxy = new Proxy(
  target,
  targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)

因为篇幅无限,下文中只举例 mutableHandlers 当作剖析的参考。当了解 mutableHandlers 后对于 collectionHandlers 只是工夫的问题。

Proxy Handle

在下面说到了依据不同的 Type 调用不同的 handle,那么一起来看看mutableHandlers 到底做了什么吧。

在根底篇中,都晓得 Proxy 能够接管一个配置对象,其中咱们演示了 getset的属性办法。而 mutableHandlers 就是何其雷同意义的事件,在外部别离定义 get, set, deleteProperty, has, oneKeys 等多个属性参数,如果不晓得什么含意的话,能够看下 Proxy Mdn。在这里你须要了解 被监听的数据
只有产生 增查删改 后,绝大多数都会进入到对应的回执通道外面。

在这里,咱们用简略的 get, set 来进行简略的模仿实例。

function createGetter () {return (target, key, receiver) => {const result = Reflect.get(target, key, receiver)
      track(target, key)
      return result
    }
}

const get = /*#__PURE__*/ createGetter()

function createSetter () {return (target, key, value, receiver) => {const oldValue = target[key]
  const result = Reflect.set(target, key, value, receiver)
  if (result && oldValue != value) {trigger(target, key)
  }
  return result
  }
}

get 的时候会进行一个 track 的依赖收集,而 set 的时候则是触发 trigger 的触发机制。在 vue3,而triggertrack的话都是在咱们 effect.ts 当中申明的,那么接下来就来看看 依赖收集 响应触发 到底做了一些什么吧。

Effect

对于整个 effect 模块,将其分为三个局部来去浏览:

  • effect:副作用函数
  • teack: 依赖收集,在 proxy 代理数据 get 时调用
  • trigger: 触发响应,在 proxy 代理数据发生变化的时候调用。

effect

通过一段实例来看下 effect 的应用,并且理解它主要参数是一个函数。在函数外部会帮你执行一些副作用记录和个性判断。

effect(() => {proxy.user = 1})

来看看 vueeffect干了什么?

在这里,首先判断以后参数 fn 是否是一个 effect,如果是的话就将raw 中寄存的 fn 进行替换。而后从新进行 createReactiveEffect 生成。

export function effect<T = any>(fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {if (isEffect(fn)) {fn = fn.raw}
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {effect()
  }
  return effect
}

createReactiveEffect 会将咱们 effect 推入到 effectStack 中进行入栈操作,而后用 activeEffect 进行存取以后执行的 effect,在执行完后会将其进行 出栈 。同时替换activeEffect 为新的栈顶。

而在 effect 执行的过程中就会触发 proxy handle 而后 tracktrigger两个要害的函数。

function createReactiveEffect<T = any>(fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {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
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

来看一个简版的effect,抛开大多数代码包袱,上面的代码是不是清晰很多。

function effect(eff) {
  try {effectStack.push(eff)
    activeEffect = eff
    return eff(...argsument)
    
  } finally {effectStack.pop()
    activeEffect = effectStack[effectStack.length  - 1]
  }
}

track(依赖收集)

track 的时候,会进行咱们所熟知的依赖收集,会将以后 activeEffect 增加到 dep 外面,而说起这一类的关系。它会有一个一对多对多的关系。

从代码看也十分的清晰,首先咱们会有一个一个总的 targetMap 它是一个 WeakMapkeytarget(代理的对象), value是一个 Map,称之为depsMap,它是用于治理以后target 中每个 keydeps也就是副作用依赖,也就是以前熟知的 depend。在vue3 中是通过 Set 来去实现的。

第一步先凭借以后 target 获取 targetMap 中的 depsMap,如果不存在就进行targetMap.set(target, (depsMap = new Map())) 初始化申明,其次就是从 depsMap 中拿以后 keydeps, 如果没有找到的话,同样是应用 depsMap.set(key, (dep = new Set())) 进行初始化申明,最初将以后 activeEffect 推入到deps, 进行依赖收集。

    1. targetMap 中找target
    1. depsMap 中找key
    1. activeEffect 保留到 dep 外面。

这样的话就会造成一个一对多对多的构造模式,外面寄存的是所有被 proxy 劫持的依赖。

function track(target: object, type: TrackOpTypes, key: unknown) {if (!shouldTrack || activeEffect === undefined) {return}
  let depsMap = targetMap.get(target)
  if (!depsMap) {targetMap.set(target, (depsMap = new Map()))
  }
  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(响应触发)

trigger 的时候,做的事件其实就是触发以后响应依赖的执行。

首先,须要获取以后 key 下所有渠道的 deps,所以会看到有一个effectsadd函数, 做的事件十分的简略,就是来判断以后传入的 depsMap 的属性是否须要增加到 effects 外面,在这里的条件就是 effect 不能是以后的 activeEffecteffect.allowRecurse,来确保以后 set key 的依赖都进行执行。

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

上面上面熟知的场景就是判断以后传入的一些变动行为,最常见的就是在 trigger 中会传递的 TriggerOpTypes 行为,而后执行 add 办法将其将符合条件的 effect 增加到 effects 当中去,在这里 @vue/reactivity 做了很多数据就变异上的行为,如 length 变动。

而后依据不同的 TriggerOpTypes 进行 depsMap 的数据取出,最初放入 effects。随后通过run 办法将以后的 effect 执行,通过 effects.forEach(run) 进行执行。

if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {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
    }
  }

run 又做了什么呢?

首先就是判断以后 effectoptions下有没有 scheduler,如果有的话就应用schedule 来解决执行,反之间接间接执行effect()

if (effect.options.scheduler) {effect.options.scheduler(effect)
    } else {effect()
    }

将其缩短一点看解决逻辑,其实就是从 targetMap 中拿对应 key 的依赖。

const depsMap = targetMap.get(target)
  if (!depsMap) {return}
  const dep = depsMap.get(key)
  if (dep) {dep.forEach((effect) => {effect()
    })
  }

Ref

家喻户晓,refvue3 对一般类型的一个响应式数据申明。而获取 ref 的值须要通过 ref.value 的形式进行获取,很多人认为 ref 就是一个简略的 reactive 但其实不是。

在源码中,ref最终是调用一个 createRef 的办法,在其外部返回了 RefImpl 的实例。它与 Proxy 不同的是,ref的依赖收集和响应触发是在 getter/setter 当中,这一点能够参考图中 demo 模式,链接地址 gettter/setter。

export function ref<T extends object>(value: T): ToRef<T>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {return createRef(value)
}

function createRef(rawValue: unknown, shallow = false) {if (isRef(rawValue)) {return rawValue}
  return new RefImpl(rawValue, shallow)
}

如图所示,vuegetter 中与 proxy 中的 get 一样都调用了 track 收集依赖,在 setter 中进行 _value 值更改后调用 trigger 触发器。

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

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

那么你当初应该晓得:

  • proxy handlereactive 的原理,而 ref 的原理是getter/setter
  • get 的时候都调用了 trackset 的时候都调用了trigger
  • effect是数据响应的外围。

Computed

computed个别有两种常见的用法, 一种是通过传入一个对象,外部有 setget办法,这种属于 ComputedOptions 的模式。

export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(options: WritableComputedOptions<T>): WritableComputedRef<T>
export function computed<T>(getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>)

而在外部会有 getter / setter 两个变量来进行保留。

getterOrOptions 为函数的时候,会将其赋值给与getter

getterOrOptions 为对象的时候,会将 setget别离赋值给setter,getter

随后将其作为参数进行实例化 ComputedRefImpl 类,并将其当作返回值返回进来。

let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  
  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any

那么 ComputedRefImpl 干了一些什么?

计算属性的源码,其实绝大多数是依赖后面对 effect 的一些了解。

首先,咱们都晓得,effect能够传递一个 函数 和一个 对象 options

在这里将 getter 当作函数参数传递,也就是 副作用 ,而在options 当中配置了 lazyscheduler

lazy示意 effect 并不会立刻被执行,而 scheduler 是在 trigger 中会判断你是否传入了 scheduler,传入后就执行scheduler 办法。

而在 computed scheduler 当中,会判断以后的 _dirty 是否为 false,如果是的话会把_dirty 设置为 true,且执行trigger 触发响应。

class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true

  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    this.effect = effect(getter, {
      lazy: true,
      scheduler: () => {if (!this._dirty) {
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })

    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

而在 getter/setter 中会对 _value 进行不同操作。

首先,在 get value 中,判断以后 ._dirty 是否为 true,如果是的话执行缓存的effect 并将其返回后果寄存到 _value,并执行track 进行依赖收集。

其次,在 set value 中,则是调用 _setter 办法从新新值。

get value() {// the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    if (self._dirty) {self._value = this.effect()
      self._dirty = false
    }
    track(self, TrackOpTypes.GET, 'value')
    return self._value
  }

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

资源援用

上面是一些参考资源,有趣味的小伙伴能够看下

  • ES6 系列之 WeakMap
  • Proxy 和 Reflect
  • Vue Mastery
  • Vue Docs
  • React 中引入 Vue3 的 @vue/reactivity 实现响应式状态治理

总结

如果你应用 vue 的话强烈建议本人 debug 将这一块看完,相对会对你写代码有很大的帮忙。vue3热火朝天,目前曾经有团队作用于生产环境进行我的项目开发,社区的生态也缓缓的倒退起来。

@vue/reactivity的浏览难度并不高,也有很多优质的教程,有肯定的工作根底和代码常识都能循序渐进的了解下来。
我集体其实并不需要将其了解的滚瓜烂熟,了解每一行代码的意思什么的,而是理解其核心思想,学习框架理念以及一些框架开发者代码写法的思路。这都是可能借鉴并将其排汇成为本人的常识。

对于一个曾经转到 React 生态体系下的前端来说,读 Vue 的源码其实更多的是丰盛本人在思维上的常识,而不是为了面试而去读的。正如同你背书不是为了考试,而是学习常识。在当初的环境下,很难做到这些事件,静下心来分心了解一件常识不如背几篇面经。

正文完
 0