preface
Learning Vue3.0 source code must have an understanding of the following knowledge:
- proxy reflect iterator
- map weakmap set weakset symbol
These knowledge can look at ruan yifeng teacher’s ES6 introduction tutorial.
Read the source code, it is recommended to go through the API under the module first, to understand what functions there are. Then look again at the relevant unit tests, which typically test out all the functional details. It is better to read the details of the source code after understanding the function.
The proxy term
const p = new Proxy(target, handler)
Copy the code
- Handler, a placeholder object that contains a trap, can be translated as a handler object.
- Target: the object that is proxied by Proxy.
Friendship remind
As you read the source code, always ask yourself three questions:
- What’s this?
- Why? Why not?
- Is there a better way to do it?
As the saying goes, know why.
Read the source code to understand not only what features a library has, but also why it is designed the way it is, and whether you can implement it in a better way. If you just stay in the “what” phase, it probably won’t help you. It’s like reading a book, and then you forget it. You have to think about it to understand it better.
The body of the
The reactivity module is a responsive system for Vue3.0. It has the following files:
baseHandlers.ts
collectionHandlers.ts
computed.ts
effect.ts
index.ts
operations.ts
reactive.ts
ref.ts
Copy the code
The API usage and implementation of each file will be explained in order of importance.
Reactive. Ts file
In vue.2x, objects are listened on using Object.defineProperty(). In Vue3.0, Proxy is used to monitor. Proxy has the following advantages over object.defineProperty () :
- You can listen for attributes to be added or deleted.
- You can listen for changes in an index of an array as well as changes in its length.
reactive()
Reactive () is primarily used to transform the target into a reactive proxy instance. Such as:
const obj = {
count: 0
}
const proxy = reactive(obj)
Copy the code
If the object is nested, the child object is recursively converted into a responsive object.
Reactive () is the API exposed to the user, and what it really does is execute the createReactiveObject() function:
// Generate a proxy instance based on target
function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler
, collectionHandlers: ProxyHandler
) {
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.isReactive]) ) {return target
}
// target already has corresponding Proxy
if (
hasOwn(target, isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive)
) {
return isReadonly
? target[ReactiveFlags.readonly]
: target[ReactiveFlags.reactive]
}
// only a whitelist of value types can be observed.
if(! canObserve(target)) {return target
}
const observed = new Proxy(
target,
WeakMap, WeakSet Determine proxy handler parameters according to whether Set, Map, WeakMap, WeakSet
collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
)
// Define an attribute on the original object ("__v_readonly" if read-only, "__v_reactive" otherwise). The value of this attribute is the proxy instance generated from the original object.
def(
target,
isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive,
observed
)
return observed
}
Copy the code
The processing logic for this function is as follows:
- If target is not an object, return target.
- If target is already a proxy instance, return target.
- If Target is not an observable object, return Target.
- Generate the proxy instance and add a property (read-only) to the original target object
__v_readonly
, or for__v_reactive
), points to the proxy instance, and returns the instance. This attribute is added for judgment purposes in step 2 to prevent repeated listening on the same object.
The third and fourth points need to be carried out separately.
What is an observable
const canObserve = (value: Target): boolean= > {
return (
!value[ReactiveFlags.skip] &&
isObservableType(toRawType(value)) &&
!Object.isFrozen(value)
)
}
Copy the code
The canObserve() function is used to check whether a value is an observable if the following conditions are met:
- The value of reactiveFlags. skip cannot be
__v_skip
.__v_skip
Is used to define whether the object can be skipped, that is, not listened on. - The type of target must be one of the following values
Object,Array,Map,Set,WeakMap,WeakSet
To be monitored. - Cannot be a frozen object.
What is the processor object passed to the proxy
As you can see from the code above, when generating the proxy instance, the processor object is generated from a ternary expression:
// collectionTypes 的值为 Set, Map, WeakMap, WeakSet
collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
Copy the code
This ternary expression is very simple. If it’s a normal Object or Array, the processor Object uses baseHandlers; So if it’s Set, Map, WeakMap, WeakSet, we use collectionHandlers.
CollectionHandlers and baseHandlers are introduced from collectionHandlers. Ts and baseHandlers.
How many proxy instances are there
CreateReactiveObject () Creates different proxy instances with different parameters:
- Fully responsive proxy instances are called recursively if they have nested objects
reactive()
. - Read-only proxy instance.
- Shallow responsive proxy instances, in which only the first layer of an object’s properties are responsive.
- Read-only shallow responding proxy instance.
What is the shallow response proxy instance?
The reason why there are shallow proxy instances is that the proxy only proxies the first level attributes of the object, not the deeper attributes. If you really need to generate a fully responsive proxy instance, you need to recursively call Reactive (). However, this process is automatically performed internally and is not perceived by the user.
Some other functions are introduced
// Check whether value is responsive
export function isReactive(value: unknown) :boolean {
if (isReadonly(value)) {
return isReactive((value as Target)[ReactiveFlags.raw])
}
return!!!!! (value && (valueas Target)[ReactiveFlags.isReactive])
}
// Check whether value is read-only
export function isReadonly(value: unknown) :boolean {
return!!!!! (value && (valueas Target)[ReactiveFlags.isReadonly])
}
// Check whether value is a proxy instance
export function isProxy(value: unknown) :boolean {
return isReactive(value) || isReadonly(value)
}
// Convert reactive data to raw data, or if not, return the source data
export function toRaw<T> (observed: T) :T {
return (
(observed && toRaw((observed as Target)[ReactiveFlags.raw])) || observed
)
}
// Set skip property for value to skip proxy and make data unproxied
export function markRaw<T extends object> (value: T) :T {
def(value, ReactiveFlags.skip, true)
return value
}
Copy the code
BaseHandlers. Ts file
Incorrect handlers are defined for the four proxy instances in the basehandlers.ts file. Since there is not much difference between them, we will only cover fully responsive processor objects here:
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
Copy the code
The processor intercepts five operations:
- Get property reading
- Set property setting
- DeleteProperty Deletes an attribute
- Has Indicates whether a property is owned
- ownKeys
OwnKeys can intercept the following operations:
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
Reflect.ownKeys()
Dependencies are collected by the GET, HAS, and ownKeys operations, and triggered by the set and deleteProperty operations.
get
The handler for the get attribute is created with the createGetter() function:
// *#__PURE__*/
const get = /*#__PURE__*/ createGetter()
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
// target is a responsive object
if (key === ReactiveFlags.isReactive) {
return! isReadonly// target is a read-only object
} else if (key === ReactiveFlags.isReadonly) {
return isReadonly
} else if (
/ / if the access key is __v_raw and receiver = = target. __v_readonly | | receiver. = = target __v_reactive
// Return target directly
key === ReactiveFlags.raw &&
receiver ===
(isReadonly
? (target as any).__v_readonly
: (target as any).__v_reactive)
) {
return target
}
const targetIsArray = isArray(target)
// If the target is an array and the key belongs to one of the three methods ['includes', 'indexOf', 'lastIndexOf'], one of the three operations is triggered
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// No matter how the Proxy changes the default behavior, you can always get the default behavior in Reflect.
// If you don't Reflect, something might go wrong when listening on the array
Specific see article / / the data detection in the Vue3 - https://juejin.cn/post/6844903957807169549#heading-10
const res = Reflect.get(target, key, receiver)
// If the key is symbol and belongs to one of Symbol's built-in methods, or if the object is a prototype, the result is returned without collecting dependencies.
if ((isSymbol(key) && builtInSymbols.has(key)) || key === '__proto__') {
return res
}
// Read-only objects do not collect dependencies
if(! isReadonly) { track(target, TrackOpTypes.GET, key) }// Shallow responses are returned immediately without recursive calls to reactive()
if (shallow) {
return res
}
// If it is a ref object, the real value is returned, i.e. Ref. Value, except for arrays.
if (isRef(res)) {
// ref unwrapping, only for Objects, not for Arrays.
return targetIsArray ? res : res.value
}
if (isObject(res)) {
// If the target[key] value is an object, it will continue to be proxied
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
Copy the code
The processing logic of this function should be clear from the code comments. There are a few points that need to be mentioned separately:
Reflect.get()
- Array handling
builtInSymbols.has(key)
True or prototype objects do not collect dependencies
Reflect.get()
The reflect.get () method is similar to reading a property from an object (target[key]), but it operates through a function execution.
Why reflect. get(target, key, receiver) when you can get the value directly from target[key]?
Let’s start with a simple example:
const p = new Proxy([1.2.3] and {get(target, key, receiver) {
return target[key]
},
set(target, key, value, receiver) {
target[key] = value
}
})
p.push(100)
Copy the code
Running this code returns an error:
Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'
Copy the code
But it works fine with a few minor changes:
const p = new Proxy([1.2.3] and {get(target, key, receiver) {
return target[key]
},
set(target, key, value, receiver) {
target[key] = value
return true // Add a line return true
}
})
p.push(100)
Copy the code
This code works fine. Why is that?
The difference is that the new code adds a return true to the set() method. The explanation I found on MDN goes like this:
The set() method should return a Boolean value.
- return
true
Indicates that the property is set successfully. - In strict mode, if
set()
Method returnsfalse
, then one will be thrownTypeError
The exception.
At this time, I tried to execute p[3] = 100 directly, and found that it works normally, only execute push method will report an error. By this point, I had the answer in mind. To verify my guess, I added console.log() to the code, printing out some properties of the code’s execution.
const p = new Proxy([1.2.3] and {get(target, key, receiver) {
console.log('get: ', key)
return target[key]
},
set(target, key, value, receiver) {
console.log('set: ', key, value)
target[key] = value
return true
}
})
p.push(100)
// get: push
// get: length
// set: 3 100
// set: length 4
Copy the code
As you can see from the code above, the length property is also accessed when the push operation is performed. The execution process is as follows: according to the value of length, obtain the final index, then set a new set, and finally change the length.
Combined with MDN’s explanation, my guess is that the array native method should run in strict mode (if anyone knows the truth, please leave it in the comments section). Because in JS a lot of code will work in both non-strict mode and strict mode, but strict mode will give you an error. As in this case, the last attempt to set the length property failed, but the result was fine. If you don’t want an error, you have to return true every time.
Then look at the reflect.set () return statement:
Returns a Boolean value indicating whether the property was set successfully.
So the above code could look like this:
const p = new Proxy([1.2.3] and {get(target, key, receiver) {
console.log('get: ', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set: ', key, value)
return Reflect.set(target, key, value, receiver)
}
})
p.push(100)
Copy the code
Also, no matter how the Proxy changes the default behavior, you can always get the default behavior at Reflect.
From the example above, it’s easy to see why reflect.set () is used instead of Proxy to do the default operation. The same goes for reflect.get ().
Array handling
// If the target is an array and the key belongs to one of the three methods ['includes', 'indexOf', 'lastIndexOf'], one of the three operations is triggered
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
Copy the code
When executing the includes, indexOf, and lastIndexOf methods, the target object is converted to arrayInstrumentations and then executed.
const arrayInstrumentations: Record<string, Function> = {}
;['includes'.'indexOf'.'lastIndexOf'].forEach(key= > {
arrayInstrumentations[key] = function(. args: any[]) :any {
// If the getter is specified in the target object, receiver is the this value when the getter is called.
// So this refers to receiver, the proxy instance, toRaw to get the raw data
const arr = toRaw(this) as any
// Track each value of the array to collect dependencies
for (let i = 0, l = (this as any).length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + ' ')}// we run the method using the original args first (which may be reactive)
If the function returns -1 or false, try again with the original value of the argument
constres = arr[key](... args)if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
returnarr[key](... args.map(toRaw)) }else {
return res
}
}
})
Copy the code
As you can see from the above code, Vue3.0 wraps includes, indexOf, and lastIndexOf. In addition to returning the results of the original methods, Vue3.0 also collects dependent values of each array.
builtInSymbols.has(key)
True or prototype objects do not collect dependencies
const p = new Proxy({}, {
get(target, key, receiver) {
console.log('get: ', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set: ', key, value)
return Reflect.set(target, key, value, receiver)
}
})
p.toString() // get: toString
// get: Symbol(Symbol.toStringTag)
p.__proto__ // get: __proto__
Copy the code
From the execution result of p.tostring (), it triggers two get’s, one we want and one we don’t want (Symbol (symbol.tostringTag) is not clear to me, please leave a comment if anyone knows). Builtinsymbols. has(key) is true and returns it directly to prevent repeated collection of dependencies.
The result of the p.__proto__ execution also triggers a get operation. In general, there is no scenario that requires a separate access to the stereotype, only to access methods on the stereotype, such as p.__proto__.tostring (), so skip dependencies with key __proto__.
set
const set = /*#__PURE__*/ createSetter()
/ / reference document data in Vue3 detection - https://juejin.cn/post/6844903957807169549#heading-10
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 the original value is ref, but the new value is not, assign the new value to ref.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 = 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) {// If the target does not have a key, it is a new operation that needs to trigger a dependency
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// Dependencies are triggered if the old and new values are not equal
// When will old and new values be equal? For example, if you're listening to an array and you do push, you fire setters multiple times
// The first setter is the new value and the second setter is the length change caused by the new value
Value === oldValue
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
Copy the code
The logic of the set() function is not so hard to handle. Track () and trigger() are explained below along with the effect.ts file.
DeleteProperty, has, ownKeys
function deleteProperty(target: object, key: string | symbol) :boolean {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
// If the delete result is true and target owns the key, the dependency is triggered
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
function has(target: object, key: string | symbol) :boolean {
const result = Reflect.has(target, key)
track(target, TrackOpTypes.HAS, key)
return result
}
function ownKeys(target: object) : (string | number | symbol) []{
track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.ownKeys(target)
}
Copy the code
These three functions are relatively simple, just look at the code.
Effect. The ts file
By the time we get through the effect.ts file, the reactive module is almost done.
effect()
Effect () is used primarily with reactive objects.
export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
// If it is already an effect function, get the original fn
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
// If lazy is false, execute it immediately
// Calculates the attribute's lazy to true
if(! options.lazy) { effect() }return effect
}
Copy the code
It is the createReactiveEffect() function that actually creates an effect.
let uid = 0
function createReactiveEffect<T = any> (fn: (... args: any[]) => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
// reactiveEffect() returns a new effect after execution
// It sets itself to activeEffect and then executes fn if reactive properties are read in fn
// The reactive property GET operation is triggered to collect the dependency, which is called activeEffect
const effect = function reactiveEffect(. args: unknown[]) :unknown {
if(! effect.active) {return options.scheduler ? undefined: fn(... args) }// To avoid recursive loops, check
if(! effectStack.includes(effect)) {// Clear dependencies
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
returnfn(... args) }finally {
// track adds the dependent function activeEffect to the corresponding DEP, and then activeEffect in finally
// Reset to the value of the previous effect
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]}}}as ReactiveEffect
effect.id = uid++
effect._isEffect = true
effect.active = true // To determine whether effect is active, a stop() is used to set it to false
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
Copy the code
Cleanup (effect) cleans up all deP instances of an effect association.
function cleanup(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0}}Copy the code
As you can see from the code, the real dependent function is activeEffect. The dependency that performs track() collection is activeEffect. Now let’s look at the track() and trigger() functions.
track()
// Rely on collection
export function track(target: object, type: TrackOpTypes, key: unknown) {
// activeEffect is null, indicating that no dependency is returned
if(! shouldTrack || activeEffect ===undefined) {
return
}
// targetMap dependency manager, used to collect and trigger dependencies
let depsMap = targetMap.get(target)
// targetMap creates a map for each target
// Each target key corresponds to a deP
// Then use the deP to collect the dependent function. When the listening key changes, the dependent function in the DEP is triggered
// Something like this
// targetMap(weakmap) = {
// target1(map): {
// key1(dep): (fn1,fn2,fn3...)
// key2(dep): (fn1,fn2,fn3...)
/ /},
// target2(map): {
// key1(dep): (fn1,fn2,fn3...)
// key2(dep): (fn1,fn2,fn3...)
/ /},
// }
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)// The onTrack event is raised in the development environment
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
Copy the code
TargetMap is a WeakMap instance.
A WeakMap object is a set of key/value pairs where the keys are weakly referenced. The key must be an object, and the value can be arbitrary.
What does weak reference mean?
let obj = { a: 1 }
const map = new WeakMap()
map.set(obj, 'test')
obj = null
Copy the code
When obj is set to null, the reference to {a: 1} is zero, and the object in WeakMap will be reclaimed in the next garbage collection.
However, if weakMap is replaced with a Map data structure, {a: 1} will not be reclaimed even if OBj is null, because the Map data structure is a strong reference and it is still referenced by map.
trigger()
// Trigger dependencies
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 no dependencies have been collected, return directly
if(! depsMap) {// never been tracked
return
}
// The collected dependencies are classified as normal or computed attribute dependencies
// Effects collects ordinary dependencies computedRunners collect dependencies for calculated properties
// Both queues are set structures to avoid repeated collection of dependencies
const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > {
// effect ! ActiveEffect Prevents repeated collection of dependencies
if(effect ! == activeEffect || ! shouldTrack) {// Calculate attributes
if (effect.options.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
} else {
// the effect mutated its own dependency during its execution.
// this can be caused by operations like foo.value++
// do not trigger or we end in an infinite loop}}}})// Add all of target's dependencies to the corresponding queue before the value is cleared
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) { // Emitted when the length property of the array changes
depsMap.forEach((dep, key) = > {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
// If not, and key! == undefined, add a dependency to the corresponding queue
if(key ! = =void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
constisAddOrDelete = 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) {
Scheduler is called if the scheduler exists, and the calculation property owns the scheduler
effect.options.scheduler(effect)
} else {
effect()
}
}
// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
computedRunners.forEach(run)
// Trigger dependent functions
effects.forEach(run)
}
Copy the code
After classifying dependent functions, you need to run the dependencies of computed properties first, because other ordinary dependency functions may contain computed properties. A dependency that executes the calculated property first ensures that the most recent calculated property value is available when the normal dependency executes.
What is the use of type in track() and trigger()?
The type range is defined in the operations.ts file:
// Type of track
export const enum TrackOpTypes {
GET = 'get'./ / get operation
HAS = 'has'./ / from the operation
ITERATE = 'iterate' / / ownKeys operations
}
// The type of trigger
export const enum TriggerOpTypes {
SET = 'set'.// Set the old value to the new value
ADD = 'add'.// Add a new value. For example, add an array of values to an object
DELETE = 'delete'.// Delete operations such as delete operations on objects and pop operations on arrays
CLEAR = 'clear' // Clear for Map and Set operations.
}
Copy the code
Type indicates the type of track() and trigger().
The sequential judgment code in trigger()
if(key ! = =void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
constisAddOrDelete = 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))
}
Copy the code
In trigger() there is a sequence of judgments. What do they do? In fact, they are used to judge the array/collection data structure of the more special operations. Here’s an example:
let dummy
const counter = reactive([])
effect(() = > (dummy = counter.join()))
counter.push(1)
Copy the code
Effect (() => (dummy = counter.join()))) generates a dependency and executes it once. When we execute counter. Join (), we access multiple properties of the array, namely join and length, and trigger track() to collect dependencies. That is, the join Length attribute of the array collects a dependency.
When you execute counter.push(1), you actually set the index 0 of the array to 1. This can be seen from the context by typing the debugger, where key is 0, the index of the array, and the value is 1.
After setting the value, execute trigger(target, triggeroptypes.add, key, value) since it is a new operation. However, as can be seen from the above, only when the key of the array is join length, there is no dependency.
As can be seen from the above two figures, only the join Length attribute has corresponding dependencies.
At this point, a string of if statements from trigger() come into play, one of which looks like this:
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
Copy the code
If target is an array, add the corresponding dependency to the length attribute to the queue. That is, if the key is 0, use the dependency corresponding to length.
There is another subtlety. The queue on which the execution depends is a set data structure. If the key is 0 and the length has the corresponding dependency, the dependency will be added twice. However, since the queue is set, it has the effect of automatic deduplication, avoiding repeated execution.
The sample
It’s hard to understand how responsive data and track() trigger() work together just by looking at code and text. So we’ll use the example to understand:
let dummy
const counter = reactive({ num: 0 })
effect(() = > (dummy = counter.num))
console.log(dummy == 0)
counter.num = 7
console.log(dummy == 7)
Copy the code
The above code execution process is as follows:
- right
{ num: 0 }
Listen and return a proxy instance, which is counter. effect(fn)
Creates a dependency and executes it once when it is createdfn
.fn()
Read num and assign it to dummy.- The read property operation triggers the proxy’s read property interception operation, which collects the dependency generated in Step 2.
counter.num = 7
This action triggers the proxy property setting interception, in which, in addition to returning the new value, the dependency that was just collected is also triggered. In this dependency, assign counter. Num to dummy(num has been changed to 7).
It looks something like this:
CollectionHandlers. Ts file
CollectionHandlers. Ts file contains Map WeakMap Set WeakSet processor object, corresponding to fully responsive proxy instance, shallow responsive proxy instance and read-only proxy instance respectively. This is only the handler object for a fully responsive proxy instance:
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(false.false)}Copy the code
Why only listen for get, set has, etc.? Take your time and look at an example:
const p = new Proxy(new Map(), {
get(target, key, receiver) {
console.log('get: ', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set: ', key, value)
return Reflect.set(target, key, value, receiver)
}
})
p.set('ab'.100) // Uncaught TypeError: Method Map.prototype.set called on incompatible receiver [object Object]
Copy the code
Running the above code results in an error. This has to do with the internal implementation of Map sets, which must be accessed through this. But when reflected, this inside target refers to a proxy instance, so it’s not hard to see why.
So how do we solve this problem? Through the source can be found, in Vue3.0 is through the proxy way to achieve the Map Set and other data structure monitoring:
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
const instrumentations = shallow
? shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
return (target: CollectionTypes, key: string | symbol, receiver: CollectionTypes) = > {
// These three if judgments are handled the same way as baseHandlers
if (key === ReactiveFlags.isReactive) {
return! isReadonly }else if (key === ReactiveFlags.isReadonly) {
return isReadonly
} else if (key === ReactiveFlags.raw) {
return target
}
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}
Copy the code
To simplify the last line of code:
target = hasOwn(instrumentations, key) && key in target? instrumentations : target
return Reflect.get(target, key, receiver);
Copy the code
2. Instrumentations
const mutableInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key, toReactive)
},
get size() {
return size((this as unknown) as IterableCollections)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false.false)}Copy the code
The actual processor object is the mutableInstrumentations. Now look at another example:
const proxy = reactive(new Map())
proxy.set('key'.100)
Copy the code
After generating the proxy instance, execute proxy.set(‘key’, 100). The proxy.set operation triggers the proxy property read interception.
As you can see, the key is set. After intercepting the set operation, reflect. get(Target, key, Receiver) is called, and the Target is not the original target, but the mutableInstrumentations object. That is to say, the final execution is mutableInstrumentations set ().
Then look at the variableInstrumentations processor logic.
get
Reactive (value) if value is an object, return a reactive(value) object, otherwise return value.
const toReactive = <T extends unknown>(value: T): T => isObject(value) ? reactive(value) : value get(this: MapTypes, key: Unknown) {// this refers to proxy return get(this, key, toReactive)} function get(target: MapTypes, key: unknown, wrap: Typeof toReactive | typeof toReadonly | typeof toShallow) {target = toRaw (target) const rawKey = toRaw / / if the key (key) Is reactive, and an additional collection depends on if (key! == rawKey) { track(target, TrackOpTypes.GET, key) } track(target, TrackOpTypes.GET, Const {has, get} = getProto(target) const {has, get} = getProto(target) Return wrap(get.call(target, key))} else if (has.call(target, key)) {return wrap(get.call(target, key))} else if (has.call(target, key)) rawKey)) { return wrap(get.call(target, rawKey)) } }Copy the code
After intercepting get, call GET (this, key, toReactive).
set
function set(this: MapTypes, key: unknown, value: unknown) {
value = toRaw(value)
// Get the raw data
const target = toRaw(this)
// Use the methods on the Target prototype
const { has, get, set } = getProto(target)
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)
const result = set.call(target, key, value)
// Prevent dependencies from being triggered repeatedly if the key already exists
if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) }else if (hasChanged(value, oldValue)) {
// Dependencies are not triggered if the old and new values are equal
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return result
}
Copy the code
Set processing logic is also relatively simple, with annotations at a glance.
There are the rest of has add delete and other methods will not explain, the number of lines of code is relatively small, the logic is very simple, it is recommended to read by yourself.
Ref. Ts file
const convert = <T extends unknown>(val: T): T => isObject(val) ? reactive(val) : val export function ref(value? : unknown) { return createRef(value) } function createRef(rawValue: Shallow = false) {// If the ref object is already shallow, If (isRef(rawValue)) {return rawValue} reactive(rawValue) let value = shallow? rawValue : convert(rawValue) const r = { __v_isRef: True, // to indicate that this is a ref object, Track (r, trackoptypes.get, 'value') return value}, set value(newVal) { if (hasChanged(toRaw(newVal), rawValue)) { rawValue = newVal value = shallow ? NewVal: convert(newVal) // Trigger dependencies when setting values trigger(r, triggeroptypes. SET, 'value', __DEV__? { newValue: newVal } : void 0 ) } } } return r }Copy the code
In ve2. X, base numeric types are not listened on. In Vue3.0, however, this effect can be achieved by ref().
const r = ref(0)
effect(() = > console.log(r.value)) / / print 0
r.value++ / / print 1
Copy the code
Ref () converts 0 to a ref object. If the value passed to ref(value) is an object, reactive(value) is called inside the function to turn it into a proxy instance.
The computed. Ts file
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> / / ifgetterOrOptionsIs a function, is not configurable,setterLet's say it's an empty functionif (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = __DEV__
? () = > {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
// If it is an object, it is readable and writable
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// dirty Is used to determine whether the reactive attributes used to calculate attribute dependencies have been changed
let dirty = true
let value: T
let computed: ComputedRef<T>
const runner = effect(getter, {
lazy: true.// If lazy is true, the resulting effect will not be executed immediately
// mark effect as computed so that it gets priority during trigger
computed: true.scheduler: () = > { / / scheduler
// Trigger when the calculated property executes effect.options.scheduler(effect) instead of effect()
if(! dirty) { dirty =true
trigger(computed, TriggerOpTypes.SET, 'value')
}
}
})
computed = {
__v_isRef: true.// expose effect so computed can be stopped
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
}
Copy the code
Here’s an example of how computed works:
const value = reactive({})
const cValue = computed(() = > value.foo)
console.log(cValue.value === undefined)
value.foo = 1
console.log(cValue.value === 1)
Copy the code
- Generate a proxy instance value.
computed()
Generate a calculated property object when cValue is evaluated (cValue.value
If dirty is false, return the value directly.- Set effect to activeEffect in the effect function and run the getter(
() => value.foo
Value). During the evaluation process, read the value of foo (value.foo
). - This triggers the get property read interception operation, which in turn triggers track to collect the dependency function that is the activeEffect generated in Step 3.
- When a reactive attribute is reassigned (
value.foo = 1
), will trigger the activeEffect function. - And then call
scheduler()
Set dirty to true so that the next time computed is evaluated, the effect function is re-executed.
Index. Ts file
The index.ts file exports the API of the reactivity module.
Vue3 series of articles
- In-depth understanding of Vue3 responsivity principle
- Vue3 template compilation principle
The resources
- Data detection in Vue3
- Vue3 Reactive source code parsing -Reactive article
- Vue3 responsive system source code analysis -Effect