Vue3 Reactivity analysis
Record my Vue3 source analysis process, first parse the principle of Reactivity
Git address: github.com/vuejs/vue-n…
Package structure directory
We went to the Packages directory and found that there were a lot of packages in it. We mainly analyzed the Reactivity package. Go to Packages > Reactivity > SRC
index.ts
Reactivity entry file, mainly exposed methods, for external use
operations.ts
export const enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}
export const enum TriggerOpTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear'
}
Copy the code
Two enumerations are exposed, respectively enumerating the type type that tracks, collects, and triggers dependent on trigger
reactive.ts
Reactive. Ts focuses on how objects are delegated, with a core focus on createReactiveObject
reactive
export function reactive<T extends object> (target: T) :UnwrapNestedRefs<T>
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
)
}
Copy the code
Reactive needs to pass in a target parameter of type Object and first check whether target is a readonly type, and if so, return target. It is then further wrapped with createReactiveObject
createReactiveObject
export const reactiveMap = new WeakMap<Target, any> ()export const readonlyMap = new WeakMap<Target, any> ()function createReactiveObject(
target: Target, // The object passed in
isReadonly: boolean.// Whether it is read-only
baseHandlers: ProxyHandler<any>, // The behavior of the Proxy handles objects
collectionHandlers: ProxyHandler<any>
) {
if(! isObject(target)) {if (__DEV__) {
console.warn(`value cannot be made reactive: The ${String(target)}`)}return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if( target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) {return target
}
// target already has corresponding Proxy
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
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
}
Copy the code
Take a look at the two weekMap collections shown above, representing readonly and Reactive collections, which are used to store created proxies
Weekmap uses object as the key and has a garbage collection mechanism to prevent memory leakage. See the es6 weekamp
First, determine whether target is an object. If not, a warning will be issued during development. So reactive(normal) is not effective.
If the target has a raw object that is not read-only, or is already a responsive object (__v_isReative), then the target object will be returned directly. Such as:
const people = { name: 'hahaha'.age: {num: 18}}const obj = reactive(people)
const __obj = shallowReactive(obj)
Copy the code
The example above will enter into this judgment, and __obj does not make obj shallow. Obj. Age is still a proxy object because target is returned directly
If the current proxy object is found in the proxyMap, it will be returned directly. Otherwise, create a Proxy object with new Proxy(). After creation, store it to weekMap and return the current proxy object. Reactive is over
Again, shallowReactive, ReadOnly, shallowReadonly are created using createReactiveObject, but the parameters passed in are different, isReadonly and different handlers
The rest of the Api
There are also several APIS isReactive isReadonly isProxy toRaw markRaw
// If target is read-only, recursively determine its original value, otherwise find target __v_isReactive
export function isReactive(value: unknown) :boolean {
if (isReadonly(value)) {
return isReactive((value as Target)[ReactiveFlags.RAW])
}
return!!!!! (value && (valueas Target)[ReactiveFlags.IS_REACTIVE])
}
// Check the __v_isReadonly attribute on target
export function isReadonly(value: unknown) :boolean {
return!!!!! (value && (valueas Target)[ReactiveFlags.IS_READONLY])
}
Call isReactive or isReadonly directly
export function isProxy(value: unknown) :boolean {
return isReactive(value) || isReadonly(value)
}
// Recursively returns the original value of the object on the __v_raw property
export function toRaw<T> (observed: T) :T {
return (
(observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed
)
}
export function markRaw<T extends object> (value: T) :T {
def(value, ReactiveFlags.SKIP, true)
return value
}
Copy the code
MarkRaw marks an object with an attribute __v_skip set to true. This is checked when createReactiveObject is created. GetTargetType gets the type of target. __v_SKIP returns targetType. INVALID, so createReactiveObject returns target directly, does not proxy target, and is never wrapped as a responsive object
// only a whitelist of value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
Copy the code
baseHandlers.ts
BaseHandler is the processing of proxies. The following are the types of processing logic in reactive
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
export const readonlyHandlers: ProxyHandler<object> = {
get: readonlyGet,
set(target, key) {
if (__DEV__) {
console.warn(
`Set operation on key "The ${String(key)}" failed: target is readonly.`,
target
)
}
return true
},
deleteProperty(target, key) {
if (__DEV__) {
console.warn(
`Delete operation on key "The ${String(key)}" failed: target is readonly.`,
target
)
}
return true}}export const shallowReactiveHandlers: ProxyHandler<object> = extend(
{},
mutableHandlers,
{
get: shallowGet,
set: shallowSet
}
)
// Props handlers are special in the sense that it should not unwrap top-level
// refs (in order to allow refs to be explicitly passed down), but should
// retain the reactivity of the normal readonly object.
export const shallowReadonlyHandlers: ProxyHandler<object> = extend(
{},
readonlyHandlers,
{
get: shallowReadonlyGet
}
)
Copy the code
The utility function extend is object.assign.
Where the GET functions are:
const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false.true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true.true)
Copy the code
Where the set function is: (where readonly should not have the set function)
const set = /*#__PURE__*/ createSetter()
const shallowSet = /*#__PURE__*/ createSetter(true)
Copy the code
We can see that get and set are created with createGetter and createSetter, respectively, just because the parameters are different
Let’s take a closer look at the createGetter and createSetter content
createGetter
First look at the source code
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 ? readonlyMap : 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 as symbol)
: isNonTrackableKeys(key)
) {
return res
}
if(! isReadonly) { track(target, TrackOpTypes.GET, key) }if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
Copy the code
CreateGetter takes isReadonly (shallow) and isReadonly (shallow), which returns a GET function for proxy handler processing
The first steps mainly focus on __v_isReactive, __v_isReadonly, and __v_raw.
// target[__v_isReactive] is the opposite of target[__v_isReadonly]. // Target [__v_raw] returns a raw valueCopy the code
If it is an array and not read-only, reflect. get(arrayInstrumentations, key, receiver) returns the value. ArrayInstrumentations is similar to array. prototype in that it hijacks the array. prototype method. Part of the method triggers the dependency collection
const arrayInstrumentations: Record<string.Function> = {}
// instrument identity-sensitive Array methods to account for possible reactive
// values; (['includes'.'indexOf'.'lastIndexOf'] as const).forEach(key= > {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ... args: unknown[]) {
const arr = toRaw(this)
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + ' ') // Collect dependencies
}
// we run the method using the original args first (which may be reactive)
const res = method.apply(arr, args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return method.apply(arr, args.map(toRaw))
} else {
return res
}
}
})
// instrument length-altering mutation methods to avoid length being tracked
// which leads to infinite loops in some cases (#2137); (['push'.'pop'.'shift'.'unshift'.'splice'] as const).forEach(key= > {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ... args: unknown[]) {
pauseTracking() // Stop collecting dependencies
const res = method.apply(this, args)
resetTracking() // Start collecting dependencies
return res
}
})
Copy the code
if(! isReadonly) { track(target, TrackOpTypes.GET, key) }Copy the code
If it is not an array, reflect. get(target, key, receiver) obtains the current value to determine whether isReadonly is available. If it is not, track dependency collection will be performed. If it is read-only, because it is read-only, there is no set, so there is no dependency collection.
if (shallow) {
return res
}
Copy the code
If it is a shallow proxy, the current result is returned.
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
Copy the code
If the current result is an object, it will be further proxied.
This is an optimization point because when deep objects are converted to reactive objects, we only need to wrap the first layer, and the deeper ones can wait until get to readonly or Reactive, similar to lazy agents. It doesn’t start recursively like Vue2 did.
createSetter
Look at the source
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
) :boolean {
const oldValue = (target as any)[key]
if(! shallow) { value = toRaw(value)if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn 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)
// don't trigger if target is something up in the prototype chain of original
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
}
}
Copy the code
Similarly, it will return the set function in proxy.
First, get the oldValue oldValue
if(! shallow) { value = toRaw(value)if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true}}else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
Copy the code
If oldValue is a ref, target is not an array, and value is not a ref, then the old ref is directly assigned to the new value and return
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
Copy the code
Returns a Boolean if target is an array and key is an integer index that exceeds the length of the array; otherwise target is an object
Finally, Reflect. Set (target, key, value, receiver) sets the value
if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) }else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
Copy the code
Determine hadkey if it is false. Then the set must be new, so the dependency needs to be triggered. Otherwise, it checks whether the new value and the old value have changed, and if the value has changed, the dependency is triggered
End of set procedure
effect.ts
Effect mainly consists of three core points effect,track and trigger
Three variables need to be paid attention to as follows:
// targetMap is the dependency of target's key
// The data structure is
/ * * * weekMap: {* target: the Map () : {* key: Set () / / Set the key in the dependence on *} *} * /
const targetMap = new WeakMap<any, KeyToDepMap>()
const effectStack: ReactiveEffect[] = [] / / stack effect
let activeEffect: ReactiveEffect | undefined // Current effect
Copy the code
effect
Effect is a side effect function that collects dependencies on variables in the function
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
}
Copy the code
Effect is passed a parameter fn function and an options configuration. If fn is already an effect function, the original value of fn is used. If options.lazy = true, the side effect function is not executed immediately. Finally, the effect function is returned.
Let’s look at the source code for Create Active Effect
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]}}}asReactiveEffect effect.id = uid++ effect.allowRecurse = !! options.allowRecurse effect._isEffect =true
effect.active = true
effect.raw = fn
effect.deps = [] //
effect.options = options
return effect
}
Copy the code
CreateReactiveEffect has two main parameters fn side effect function and options configuration
Take a look at the data structure of Options
export interfaceReactiveEffectOptions { lazy? :boolean // This is the first runscheduler? :(job: ReactiveEffect) = > voidonTrack? :(event: DebuggerEvent) = > void // A custom collection dependency functiononTrigger? :(event: DebuggerEvent) = > void // A custom distribution functiononStop? :() = > voidallowRecurse? :boolean
}
Copy the code
How createReactiveEffect works:
First, check whether the effectStack has an effect function to avoid repeated loading. The effectStack is used to determine which effect is currently active. For example:
const state = reactive({
a: 1.b: 2.c: 3
})
effect(function fn1 () {
console.log(state.a) // activeEffect --> fn1
effect(function fn2 () {
console.log(state.b) // activeEffect --> fn2
})
console.log(state.c) // activeEffect --> fn1
// If fn2 is added to the stack, the current activeEffect must be removed from the stack to keep fn1 as the top element
})
Copy the code
Then look at createReactiveEffect function, the try finally structure is used to avoid the error of fn function execution, do not need to capture and deal with the error, just need to stack the current effect, and then assign the current effect to the current activeEffect. Makes it clear which function effect is. Finally returns fn execution and returns; After that, in finally, we need to push the effectStack off the stack and mark the current activeEffect as the top element of the stack. At this point, the function logic for Create Active Effect ends. Finally, operate on some properties of effect and return the created effect function.
track
Track is to collect dependencies. The principle of responsiveness is to collect dependencies on track and trigger dependencies on trigger
const targetMap = new WeakMap<any, KeyToDepMap>()
export 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
})
}
}
}
Copy the code
Ps: I don't quite understand the effect.active attribute. If you know, please let me know
We learned that the dependency collection method is track(Target, type: TrackOpTypes, key). Then we need to know a targetMap variable, which is used to store the mapping between target object and DEPS. The data structure is
targetMap {
target:[object] : Map() : {key: Set()}}Copy the code
The function runs as follows: first check whether there is a Set stored by target in targetMap, if not, Set a Set, because the Set is indexed by key, and the Set structure is the value, because Set has the function of deduplication, to avoid repeated collection of the same dependency. Such as:
const state = reactive({name: 'hahahh'})
effect(() = >{
console.log(state.name) // Collect dependencies
console.log(state.name) // There is no need to collect dependencies again
})
Copy the code
Finally, look at DEP. Dep adds the current activeEffect. The activeEffect DEPS array also adds the current DEP. Finally, if options has a custom onTrack, the custom collection function onTrack will be executed. This collects dependencies.
If the target variable is passed to the fn by effect, target will collect the current fn and store it in targetMap. For example:
const target = reactive({
name: 'ahhah'
})
effect(function fn(){
console.log(target.name)
})
/* effect if the current activeEffect = effect(fn), the current target will collect the dependent activeEffect * If the current activeEffect is effect(FN), it will also collect the deP. TargetMap = {// weekMap * target: {// Map * name: [activeEffect] // Set * } * } * activeEffect.deps = [[activeEffect] // Set ] */
Copy the code
trigger
Trigger Triggers dependencies and sends collected dependencies in turn.
First take a look at the source code
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) {// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > {
if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }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}}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()
}
}
effects.forEach(run)
}
Copy the code
Look at the parameters
target: object.// Target object, corresponding to key in targetMap
type: TriggerOpTypes, // The type of triggerkey? : unknown,// The key that is triggered corresponds to the target key. In the target Map, the key corresponds to a SetnewValue? : unknown,/ / the new valuesoldValue? : unknown,/ / the old valueoldTarget? :Map<unknown, unknown> | Set<unknown> // oldTarget
Copy the code
If no dependencies are collected, return
const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > {
if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }Copy the code
Then you define an Effects collection to hold a list of collections that need to be triggered. Defines an add function that stores effects from the Set into the effects defined above
If it is a type of clear, all dependencies in target are stored directly into effects.
If you are modifying an array and the key triggered is the length of the array, then you need to collect the dependencies if the corresponding length has dependencies
/ * / * * source code
// Change the length of the array
if (key === 'length' && isArray(target)) {
// If the corresponding length has a dependency, the dependency needs to be collected
depsMap.forEach((dep, key) = > {
if (key === 'length' || key >= newValue) {
// If the length of the change is smaller than the collected index, then this index also needs to trigger effect re-execution
add(dep)
}
})
}
/ * / * * examples
const arr = reactive([1.2.3.4.5])
effect(() = >{
console.log(arr.toString())
})
setTimeout(() = >{
arr.length = 3
})
Copy the code
In the example above, changing the length of the arR will enter the source code above, newVlaue = 3, and depsMap loop will collect the dependencies for the array’s key index. If the arR index is 3 or 4, the arR index is key >= newValue.
If judgment is done, else case.
Else can be an object or a Map. Map is not used in many scenarios
if(key ! = =void 0) {
add(depsMap.get(key))
}
Copy the code
Viod 0 = undefined. If there is a key, add the dependency of the key to the effects. Note that this can only be a modified value, because depmap.get (key) returns undefined if a new key is added, because the newly added key has not collected any dependencies yet, so it can only be a modified value.
The following is to determine which type of update-triggered operation is based on the switch
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
}
Copy the code
Note that if a new element is added to the array, the length of the array will change and the length dependency will be triggered
At this point, all dependencies are stored in the effects variable, which is then iterated through the forEach loop.
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
Copy the code
Note: When the Options function of Effect is set to scheduler, it takes precedence over the options.scheduler function.
Scheduler is involved in computed, which I’ll talk about next.
This trigger ends dispatch.
ref.ts
It also has the principle that the response is and the ref. Let’s talk about how does that work
ref
View the source code
export function ref(value? : unknown) {
return createRef(value)
}
Copy the code
We found that it was created through the createRef function
function createRef(rawValue: unknown, shallow = false) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
Copy the code
The createRef parameter has two main parameters: rawValue rawValue and shallow proxy
shallowRef
export function shallowRef(value? : unknown) {
return createRef(value, true)}Copy the code
We find that it is also created by the createRef function, only shallow = true
function createRef(rawValue: unknown, shallow = false) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
Copy the code
The createRef function is very simple. If rawValue is a ref, it returns directly; If not, create a ref object with new RefImpl().
RefImpl
This is the ref construction factory through which you can create ref objects. Take a look at the source code
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)
}
}
}
Copy the code
RefImpl is a class that has a private variable _value and a __v_isRef = true identifier that hijacks value by get set. That’s why ref creates an object that needs to get its value by.value
The constructor process is simple if _shallow is a shallow proxy and _value is the original value, otherwise wrapped by the convert function
const convert = <T extends unknown>(val: T): T =>
isObject(val) ? reactive(val) : val
Copy the code
The convert function is also very simple. It checks whether val is an object and is wrapped in reactive. This is why a REF passing in a complex object is converted to Reactive.
Let’s look at get. It’s very simple. Get the value and collect the dependencies directly
So if we look at set, first of all it’s going to determine if the old value has changed, and if it has, it’s going to update the old value _rawValue, _value, and then it’s going to trigger the dependency
toRef
Take a look at the source code
export function toRef<T extends object.K extends keyof T> (
object: T,
key: K
) :ToRef<T[K] >{
return isRef(object[key])
? object[key]
: (new ObjectRefImpl(object, key) as any)}Copy the code
Convert the key of an object to a ref and return. If the current object[key] is already a ref object, return it if it is, or wrap it with ObjectRefImpl
class ObjectRefImpl<T extends object.K extends keyof T> {
public readonly __v_isRef = true
constructor(private readonly _object: T, private readonly _key: K) {}
get value() {
return this._object[this._key]
}
set value(newVal) {
this._object[this._key] = newVal
}
}
Copy the code
toRefs
Take a look at the source code.
export function toRefs<T extends object> (object: T) :ToRefs<T> {
if(__DEV__ && ! isProxy(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`)}const ret: any = isArray(object)?new Array(object.length) : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
Copy the code
IsProxy is first used to determine if it is a responsive object. It’s not a warning
We then define a variable ret to determine whether the current object is an array or an object. If it is an array, ret is a new array of equal length, otherwise it is an empty object
Finally, all keys in ret are a REF object through a loop called toRef.
Finally, return ret, and the transformation is complete
conclusion
A REF creates an object from a class and hijacks data by getting a set. Unlike Reactive, which is not represented by a proxy, the class Get set is compiled by Babel and we can see that the final proxy is still represented by Object.defineProperty. Ref usually passes in plain values, but it can also pass in complex types, but underneath it is wrapped in Reactive objects via convert.
computed.ts
Here is only about the principle, but more introduction
computed
export function computed<T> (
options: WritableComputedOptions<T>
) :WritableComputedRef<T>
export 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
}
return newComputedRefImpl( getter, setter, isFunction(getterOrOptions) || ! getterOrOptions.set )as any
}
Copy the code
Computed functions have only one parameter, getterOrOptions
The first thing you define inside a function is the getter setter to determine if it’s a function, and if it’s a function, then the getter is this getterOrOptions
const a = ref(1)
const b = ref(2)
const c = computed(function getterOrOptions () {
return a.value + b.value
})
Copy the code
Otherwise getterOrOptions is an object with a GET set
const a = ref(1)
const b = ref(2)
const c = computed({
get: () = > a.value + b.value,
set: val= > { a.value = val + 1}})Copy the code
Finally, call ComputedRefImpl to create a computed object that is essentially a REF object
ComputedRefImpl
Finally we observe the ComputedRefImpl source code
class ComputedRefImpl<T> {
private_value! : Tprivate _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
}
get value() {
if (this._dirty) {
this._value = this.effect()
this._dirty = false
}
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
Copy the code
We observe that ComputedRefImpl is also a class that has the private _value value _dirty whether to recalculate, the public __v_isRefref identifier, and whether __v_isReadonly is read-only
Constructor () {getsetter () {isReadonly ()
this.effect = effect(getter, {
lazy: true.scheduler: () = > {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value')}}})Copy the code
Pass the getter through the effect function, and the variable in the getter will collect the dependency, which is the current getter
const a = ref(1)
const b = ref(2)
const c = computed(function fn () {
return a.value + b.value
})
Copy the code
In the example above, both A and B collect dependencies on FN because computed passes FN into effect as a getter.
The constructor/effect option configures lazy = true, so the getter does not execute immediately. There is also a scheduler function configured. Remember the trigger function? If effect.options.scheduler exists, it takes precedence over the scheduler function.
// trigger
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
Copy the code
Let’s look at the get function
get value() {
if (this._dirty) {
this._value = this.effect()
this._dirty = false
}
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
Copy the code
When _dirty = false is turned off, the next time you get the value of _value, you don’t need to calculate it again. After that, you go directly to the old cached value, and then collect the dependency and return _value.
The set function is very simple, just by breaking in the setter and following the new value
set value(newValue: T) {
this._setter(newValue)
}
Copy the code
Finally, a quick example of a computed update process:
const a = ref(1)
const b = ref(2)
const c = computed(function fn () {
return a.value + b.value
})
effect(function effectNameC(){
console.log(c)
})
setTimeout(() = > {
a.value = 10
}, 1000)
Copy the code
In computed, FN is wrapped by Effect and scheduler.
So in the above example, when the timer starts after 1s and the value of A changes to 10, it will trigger, and the dependencies it collects are the fn function wrapped by Effect, so it will execute the options.scheduler function
scheduler: () = > {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value')}}Copy the code
When _dirty happens to be false after get, it enters judgment. _dirty is marked true again, finally triggering a dependency on this(the current calculated property)(c’s dependency in the above example). The dependency of C (effectNameC in this example), once triggered, goes into C’s get function, and now _dirty = true is recalculated, triggering this.effect to update the value.
Since computed End
conclusion
The end of the first chapter scatter flowers. Record my source code to share the process, above some mistakes, welcome correction. If help, trouble one key three connect.