Last time, made a wave of VUe3 initialization process source code learning, in fact, that is just an appetizer, learning initialization this part of the source code can let me first understand the basic architecture of VUe3, and then also let me have a deeper understanding of the vue3 initialization process and usage. The benefit of learning source code is in this, after doing the project with vue3 in the initialization of what problems can be faster positioning, this is also to improve their programming ability. This time I’m going to learn about VUE3, which is the most important part of the VUE framework: responsiveness.

Previous article: VuE3 source reading notes — initialization process

vue2 vs vue3

The first thing we need to understand is what the data response is. It listens for changes in your data and updates the content associated with that data. This is responsive, and it can make our web pages more flexible and soulful.

The realization of the vue2

I haven’t actually studied the source code for VUe2, but I do know that the response of VUe2 is implemented via Object.defineProperty.

const reactive = (obj, key, val) = > {
    Object.defineProperty(obj, key, {
        get() {
            return val
        },
        set(v) {
            val = v;
            // Data changes are detected and related processing is performed
            update()
        }
    })
}
Copy the code

The idea is to use Object.definePropert to dynamically set the key of your data Object, and then get and set the data Object. Because Reactive creates a closure, the current val state is stored when we call the function, so we return the val in the closure when we fetch data. If there’s a set value we’re going to store it in the closure to update it, and then when we do get we’re going to get the new value back. That’s the basic way it works. Then we call an external function to do something else on set, and that’s reactive.

The realization of the vue3

Vue3 rewrites the reactive formula, which is implemented using a proxy. This new es6 feature, but compatibility is really poor. But because it’s really good for reactive, VUe3 uses proxy to rewrite reactive.

const newReactive = (obj) = > {
    return new Proxy(obj, {
        get(target, key) {
            return target[key]
        },
        set(target, key, val) {
            target[key] = val
            // Data changes are detected and related processing is performed
            update()
        }
    })
}
Copy the code

I don’t want to talk about the implementation of proxy, it is not only set and GET but also a lot of other interception operations, so you can achieve more ideas. So here is the advantage of vuE2?

  1. Vue2 initializes it by iterating over all the keys of the object, which affects its initialization speed. If the data is very large, then the traversal and the memory consumption of the previous closure to hold the data are also large. However, proxy adds a layer of interception on the outside of the target object, and the proxy will process any data only when it uses any data, so its memory consumption and speed are relatively fast.
  2. Vue2 has special treatment for arrays. We know that in VUe2 you cannot modify data directly with array indexes. And vue. set and vue. delete must be used to add or delete a key from an object. It’s a bit of a departure from our programming habits, but vuE3 fixes this problem and makes typing more enjoyable.

The source code to read

Before reading the source code of vue3, we need to take a look at how the vue3 response is declared, so that we can find the source location.

const state = reactive({ count: 0 })
state.count++
console.log(state.count)
Copy the code

Reactive: Vue3 reactive: vuE3 reactive: VUE3 reactive: VUE3 reactive: VUE3 reactive: VUE3 reactive: VUE3 reactive: VUE3 reactive: VUE3 reactive: VUE3 reactive: VUE3 reactive We find the location of the reactive function in the source code: the vue – next/packages/reactivity/SRC/reactive. Ts

export function reactive(target: object) {
  // Check whether it is a readonly object
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
Copy the code

CreateReactiveObject this function is simple, first check whether it is a readonly object, if not, use the createReactiveObject function to create a response, let’s look at the createReactiveObject function.

export const reactiveMap = new WeakMap<Target, any> ()export const readonlyMap = new WeakMap<Target, any> ()function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
    // Not an object, WARN warns and returns directly
  if(! isObject(target)) {if (__DEV__) {
      console.warn(`value cannot be made reactive: The ${String(target)}`)}return target
  }
  // Whether the object is already a proxy
  if( target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) {return target
  }
  // Go to reactiveMap
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // This is also a judgment to determine whether it is whitelist (Object, Array, Map, WeakMap, Set, WeakSet type).
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // Hang a layer of proxy proxy on the object (responsive core!)
  const proxy = new Proxy(
    target,
    // Objects go baseHandlers, arrays go collectionHandlers
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy

Copy the code

Here we find that Vue uses WeakMap to do mapping, just to learn a wave of WeakMap. WeakMap’s key can only be objects, and the objects referenced by its key name are weak references, that is, the garbage collection mechanism does not take this reference into account. Therefore, as long as all other references to the referenced object are cleared, the garbage collection mechanism frees the memory occupied by the object. In other words, once it is no longer needed, the key name object and the corresponding key value pair in WeakMap will disappear automatically, without manually deleting the reference. This improves performance. The first part of this function is just a bunch of judgments about what you’re passing in, and then you hang a layer of proxy on the target object and return the proxy. So now the focus is on the proxy handler, which is passed in through reactive, where mutableHandlers handle objects and arrays, MutableCollectionHandlers processing Map, WeakMap, Set, WeakSet. Let’s look at the most common ones that deal with objects and arrays.

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
Copy the code

I’m just going to focus on get and set, so let’s look at the get method.

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)
    }
    // Get the value via Reflect
    const res = Reflect.get(target, key, receiver)
    // If it is already a built-in method, no proxy is required
    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      return res
    }
    // track relies on collection
    if(! isReadonly) { track(target, TrackOpTypes.GET, key) }if (shallow) {
      return res
    }
    // If it is a ref object, it is propped to value
    if (isRef(res)) {
      constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
    }
    // Check whether the object is nested, and if so, check reactive again
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}
Copy the code

In get, Reflect is used to get the value, then track is used to collect dependencies, and finally return the result. Notice that the above process is repeated if the object is found to be nested. Implement deep listening. Now let’s see what the track function does

export function track(target: object.type: TrackOpTypes, key: unknown) {
  if(! shouldTrack || activeEffect ===undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  // Check whether it has been collected, if not, collect it
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key)
  // Check whether the key has been collected
  if(! dep) { depsMap.set(key, (dep =new Set()))}// Plug activeEffect into map
  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

This function does two things

  1. Stuff the activeEffect into the map
  2. Trigger onTrack

In summary, the track function is a mapping process, but we have a question: what is this activeEffect? This activeEffect is created by createReactiveEffect. The function that calls createReactiveEffect is effect. This effect is created in three places

  1. The trigger function invokes effect via schedulerRun
  2. MountComponent calls effect with setupRenderEffect
  3. DoWatch invokes effect by calling scheduler

Here we should look at the trigger function, which is the core function

export function trigger(
  target: object.type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  // No collection, nothing to do
  if(! depsMap) {return
  }
  / / to heavy
  const effects = new Set<ReactiveEffect>()
  
  // ...

  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()
    }
  }
  // Execute the function
  effects.forEach(run)
}
Copy the code

This function, in summary, just does one thing, executes the function. So we can also know that Active effects are methods to perform. Where is the trigger function called, in the set method of the proxy

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ) :boolean {
    // Get the original value
    const oldValue = (target as any)[key]
    if(! shallow) { value = toRaw(value)// If the original value is ref and the new value is not, change the value of the ref object directly
      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
    }
    // See if the original object has the new value key
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // Reflect gets the original set behavior
    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)) {
    // Add the property without the key, otherwise modify the value of the original property
    // trigger the function
      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

It’s kind of amazing, we’re still looking at the get method, and we’re looking at the set method, and we’re forming a closed loop. The set function does four things:

  1. Get the original oldValue first
  2. If the original value is a ref object and the new assignment is not a REF object, modify the value attribute of the ref wrapper object directly
  3. And then Reflect gets the original set behavior, if the original object has the new assigned key, then it adds the property, otherwise it assigns the original property
  4. The corresponding modification and attribute addition operations are performed, and the dePS is notified of the update by calling trigger, and the objects that depend on the state are notified of the update.

So to this source part is finished, as the most core part of vUE, it does not initialize the process as around, the idea is very clear, but you can also see that the logic is careful, and there are a lot of processing about performance optimization. But also learned the WeakMap type did not contact before, after writing VUe3 will certainly be more handy.

Handwriting simple response type

Read the source code, of course, is not enough, we still have to write a simple response to consolidate the knowledge of learning. So the first step is to write a reactive function

const reactive = (obj) = > {
    return new Proxy(obj, {
        get(target, key) {
            // Read interception
            const res = Reflect.get(target, key)
            console.log('get')
            return res
        },
        set(target, key, value) {
            // Write interception
            const res = Reflect.set(target, key, value)
            console.log('set')
            returnres; }})}Copy the code

This will allow you to detect property reads and writes in an object, but not in nested objects, so you have to recurse.

const isObject = value= > typeof value === 'object'

const reactive = (obj) = > {
    // Enter an object
    if(! isObject(obj)) {return
    }
    return new Proxy(obj, {
        get(target, key) {
            // Read interception
            const res = Reflect.get(target, key)
            console.log('get')
            return isObject(res) ? reactive(res) : res
        },
        set(target, key, value) {
            // Write interception
            const res = Reflect.set(target, key, value)
            console.log('set')
            returnres; }})}Copy the code

Check whether the value passed in is an object, then check whether the value read in the return of get is an object, and if so, call reactive. In this way, nested object responses are implemented. The next step is to rely on the collection functionality

const effect = (fn) = > {
    const e = createReactiveEffect(fn)

    // Trigger the dependent track procedure
    e()

    return e
}

const createReactiveEffect = (fn) = > {
    const effect = function reactiveEffect() {
        // Catch an error
        try{
            // Push operation
            effectStack.push(effect)
            return fn()
        }finally{
            effectStack.pop()
        }
    }

    return effect
}
Copy the code

A higher-order function is created and stored in the effectStack to catch errors. Next, implement the track function

// WeakMap saves the mapping
const targetMap = new WeakMap(a)const track = (target, key) = > {
    const effect = effectStack[effectStack.length - 1]
    if(effect) {
        // Get the target mapping map
        const depMap = targetMap.get(target)
        if(! depMap) { depMap =new Map()
            targetMap.set(target, depMap)
        }
        // get the set for key
        const deps = depMap.get(key)
        if(! deps) { deps =new Set()
            depMap.set(key, deps)
        }
        // Put the currently active reactive function into the deps
        deps.add(effect)
    }
}
Copy the code

WeakMap{Map{Set}} This function is very simple, is to collect the current activity of the corresponding function, if not to create a structure, structure WeakMap{Map{Set}}. This function is executed in the proxy get. Finally, we need to implement the trigger function, which is the reactive function that triggers your collection

// Trigger function
const trigger = (target, key) = > {
    const depMap = targetMap.get(target)
    if(! depMap) {return 
    }

    const deps = depMap.get(key)
    if(deps) {
        deps.forEach(dep= > dep())
    }
}
Copy the code

This function is not explained; it is executed in the proxy set. Here, a simple response is finished, we certainly have to write a page to test the page source code in this, is to click the button on the page count number increase effect

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Initial - scale = 1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <button id="button">count++</button>
    <script>
        const isObject = value= > typeof value === 'object'

        const reactive = (obj) = > {
            // Enter an object
            if(! isObject(obj)) {return
            }
            return new Proxy(obj, {
                get(target, key) {
                    // Read interception
                    const res = Reflect.get(target, key)
                    // Rely on collection
                    track(target, key)
                    return isObject(res) ? reactive(res) : res
                },
                set(target, key, value) {
                    // Write interception
                    const res = Reflect.set(target, key, value)
                    trigger(target, key)
                    returnres; }})}// Temporarily save dependent functions
        const effectStack = []

        const effect = (fn) = > {
            const e = createReactiveEffect(fn)

            // Trigger the dependent track procedure
            e()

            return e
        }

        const createReactiveEffect = (fn) = > {
            const effect = function reactiveEffect() {
                // Catch an error
                try{
                    // Push operation
                    effectStack.push(effect)
                    return fn()
                }finally{
                    effectStack.pop()
                }
            }

            return effect
        }

        // WeakMap saves the mapping
        const targetMap = new WeakMap(a)// Trace function
        const track = (target, key) = > {
            const effect = effectStack[effectStack.length - 1]
            if(effect) {
                // Get the target mapping map
                let depMap = targetMap.get(target)
                if(! depMap) { depMap =new Map()
                    targetMap.set(target, depMap)
                }
                // get the set for key
                let deps = depMap.get(key)
                if(! deps) { deps =new Set()
                    depMap.set(key, deps)
                }
                // Put the currently active reactive function into the deps
                deps.add(effect)
            }
        }

        // Trigger function
        const trigger = (target, key) = > {
            const depMap = targetMap.get(target)
            if(! depMap) {return 
            }

            const deps = depMap.get(key)
            if(deps) {
                deps.forEach(dep= > dep())
            }
        }
    </script>
    <script>
        const state = reactive({
            count: 0
        })
        effect(() = > {
            const app = document.querySelector('#app')
            app.innerHTML = state.count
        })
        document.querySelector('#button').onclick = () = > {
            console.log(11)
            state.count++
        }
    </script>
</body>
</html>
Copy the code

The effect is present!

conclusion

Compared to Vue 2.x, Vue 3.x fully embraces the Proxy API for responsiveness, which is implemented by Proxy initial object default behavior; Reactive makes use of weak-reference properties and fast indexing of WeakMap, and uses WeakMap to preserve responsive proxy and original object. Readonly proxy and original object map each other. For object and array types, the original object responsiveness is realized through the relevant trap method of responsive Proxy, while for Set, Map, WeakMap, WeakSet types, due to the limitation of Proxy, Vue 3.x implements the reactive principle by hijacking get, HAS, add, set, Delete, clear, forEach and other method calls. More strange knowledge!

My name is WZK-Developer, welcome to exchange front-end knowledge with me, my email is [email protected]; Making: WZK – zjut