The vue2. X -Object Change Monitor has basically introduced how vUE can be responsive or data-driven in ve2. X

A method to listen for changes in data

From the previous analysis we used object.defineproperty to listen for read and write Object attributes in vue2.0, but it had some limitations that were hard to overcome:

  1. Cannot listen for object attributes to be added or deleted
  2. All properties of the object need to be traversed to set, and the performance of objects with deeper nesting levels is poor
  3. There is a bug in listening on arrays

Based on the above knowledge, we need to find some better solutions. Just as the new js feature Proxy appeared in our vision, it can solve the defects of Object.defineProperty in this scenario very well

var obj = {name: Var person = new Proxy(obj, {get(target, key) {console.log(' read ') return reflect.get (target, key)}, Set (target, key, value) {console.log(' update ') return reflect. set(target, key, value)}, deleteProperty(target, Key) {console.log(' delete ') return reflect.deleteProperty (target, key)}, defineProperty(target, key, defineProperty) Descriptor) {console.log(' set ') return reflect.defineProperty (target, key, descriptor) {console.log(' set ') return reflect.defineProperty (target, key, descriptor) Descriptor)}}) person.name // 'read' person.name // 'update' delete.name // 'delete' object.defineProperty (person, 'age', 18) // 'set' person.tall = 185 // 'update'Copy the code

The above examples are just the tip of the iceberg of what Proxy provides, and you can Google more, but knowing the above is enough to understand the responsive/data-driven refactoring of VUe3.0

Responsive data

In order to realize data change-driven view update, it must be able to listen for data changes first. Compared with 2.x version, VUe3.0 has a big change in the aspect of responsive data. 2.x version can only listen for data of reference type, but this aspect has been improved in 3.0 version

Reactive reference data types

Const Handlers = {get(target, key, receiver) {console.log(' read ')}, Set (target, key, value) {console.log(' write ')}, has(target, key) {console.log(' find ')}, deleteProperty(target, Key) {console.log(' delete ')},} const proxy = new proxy (target, handlers) return proxy } function reactive (target) { return createReactiveObject(target) }Copy the code

Reactive base data types

function isRef(r) { return r && r.__v_isRef === true } function ref(value) { return createRef(value) } class RefImpl { Constructor (rawValue) {this._value = rawValue // initialize this.__v_isRef = true} get value() { Console. log('ref reads ')} set value(newValue) {console.log('ref writes ')}} function createRef(rawValue) { If (isRef(rawValue) {return rawValue} return new RefImpl(rawValue)}Copy the code

So what we’ve done here is we’ve implemented a Proxy that’s responsive to a reference data type, and an extension that’s responsive to a base data type and we’ve actually created an instance of an object based on the change, and that instance contains get value and set value, To use this variable, you need to access its value property

Depend on the collection

In the same way as the 2.x version, we preliminarily realized the reading and writing monitoring of data. When the subscriber reads the data, the dependency between the subscriber and the data should be recorded so as to realize the notification to the subscriber for updating after the data changes

Const targetMap = new WeakMap() // Save all dependencies function track(target, type, key) {if (! activeEffect) 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) } }Copy the code

The dependency collection implemented in version 3.0 actually globally extends the variable targetMap of WeakMap type to store all the subscriber – dependency correspondence as follows:

targetMap = {
    target: {
        key: [effect]
    }
}
Copy the code

Target represents the response data. Key represents the key effect of the response data. It indicates that the subscriber will trigger Proxy GET after the data is referenced, and track will be executed in GET for dependent collection

Change notification

Now that the data response is implemented, and the dependency collection triggered by the reference to the data establishes a link between the subscriber and the dependency, it is time to implement notification to the subscriber for updates when the data changes

function trigger(target, type, key, newValue) { const effects = new Set() const depsMap = targetMap.get(target) if (! depsMap) return function add(effectsToAdd) { effectsToAdd.forEach(effect => { if (effect ! == activeEffect) { effects.add(effect) } }) } if (! depsMap.get(key)) return add(depsMap.get(key)) function run(effect) { effect() } effects.forEach(run) }Copy the code

Find the subscribers corresponding to the data and notify execution one by one

The subscriber

Portrait of subscriber:

  1. Subscribers to includedepsList to hold dependencies
  2. Subscribers are required to send theactiveEffectMake it your own so that you can rely on the collection
let uid = 0 function cleanup(effect) { const deps = effect.deps for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 } function createReactiveEffect(fn) { const effect = function ReactiveEffect () {activeEffect = effect cleanup(effect) // cleanup all dependencies before each subscriber method run, Try {fn() // Subscriber method references data, triggering the getter for the referenced data} finally {activeEffect = null  } effect.id = uid++ effect.deps = [] return effect }Copy the code

Refer to code comments for details

The complete code

Let’s summarize the process before putting in the whole code

  1. useProxyReactive data, and secondary objects are created if the data is of an underlying data typeRefImpl, and then access its value property
  2. Based on the subscriber methodfncreateeffectFunction, in which the subscriber methodfnThe subscriber’s dependencies will be cleaned up before execution, and theactiveEffectSet to the currenteffectFunction to wait for dependency collection
  3. effectThe function executes, the subscriber references the data, triggers the getter for the data, and executestraceFunction, in which dependency collection is performedEstablish a relationship graph between dependent data and subscribersSpecific seetraceimplementation
  4. When the data is reassigned and the change is triggered, the setter is executedtriggerFunction notifies all subscribers corresponding to the data to be updated
  5. Repeat Step 3 for dependency updates for subscribers and dependencies

The complete code is as follows:

window.activeEffect = null function createReactiveObject(target) { const handlers = { get(target, key, Receiver) {console.log(' read, ') track(target, 'get', key) return reflect. get(target, key)}, set(target, key, value) {reflect. set(target, key, value) {reflect. set(target, key, value) Log (' write, Trigger (target, 'set', key, value)},} const proxy = new proxy (target, handlers) return proxy; } function reactive (target) {return createReactiveObject(target)} function isRef(r) {return r && r.__v_isRef === true } function ref(value) { return createRef(value) } class RefImpl { constructor(rawValue) { __v_isRef = true} get value() {console.log('ref reads ') track(this, 'get', 'value') return this._value} set value(newValue) {this._value = newValue console.log('ref writes ') trigger(this, 'set', 'value')}} function createRef(rawValue) { If (isRef(rawValue)) {return rawValue} return new RefImpl(rawValue)} const targetMap = new WeakMap() // Function track(target, type, key) {if (! activeEffect) 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)}} // Change notification function trigger(target, type, key, newValue) { const effects = new Set() const depsMap = targetMap.get(target) if (! depsMap) return function add(effectsToAdd) { effectsToAdd.forEach(effect => { if (effect ! == activeEffect) { effects.add(effect) } }) } if (! Depmap.get (key)) return add(depmap.get (key)) function run(effect) {effect()} effects.foreach (run)  0 function cleanup(effect) { const deps = effect.deps for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 } function createReactiveEffect(fn) { const effect = function ReactiveEffect () {activeEffect = effect cleanup(effect) // cleanup all dependencies before each subscriber method run, Try {fn() // Subscriber method references data, triggering the getter for the referenced data} finally {activeEffect = null  } effect.id = uid++ effect.deps = [] return effect }Copy the code

Test it separately:

Var obj = {name: 'jack'} var person = reactive(obj) function fn() { console.log('fn called with ' + person.name) } var consoleEffect = Name = 'Tom' var count = ref(1) var fn1 = function () { console.log('fn1 called with ' + count.value) } var countEffect = createReactiveEffect(fn1) countEffect() // Change the value of count count. Value = 2Copy the code

Summary & Reflection

The entire data-driven implementation process of VUe3.0 is shown in the beginning of the previous section and the problems left over from the previous notes:

  1. How to solve the problem of the subscriber updating multiple times in a round of changes where multiple attributes of the subscriber’s dependency are changed or one attribute of the dependency is changed multiple times
  2. What would the dependency collection process look like if you nested another subscriber in the getter function of one subscriber

This is not resolved in this article, but we will focus on addressing these issues later in this article, and there are some subtle differences between 3.0 and 2.x that are worth noting:

  1. 2. In X, data (dependents) needs to maintain subscriber information, but data itself (attributes) does not have the ability to store data, so it is introducedDepSimilar data attribute one-to-one correspondence is used to maintain subscriber information; In 3.0, however, global is usedtargetMapWeakmap object manages the relationship between all data (dependents) and subscribers uniformly.targetMapWith the proxy of the data as the key and with the value of the object with all the attributes of the data as the key, the subscriber is stored in the Set data structure in each attribute of this object; Half a day of description is worth a few lines of code:
targetMap = {
    target/*proxy*/: {
        key: [effect]
    }
}
Copy the code
  1. In versions 2.x and 3.0, the situation of avoiding the circular trigger notification to subscribers during the dependency update process is handled differently. In 2.0, the concepts of new generation dependency and old generation dependency are introduced. The old generation dependency is not modified when the update of subscriber method dependency is implemented to avoid the occurrence of cycle triggering change notification to subscribers. After the completion of the update of subscriber method dependency, the new generation dependency is changed to old generation dependency. In 3.0, the list of subscribers to be notified is first determined before the subscriber update is performed in the change notification method and then notified one by one according to this list, while the subscriber effect method is executed first before the subscriber fn methodcleanupClear all dependencies for this subscriber and execute the subscriber method fn to update the dependencies

Subsequent articles will focus on the two remaining issues from the recent articles, which will lead to subscriber updating strategies after vUE change notifications