Vue 3 原理解析 - 数据侦测原理 Vue 2.x 及之前的版本中实现数据的可响应,需要对 Object 和 Array 分别进行处理:
Object 类型通过 Object.definePropery 把属性转换成 getter/setter ,这个过程需要递归侦测所有的对象 key 来实现深度侦测
Array 类型通过对改变数组自身的几个方法进行拦截来实现对数组的可响应
而在 Vue3 中则是通过 Proxy 实现数据读取和设置拦截,在捕捉器中实现数据依赖收集和触发视图更新的操作。
ProxyProxy 是 ES6 中新引入的特性。
Proxy 正如其含字面意义 - “代理” 所表明的那样,它是对象与对象之间的一层代理,程序可以通过 Proxy 来访问或操作目标对象,进而可以实现基本操作的拦截和自定义。
基本语法如下:
1 const proxy = new Proxy (target, handler);
参数:
target 要代理的原始对象,可以是任何类型的对象,包括原生数组、函数甚至另一个代理对象
handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理的行为
举个栗子:
1 2 3 4 5 let const obj = {};const proxyHandler = {};const objProxy = new Proxy (obj, proxyHandler);objProxy.a = 37 ;
handler 中可以设置对原始对象各种操作的捕获器,在捕获器中可以自定义修改操作的行为。
1 2 3 4 5 6 7 const obj = { a : 'A' };const proxy = new Proxy (obj, { get : (o, p ) => { if (p === 'a' ) return 'This is A' ; return o[p]; } });
有时我们并不知道目标对象的具体类型,这种情况下使用 Reflect 返回 trap (捕捉器)相应的默认行为。
Reflect 是一个内置的不可构造的非函数对象,它提供了拦截 JavaScript 各种操作的方法。
1 2 3 4 5 6 7 8 9 10 let target = {};const proxyObj = new Proxy (target, { get : (obj, prop ) => { return Reflect .get(obj, prop, receiver); }, set : (obj, prop, value ) => { Reflect .set(obj, prop, value, receiver); return true ; } });
对于 set 操作,可能会引起代理对象的属性更改,导致 set 多次执行。比如当代理对象是数组时,执行 push 操作,会多次触发 set,同时也引发 get 操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 let array = [ 1 , 3 ];const proxyHandler = { get : (target, prop, receiver ) => { console .log('Get value : ' , prop, Reflect .get(target, prop, receiver)); return Reflect .get(target, prop, receiver); }, set : (target, prop, value, receiver ) => { console .log('Set value :' , prop, value); return Reflect .set(target, prop, value, receiver); } }; const objProxy = new Proxy (array, proxyHandler);objProxy.push(4 );
当要代理的对象是多层结构时,Proxy 的代理只能到第一层,即不能感知操作对象内部的 set 操作,但是 get 会被执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 let obj = { 'foo' : 'foo' , 'bar' : { 'name' : 1 } };const proxyHandler = { get : (target, prop, receiver ) => { console .log('Get value : ' , prop, Reflect .get(target, prop, receiver)); return Reflect .get(target, prop, receiver); }, set : (target, prop, value, receiver ) => { console .log('Set value :' , prop, value); Reflect .set(target, prop, value, receiver); return true ; } }; const objProxy = new Proxy (obj, proxyHandler);console .log(objProxy.bar.name);
一句话总结:Proxy 是 ES6 引入的新特性,可以使用 Proxy 间接访问或操作其代理的对象。
Vue 3 中响应式数据实现 Vue 3 中响应式系统的 API 主要有
reactive
ref
computed
readonly
watchEffect
watch
其中 reactive 是最核心的 API
reactiveVue 3 中是用全局的 WeakMap 来存储正在追踪的响应式对象,如 reactiveMap 、shallowReactiveMap 、readonlyMap、shallowReadonlyMap 。
1 2 3 4 export const reactiveMap = new WeakMap <Target, any>()export const shallowReactiveMap = new WeakMap <Target, any>()export const readonlyMap = new WeakMap <Target, any>()export const shallowReadonlyMap = new WeakMap <Target, any>()
此外还定义了代理对象的可响应操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export const mutableHandlers: ProxyHandler<object> = { get, set, deleteProperty, has, ownKeys }; export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = { get : createInstrumentationGetter(false , false ) }
reactive 创建响应式对象的流程如下:
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 function reactive (target: object ) { if (target && (target as Target).__v_isReadonly) { return target } return createReactiveObject( target, false , mutableHandlers, mutableCollectionHandlers, reactiveMap ) } 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 }
上面提到了对于多层对象的代理,set 并不能感知到内层对象的变化,但是 get 会被触发,Vue 3 利用这个原理,再对内层数据进行一次代理。
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 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 } }
使用 reactive 创建了响应式对象后,改变响应式对象的属性操作,会被侦测,并即时地在目标对象上触发对应的响应。
拿 set 方法,即赋值来举例,就是:
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 const set = createSetter()const shallowSet = createSetter(true )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 } }
ref由于 Proxy 代理的目标需是对象形式,不能对简单类型进行代理,为此 Vue 3 中使用 ref 函数为简单类型的值生成了一个包装,这样就可以通过 recative 函数构建响应式数据了。
包装对象的实现如下:
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 class RefImpl <T > { private _value: T private _rawValue: T public dep?: Dep = undefined public readonly __v_isRef = true constructor (value: T, public readonly _shallow = false ) { 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) } } }
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 interface Ref<T = any> { [RefSymbol]: true value : T } function ref (value?: unknown ) { return createRef(value) } function createRef (rawValue: unknown, shallow = false ) { if (isRef(rawValue)) { return rawValue } let value = shallow ? rawValue : convert(rawValue) const r = { __v_isRef : true , get value () { track(r, TrackOpTypes.GET, 'value' ) return value }, set value (newVal ) { if (hasChanged(toRaw(newVal), rawValue)) { rawValue = newVal value = shallow ? newVal : convert(newVal) trigger( r, TriggerOpTypes.SET, 'value' , __DEV__ ? { newValue : newVal } : void 0 ) } } } return r }
computed1 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 function computed <T >( getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T> ) { 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 } let dirty = true let value: T let computed: ComputedRef<T> const runner = effect(getter, { lazy : true , computed : true , scheduler : () => { if (!dirty) { dirty = true trigger(computed, TriggerOpTypes.SET, 'value' ) } } }) computed = { __v_isRef : true , effect : runner, get value () { if (dirty) { value = runner() dirty = false } track(computed, TrackOpTypes.GET, 'value' ) return value }, set value (newValue: T ) { setter(newValue) } } as any return computed }
effect1 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 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 } function createReactiveEffect <T = any >( fn: (...args: any []) => T, options: ReactiveEffectOptions ): ReactiveEffect <T > { const effect = function reactiveEffect (...args: unknown[] ): unknown { if (!effect.active) { return options.scheduler ? undefined : fn(...args) } if (!effectStack.includes(effect)) { cleanup(effect) try { enableTracking() effectStack.push(effect) activeEffect = effect return fn(...args) } finally { effectStack.pop() resetTracking() activeEffect = effectStack[effectStack.length - 1 ] } } } as ReactiveEffect effect.id = uid++ effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = options return effect }
关于 trigger 和 track trigger 用来当观测值发生发生变化时通知观察者
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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 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>() const computedRunners = new Set <ReactiveEffect>() const add = (effectsToAdd: Set <ReactiveEffect> | undefined ) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { if (effect !== activeEffect || !shouldTrack) { if (effect.options.computed) { computedRunners.add(effect) } else { effects.add(effect) } } else { } }) } } if (type === TriggerOpTypes.CLEAR) { depsMap.forEach(add) } else if (key === 'length' && isArray(target)) { depsMap.forEach((dep, key ) => { if (key === 'length' || key >= (newValue as number )) { add(dep) } }) } else { if (key !== void 0 ) { add(depsMap.get(key)) } const isAddOrDelete = type === TriggerOpTypes.ADD || (type === TriggerOpTypes.DELETE && !isArray(target)) if ( isAddOrDelete || (type === TriggerOpTypes.SET && target instanceof Map ) ) { add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY)) } if (isAddOrDelete && target instanceof Map ) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) } } 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() } } computedRunners.forEach(run) effects.forEach(run) }
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 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 }) } } }