Introduction: This paper is based on Vue 3.0.5 for analysis, mainly for personal learning comb process. It was also the first time I had written a serious article. Correct me if I am wrong.
preface
In the last Vue3 analysis series, we analyzed the mount logic and learned that in fact, in the last step of the component mount setupRenderEffect function will collect the side effects (componentEffect) and execute them. Render the current instance by executing the deputy and generate the final page. So how does Vue update the page by modifying the data? Let’s move on to reactive logic parsing of data.
Don’t say much about the source code
createReactiveEffect
First, the createReactiveEffect function that appeared in the previous parsing is called. Among them, activeEffects need to be noted. Its main use is to mark the current side effects that need to be collected, so that the corresponding dependency objects can be associated during dependency collection
// packages/reactivity/src/effect.ts
// TODO generates effect
function createReactiveEffect<T = any> (
fn: () => T, // The side effect function passed in
options: ReactiveEffectOptions / / configuration items
) :ReactiveEffect<T> {
const effect = function reactiveEffect() :unknown {
// If effect is not active, which happens after the stop method in effect is called, then the original method fn is called if the scheduler function was not previously called, otherwise it is returned.
if(! effect.active) {return options.scheduler ? undefined : fn()
}
// What about active effects? Check whether the current effect is in the effectStack. If so, no call is made. This is mainly to avoid repeated calls.
if(! effectStack.includes(effect)) {// Remove side effects
cleanup(effect)
try {
// To enable tracing, a trace state is stored in the trackStack queue and corresponds to the following effectStack to distinguish which effect needs to be tracked
enableTracking()
// Place the current effect in the effectStack
effectStack.push(effect)
// Then set activeEffect to the current effect
activeEffect = effect
// fn and return a value
return fn()
} finally {
When this is complete, the finally phase pops the current effect, restores the original collective-dependent state, and restores the original activeEffect.
effectStack.pop()
// Reset the trace
resetTracking()
activeEffect = effectStack[effectStack.length - 1]}}}as ReactiveEffect
effect.id = uid++ // Add id unique effecteffect.allowRecurse = !! options.allowRecurse effect._isEffect =true // is used to indicate whether the method is effect
effect.active = true // Whether to activate
effect.raw = fn // The callback method passed in
effect.deps = [] // Hold the current effect DEP array
effect.options = options // Create effect is passed options
return effect
}
Copy the code
Let’s look at how to collect dependencies and trigger corresponding effects.
The data response module reactivity
reactive
// packages/reactivity/src/reactive.ts
export function reactive(target: object) {
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
// Is the object __v_isReadonly read-only yes Read-only returns target without hijacking
return target
}
// The actual operation is in createReactiveObject
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
Copy the code
conclusion
Check whether the current object is IS_READONLY, and return if it is. If not, do the proxy logic.
createReactiveObject
// Omit some DEV environment code
// packages/reactivity/src/reactive.ts
// TODO creates proxy objects
function createReactiveObject(
target: Target, // The object to be proxied
isReadonly: boolean, The read-only attribute indicates whether the agent to be created is readable only,
baseHandlers: ProxyHandler<any>, [Object, Array] [Object, Array]
collectionHandlers: ProxyHandler<any> // Is a hijacking of the Set type, i.e. [Set, Map, WeakMap, WeakSet].
) {
if(! isObject(target)) {// Return target if it is not an object
return target
}
if( target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) {// If the target is already proxied, return it directly
return target
}
// We create two types of proxy, one is responsive and the other is read-only
const proxyMap = isReadonly ? readonlyMap : reactiveMap
// If the incoming target already exists, it is returned directly
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const targetType = getTargetType(target)
If the type is invalid, return the object directly
if (targetType === TargetType.INVALID) {
return target
}
// TODO is the final agent
WeakMap weakSet Set Map // Object Array // Determine the method to be called according to the status
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// Insert the proxy Map table target => proxy
proxyMap.set(target, proxy)
return proxy
}
Copy the code
conclusion
The object to be proxied is judged to check whether it meets the proxied conditions. The final judgment type is the proxy type. Let’s examine the corresponding proxy types
mutableHandlers
The main concern is get() and set() because it is through them that the dependency collection is implemented
// packages/reactivity/src/baseHandlers.ts
The /** * handler.get() method is used to intercept an object's read property operations. * The handler.set() method is the catcher for setting the property value operation. * The handler.deleteProperty() method is used to intercept delete operations on object properties. The handler.has() method is a proxy method for the in operator. * handler ownKeys () method is used to intercept * Object in getOwnPropertyNames () * Object in getOwnPropertySymbols () * Object. The keys () * for... In circulation * /
// The TODO agent listens for status
// Normal listening status
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
// Get has four states in the source code
const get = /*#__PURE__*/ createGetter() // Normal get
const shallowGet = /*#__PURE__*/ createGetter(false.true) // Shallow get
const readonlyGet = /*#__PURE__*/ createGetter(true) // Read-only get
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true.true) // Shallow read-only get
// Data is returned mainly via isReadonly shallow and the target type target and whether the data is propped
// The factory function of get
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// If key is proxied, return the corresponding state! isReadonly
if (key === ReactiveFlags.IS_REACTIVE) {
return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
If the key is read-only, return the corresponding state isReadonly
return isReadonly
} else if (
key === ReactiveFlags.RAW &&
receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
) {
return target
}
// Whether the target is an array
const targetIsArray = isArray(target)
// It is not read-only and is an array with the attributes 'includes', 'indexOf' and 'lastIndexOf'
if(! isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {// If the getter is specified in the target object, receiver is the this value when the getter is called. Receiver => Proxy Proxy object
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
// If the key is Symbol, return the res value directly.
if (
isSymbol(key)
? builtInSymbols.has(key as symbol)
: key === `__proto__` || key === `__v_isRef`
) {
return res
}
// If TODO is not read-only, dependency collection is performed
if(! isReadonly) {// TODO core track relies on collection
track(target, TrackOpTypes.GET, key)
}
// If shallow get returns data directly, no listening is performed
if (shallow) {
return res
}
// check if it is Ref
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
}
// If it is an object
if (isObject(res)) {
// Not read-only continue proxy create read-only continue proxy create proxy
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
// Set has two states in the source code
const set = /*#__PURE__*/ createSetter() // Normal set
const shallowSet = /*#__PURE__*/ createSetter(true) // Shallow set
// TODO set factory function
function createSetter(shallow = false) {
return function set(target: object, key: string | symbol, value: unknown, receiver: object) :boolean {
// Get the value you want to change
const oldValue = (target as any)[key]
// Not in shallow mode
if(! shallow) {// toRaw is a simple function that iterates until the object passed does not exist reactiveFlags. RAW, and the desired RAW data is retrieved (before proxy).
value = toRaw(value)
if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true}}// Returns a length comparison if the target is an array
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)) {
// TODO core trigger relies on update trigger
if(! hadKey) {// Field changes are new
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// The length changes to update
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
Copy the code
conclusion
This is similar to the implementation logic of Vue2. The big change is that Vue2 hijacks the attributes of the Object via Object.defineProperty. Dependencies are collected and triggered within sets and GETS. It is also worth noting that Object.defineProperty can only hijack Object attributes. There is no way to hijack an array. So in Vue2. Rewrite the seven can modify the array method, a push, pop, shift, unshift, splice, sort, reverse. Dependency collection and dependency triggering occur during execution.
Vue3 abandons Vue2’s hijacking of attributes. The object is changed to be represented by Proxy.
-
The Proxy advantages
- You can listen directly on the entire object rather than the properties.
- You can listen for changes in the array directly.
- There are 13 interception methods, such as
ownKeys
,deleteProperty
,has
Is such asObject.defineProperty
Do not have. - It returns a new Object, so we can just manipulate the new Object for our purposes, whereas Object.defineProperty can only be modified by iterating through the Object attributes.
- As a result of the new standard, browser manufacturers will continue to focus on performance optimization, known as the performance bonus of the new standard.
-
The Proxy shortcomings
- Browser compatibility issues and can’t be smoothed with Polyfill.
-
Object. DefineProperty advantage
- It has good compatibility and supports IE9, but Proxy has browser compatibility problems and cannot be smoothed by Polyfill.
-
Object. DefineProperty faults
- Only properties of objects can be hijacked, so we need to traverse each property of each object.
- Do not listen on arrays. The array is listened on by overriding the seven methods that change the data.
- Nor is it new to es6
Map
,Set
These data structures do the listening. - Can also not listen to add and delete operations, through
Vue.set()
和Vue.delete
To implement responsive.
track
Dependency collection and dependency mapping stage
// packages/reactivity/src/effect.ts
export function track(target: object, type: TrackOpTypes, key: unknown) {
// Receive three parameters target Proxy object type Trace type key Trigger proxy object key
// Determine that there is no current pause trace and no active side effects
// Remember activeEffect. This is an effect that is assigned when a side effect is created
if(! shouldTrack || activeEffect ===undefined) {
return
}
// targetMap ===> weakMap weak reference to see if the current proxy object is already proxied
let depsMap = targetMap.get(target)
// If not, store it in weakMap object to avoid repeated proxy
if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// depsMap===> Map object
let dep = depsMap.get(key)
// Checks if properties on the current target are proxied to avoid double proxiing
if(! dep) {// dep is a Set of unique values in the deP
depsMap.set(key, (dep = new Set()))}// if the set dependent on the key also has no activeEffect, add the activeEffect to the set and insert the current set into the deps array of the activeEffect
if(! dep.has(activeEffect)) { dep.add(activeEffect)// Deps is the set array for the key dependent in effect
activeEffect.deps.push(dep)
}
}
Copy the code
trigger
export function trigger(
target: object, // Trigger the object
type: TriggerOpTypes, // type Indicates the trigger typekey? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
// If depsMap is empty, the dependency is not traced
if(! depsMap) {return
}
// Side effect mapping table
const effects = new Set<ReactiveEffect>()
// The main effect is to add the side effects of each change key to the effects mapping table. Form an execution stack. Because it's Set. The only value
// TODO so if multiple properties are modified under the same component. This is because the Set type actually performs the effect execution to replay the last time. Clever solution to multiple effect triggers and repeated rendering
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > {
if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }// If it is clear, all effects of the object are triggered.
if (type === TriggerOpTypes.CLEAR) {
// depsMap stores all attributes of the current target and their corresponding side effects => key:effect
// passed to the add method
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
// If key is length and target is array, effects with key is length and key is greater than or equal to the new length are triggered because the array length is changed.
depsMap.forEach((dep, key) = > {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// Get the side effects of the key
if(key ! = =void 0) {
add(depsMap.get(key))
}
// Add modify delete
switch (type) {
case TriggerOpTypes.ADD:
/ / add
if(! isArray(target)) {// Not an array
// packages/reactivity/src/collectionHandlers.ts
Size createForEach createIterableMethod TODO overwrites several methods that intercept length related methods
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
// packages/reactivity/src/collectionHandlers.ts
// TODO createIterableMethod
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// Yes array key is an integer. When changing the length
add(depsMap.get('length'))}break
case TriggerOpTypes.DELETE:
/ / delete
// All in the DEV environment
if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
/ / set
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break}}const run = (effect: ReactiveEffect) = > {
// effect. Options. Scheduler is used to run effect if a scheduler function is passed in.
// However, effect may not be used in scheduler, such as computed, because computed runs effect lazily
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
Effects.foreach (run) => effects.foreach (effect => run(effect))
// Perform all side effects
effects.forEach(run)
}
Copy the code
conclusion
The function of track and trigger is to establish the dependency relationship so that the corresponding side effect function in the mapping table can be executed during attribute modification, and then the Patch method of VNode can be triggered. The DOM is updated and the page is finally updated.
The flow chart
Add to that flow chart I’ve been talking about.
conclusion
Vue3 the entire component initialization (mount) process has been covered. The component update process will be supplemented later.
Previous articles:
Vue3 parse series createAppAPI function
Vue3 parse series of mount functions