本文源码版本 Vue3.2.11
,Vue2 响应式源码剖析点这里 深入浅出 Vue2 响应式原理源码剖析
我们知道相较 Vue2.x 的响应式 Vue3 对整个响应式都做了重大升级;然后 Vue3.2 相较 3.0 版本源码又做了许多变更,一起来看看吧
Vue3 和 Vue2 响应式区别
响应式性能的提升
根据 8 月 10 号尤大发布 Vue3.2 说明原文 得知:
- 更高效的
ref
实现,读取提升约 260%
,写入提升约 50%
- 依赖收集速度提升约
40%
- 减少内存消耗约
17%
使用上的区别
Vue2 中只要写在组件中 data 函数返回的对象里的属性 自动就有响应式
Vue3 则是通过 ref 定义普通类型响应式和 reactive 定义复杂类型响应式数据
1 2 3 4 5 6
| <script setup> import { ref, reactive, toRefs } from "vue" const name = ref('沐华') const obj = reactive({ name: '沐华' }) const data = { ...toRefs(obj) } </script>
|
扩展:通过 toRefs 可以把响应式对象转为普通对象
因为使用 reactive 定义的响应式对象在进行解构(展开)或者销毁的时候,响应式就会失效了,因为 reactive 实例下有很多属性,解构就丢失了,所以在需要解构且保持响应式的时候就可以用 toRefs
源码目录结构区别
Vue2 响应式的源码核心部分在 src/core/observer
这个目录,但是里面也引入了很多其他目录东西,不独立,耦合度比较高
Vue3 响应式源码全部在 packages/reactivity
这个目录下,不涉及其他任何地方,功能独立,而且单独发布成 npm 包,可以集成进其他框架
原理上的区别
我们知道在 Vue2 中使用 Object.defineProperty
实现响应式对象,而这种方式是存在一些缺陷的
- 基于属性拦截,初时化时会递归全部属性,对性能有一定影响,并且后续给对象中添加的新属性,无法触发响应式,对应的解决办法是通过
Vue.set()
方法来添加新属性
- 无法检测到数组内部变化,对应的解决方法是通过重写了 7 个会改变原数组的方法
而在 Vue3 中则是用 Proxy
进行重构,完全取代了 defineProperty
,就不存在上述问题了
那么是如何解决这些问题的呢?
对象
先看下 Vue2 在首次渲染时的响应式处理,源码地址:src/core/observer/index.js - 157行
1 2 3 4 5 6 7 8
| ... Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { ... }, set: function reactiveSetter (newVal) { ... } }) ...
|
由参数可以看出,它是需要根据具体的 key 去 obj 里找 obj[key]
,来进行拦截处理的,所以就有需要满足一个前置条件,一开始就得知道 key 是啥,所以就需要遍历每一个 key,并定义 getter
、setter
,这也是为什么后面添加的属性没有响应式的原因
而 Vue3 中则是这样的
1 2 3 4 5 6 7
|
new Proxy(target, { get(target, key) {}, set(target, key, value) {}, })
|
同样由参数就可以看出,开始创建响应式的时候,根本不需要知道这个对象里有哪些字段,因为不用传具体的 key,这样就算是后面新增的,自然也能够拦截得到
也就是说不会上来就递归遍历把所有用到没用到的都设置响应式,从而加快了首次渲染
数组
在 Vue2 中
- 一个是因为
Object.defineProperty
这个 api 无法监听到数组长度的变化
- 二是因为数组长度可能很长,比如
lenth
是大几千,上万的,所以尤大考虑到性能消耗与用户体验,设计的就是 Vue 本身就不能监听直接通过下标修改数组元素的操作
延伸一个问题,为什么无法监听到数组长度的变化呢?先看图
如图就是 configurable
为 true 时对应的值才能被改变,也可以理解成才能被监听,而 length 本身是不可以被监听的,所以数组长度改变时也监听不到
如果强行把它的 configurable 修改为 true 则会报错,因为各大浏览器厂商和 JS 引擎规定就不允许修改 length 的 configurable,规定就是这样,所以在源码里才会有这样的代码
1 2 3 4 5
| const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return }
|
所以为了更好的操作数组并触发响应式,就重写了会改变原数组的 7 个方法,再通过 ob.dep.notify()
手动派发更新,源码地址:src/core/observer/array.js
在 Vue3 中使用 Proxy,Proxy 就是代理的意思,回顾一下语法
1 2 3 4
| new Proxy(target, { get(target, key) {}, set(target, key, value) {}, })
|
根据 MDN 中对 Proxy 的描述 是这样的
target
: 被 Proxy 代理虚拟化的对象。它常被作为代理的存储后端。根据目标验证关于对象不可扩展性或不可配置属性的不变量(保持不变的语义)
注意了:数组的 length 就是不可配置的属性,所以 Proxy 天生就能监听数组长度变化
依赖收集的区别
Vue2 中是通过 Observer
,Dep
,Watcher
这三个类来实现依赖收集,详细流程可以看我另一篇文章 深入浅出 Vue 响应式原理源码剖析
Vue3 中是通过 track
收集依赖,通过 trigger
触发更新,本质上就是用 WeakMap,Map,Set 来实现,具体可以看下面源码的实现过程
缺点区别
上面也有提到 Vue2 中 defineProperty
监听不到新增对象属性/数组内部变化,而且属性值是对象的话会多次调用 observe()
递归遍历,还有就是会对所有数据属性都设置监听,包括没有用到的属性,性能上自然就没那么好
在 Vue3 中主要就是大量使用 Es6+ 新特性,在老版本浏览器上兼容就没那么好
Vue3 响应式源码解析
先看一下在 Vue3 中定义的几个用来标记目标对象 target 的类型的 flag,下面先是枚举的属性
源码地址:packages/reactivity/reactive.ts -16行
1 2 3 4 5 6 7 8 9 10 11 12
| export const enum ReactiveFlags { SKIP = '__v_skip', IS_REACTIVE = '__v_isReactive', IS_READONLY = '__v_isReadonly', RAW = '__v_raw' } export interface Target { [ReactiveFlags.SKIP]?: boolean [ReactiveFlags.IS_REACTIVE]?: boolean [ReactiveFlags.IS_READONLY]?: boolean [ReactiveFlags.RAW]?: any }
|
然后开始一一解析
reactive()
源码地址:packages/reactivity/src/reactive.ts -87行
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export function reactive<T extends object>(target: T): UnwrapNestedRefs<T> export function reactive(target: object) { // 如果 target 是只读类型的对象就直接返回 if (target && (target as Target)[ReactiveFlags.IS_READONLY]) { return target } return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap ) }
|
这里代码很简单,主要就是调用 createReactiveObject()
createReactiveObject()
源码地址:packages/reactivity/src/reactive.ts -173行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any> ) { 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 } const targetType = getTargetType(target) if (targetType === TargetType.INVALID) { return target } const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ) proxyMap.set(target, proxy) return proxy }
|
大概了解了这个方法里要做的事,接下来我们还要先明白传入的几个参数是什么
参数配置定义是这样的
1 2 3 4 5 6 7 8 9
| const get = createGetter() const set = createSetter() export const mutableHandlers: ProxyHandler<object> = { get, set, deleteProperty, has, ownKeys, }
|
这里 get、has、ownKeys 会触发依赖收集 track()
set、deleteProperty 会触发更新 trigger()
其中有两个重要的方法就是 get 和 set 对应的 createGetter 和 createSetter
createGetter()
源码地址:packages/reactivity/src/baseHandlers.ts -80行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| 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) if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } const res = Reflect.get(target, key, receiver) if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res } if (!isReadonly) { track(target, TrackOpTypes.GET, key) } if (shallow) { return res } if (isRef(res)) { const shouldUnwrap = !targetIsArray || !isIntegerKey(key) return shouldUnwrap ? res.value : res } if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res) }
return res } }
|
track() 依赖收集放到后面,和派发更新一起
createSetter()
源码地址:packages/reactivity/src/baseHandlers.ts -80行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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) if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } } else { } const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) 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 } }
|
trigger() 派发更新放到后面
有个疑问,为什么用 Reflect.get() 和 Reflect.set(),而不是直接用 target[key]?
根据 MDN 介绍 set() 要返回一个布尔值,比如 Reflect.set() 会返回一个是否修改成功的布尔值,直接赋值 target[key] = newValue
,而不返回 true 就会报错。而且不管 Proxy 怎么修改默认行为,都可以通过 Reflect 获取默认行为。get() 同理
接着是依赖收集和派发更新相关的核心内容,相关代码全部在 effect.ts 文件中,该文件主要是处理一些副作用,主要内容如下:
- 创建 effect 入口函数
- track 依赖收集
- trigger 派发更新
- cleanupEffect 清除 effect
- stop 停止 effect
- trackStack 收集栈的暂停(pauseTracking)、恢复(enableTracking)和重置(resetTracking)
我们先从入口函数看起
effect()
源码地址:packages/reactivity/src/effect.ts -145行
这里主要就是暴露一个创建 effect
的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| export function effect<T = any>( fn: () => T, options?: ReactiveEffectOptions ): ReactiveEffectRunner { if ((fn as ReactiveEffectRunner).effect) { fn = (fn as ReactiveEffectRunner).effect.fn } const _effect = new ReactiveEffect(fn) if (options) { extend(_effect, options) if (options.scope) recordEffectScope(_effect, options.scope) } if (!options || !options.lazy) { _effect.run() } const runner = _effect.run.bind(_effect) as ReactiveEffectRunner runner.effect = _effect return runner }
|
可以看出主要的就是在 effect 里使用 new ReactiveEffect
创建了一个 _effect
实例,并且函数最后返回的 runner 方法就是指向 ReactiveEffect 里的 run 方法
由此可见在执行副作用函数 effect
方法时,实际上执行的就是这个 run
方法
所以我们就需要知道知道这个 ReactiveEffect
和它返回的 run
方法,里面都干了些什么
我们继续看
ReactiveEffect
源码地址:packages/reactivity/src/effect.ts -53行
这里主要做的就是在依赖收集前用栈数据结构 effectStrack
来做 effect
的执行调试,保证当前 effect
的优先级最高,并及时清除己收集依赖的内存
需要注意的是标记完成后就会执行 fn()
函数,这个 fn 函数就是副作用函数封闭的函数,如果是在组件渲染,就是 fn 就是组件渲染函数,执行的时候就会就会访问数据,就会触发 target[key]
的 getter
,然后触发 track
进行依赖收集,这也就是 Vue3 的依赖收集过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| const effectStack: ReactiveEffect[] = []
const trackStack: boolean[] = []
const maxMarkerBits = 30 export class ReactiveEffect<T = any> { active = true deps: Dep[] = [] computed?: boolean allowRecurse?: boolean onStop?: () => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void constructor( public fn: () => T, public scheduler: EffectScheduler | null = null, scope?: EffectScope | null ) { recordEffectScope(this, scope) } run() { if (!this.active) { return this.fn() } if (!effectStack.includes(this)) { try { effectStack.push((activeEffect = this)) enableTracking() trackOpBit = 1 << ++effectTrackDepth if (effectTrackDepth <= maxMarkerBits) { initDepMarkers(this) } else { cleanupEffect(this) } return this.fn() } finally { if (effectTrackDepth <= maxMarkerBits) { finalizeDepMarkers(this) } trackOpBit = 1 << --effectTrackDepth resetTracking() effectStack.pop() const n = effectStack.length activeEffect = n > 0 ? effectStack[n - 1] : undefined } } } stop() { if (this.active) { cleanupEffect(this) if (this.onStop) { this.onStop() } this.active = false } } }
|
track()
源码地址:packages/reactivity/src/effect.ts -188行
track
就是依赖收集器,负责把依赖收集起来统一放到一个依赖管理中心
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
const targetMap = new WeakMap<any, KeyToDepMap>() export function isTracking() { return shouldTrack && activeEffect !== undefined } export function track(target: object, type: TrackOpTypes, key: unknown) { if (!isTracking()) { 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 = createDep())) } const eventInfo = __DEV__ ? { effect: activeEffect, target, type, key } : undefined trackEffects(dep, eventInfo) }
|
trackEffects()
源码地址:packages/reactivity/src/effect.ts -212行
这里把当前激活的 effect 收集进对应的 effect 集合,也就是 dep
这里了解一下两个标识符
dep.n
:n 是 newTracked
的缩写,表示是否是最新收集的(是否当前层)
dep.w
:w 是 wasTracked
的缩写,表示是否已经被收集,避免重复收集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| export function trackEffects( dep: Dep, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { let shouldTrack = false if (effectTrackDepth <= maxMarkerBits) { if (!newTracked(dep)) { dep.n |= trackOpBit shouldTrack = !wasTracked(dep) } } else { shouldTrack = !dep.has(activeEffect!) } if (shouldTrack) { dep.add(activeEffect!) activeEffect!.deps.push(dep) if (__DEV__ && activeEffect!.onTrack) { activeEffect!.onTrack( Object.assign( { effect: activeEffect! }, debuggerEventExtraInfo ) ) } } }
|
trigger()
源码地址:packages/reactivity/src/effect.ts -243行
trigger
是 track 收集的依赖对应的触发器,也就是负责根据映射关系,获取响应式函数,再派发通知 triggerEffects
进行更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| 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 }
let deps: (Dep | undefined)[] = [] if (type === TriggerOpTypes.CLEAR) { deps = [...depsMap.values()] } else if (key === 'length' && isArray(target)) { depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { deps.push(dep) } }) } else { if (key !== void 0) { deps.push(depsMap.get(key)) } switch (type) { case TriggerOpTypes.ADD: ... break case TriggerOpTypes.DELETE: ... break case TriggerOpTypes.SET: ... break } }
const eventInfo = __DEV__ ? { target, type, key, newValue, oldValue, oldTarget } : undefined
if (deps.length === 1) { if (deps[0]) { if (__DEV__) { triggerEffects(deps[0], eventInfo) } else { triggerEffects(deps[0]) } } } else { const effects: ReactiveEffect[] = [] for (const dep of deps) { if (dep) { effects.push(...dep) } } if (__DEV__) { triggerEffects(createDep(effects), eventInfo) } else { triggerEffects(createDep(effects)) } } }
|
triggerEffects()
源码地址:packages/reactivity/src/effect.ts -330行
执行 effect 函数,也就是『派发更新』中的更新了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| export function triggerEffects( dep: Dep | ReactiveEffect[], debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) { if (__DEV__ && effect.onTrigger) { effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) } if (effect.scheduler) { effect.scheduler() } else { effect.run() } } } }
|
创建 ref
源码地址:packages/reactivity/src/ref.ts
这里开始主要就是处理 ref
相关的了,先看一下几个相关函数,后面会用的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| export function isRef(r: any): r is Ref { return Boolean(r && r.__v_isRef === true) }
function createRef(rawValue: unknown, shallow: boolean) { if (isRef(rawValue)) { return rawValue } return new RefImpl(rawValue, shallow) }
export function shallowRef(value?: unknown) { return createRef(value, true) }
export function unref<T>(ref: T | Ref<T>): T { return isRef(ref) ? (ref.value as any) : ref }
|
RefImpl
源码地址:packages/reactivity/src/ref.ts -87行
从上面我们知道 ref
对象是通过 new RefImpl()
创建的,RefImpl 类的实现比较简单,这里就不多废话了,请看注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class RefImpl<T> { private _value: T private _rawValue: T
public dep?: Dep = undefined public readonly __v_isRef = true
constructor(value: T, public readonly _shallow: boolean) { this._rawValue = _shallow ? value : toRaw(value) this._value = _shallow ? value : convert(value) } get value() { trackRefValue(this) return this._value } set value(newVal) { newVal = this._shallow ? newVal : toRaw(newVal) if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal this._value = this._shallow ? newVal : convert(newVal) triggerRefValue(this, newVal) } } }
|
trackRefValue()
源码地址:packages/reactivity/src/ref.ts -29行
这里主要做一些 ref 依赖收集之前的工作,主要就是判断是否激活了 effect,有没有收集过依赖的 effect,没有就创建一个 dep,准备收集,然后调用本文上面的 trackEffects
进行依赖收集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| export function trackRefValue(ref: RefBase<any>) { if (isTracking()) { ref = toRaw(ref) if (!ref.dep) { ref.dep = createDep() } if (__DEV__) { trackEffects(ref.dep, { target: ref, type: TrackOpTypes.GET, key: "value", }) } else { trackEffects(ref.dep) } } }
|
triggerRefValue()
源码地址:packages/reactivity/src/ref.ts -47行
这里进行 ref 派发更新,源码比较简单,没啥说的,就是区分一下开发环境,然后执行本文上面的 triggerEffects
执行对应在的 effect 函数进行更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export function triggerRefValue(ref: RefBase<any>, newVal?: any) { ref = toRaw(ref) if (ref.dep) { if (__DEV__) { triggerEffects(ref.dep, { target: ref, type: TriggerOpTypes.SET, key: "value", newValue: newVal, }) } else { triggerEffects(ref.dep) } } }
|
到这里,Vue3 的响应式对象的源码就基本上剖析结束了