Vue talk about two-way binding

Bidirectional binding (responsive)

Understand how to implement data responsive system in Vue, so as to achieve data-driven view.

1, Data driven view?

In a bold sentence, the view depends on two changes of data, namely:

UI = render(state)

  • State: enter data
  • UI (page): output view
  • Render (driver): played by Vue. When Vue finds the change of state, after a series of processing, it finally reflects the change on the UI

So the first question is, how does Vue know that the state has changed?

2, Data detection (Vue 2.x)

From the perspective of God, we know that the whole two-way binding is realized by publishing and subscribing + data broker (hijacking), that is:

Object. The defineproperty () method will directly define a new property on an object, or modify an existing property of an object and return the object.

2.1 understand object defineProperty

Syntax:

Object.defineProperty(obj, prop, descriptor)

Parameter Description:

  • obj: required. Target object
  • prop: required. The name of the attribute to be defined or modified
  • descriptor: required. Characteristics of the target attribute

At the same time, it provides get and set methods to facilitate us to view the "operation log" of this property

Basic usage:

let a = {};
let val = null
Object.defineProperty(a, 'name', {
    enumerable: true,
    configurable: true,
    get() {
        console.log('a of name Property was obtained:', val);
        return val;
    },
    set(newVal) {
        console.log('a of name Property has been modified:', newVal);
        val = newVal;
    }
})
console.log('a:', a.name);
a.name = 100

Through such means, we can:

  1. Know that the specified attribute is obtained
  2. Know that the specified attribute is updated

We can do the simplest two-way binding.

2.2 A Minimalist two-way binding

Objectives:

Input input box input content, synchronously updated to the p tag

Idea:

  1. Monitoring: define obj variable and specify "hijack" obj Text attribute
  2. Update: input monitors keyboard events and modifies obj in real time when entering content Text properties
  3. Notification: trigger the attribute set method to notify the p tag to update the content

realization:

<input type="text" id="input">
<p id="text"></p>

<script>
    const oInput = document.getElementById('input');
    const oText = document.getElementById('text');
    var obj = {}; // Define variables
    // monitor
    Object.defineProperty(obj, 'text', {
        get(e){
            console.log(e)
        },
        set(newValue){
            // notice
            oText.innerHTML = newValue;
        }
    })
    // to update
    oInput.onkeyup = function(e){
        obj.text = e.target.value;
    }
</script>

Illustration:

After reading the above demo, you should have a lot of ideas. There are many inconveniences, such as knowing which attribute to hijack and which object to notify in advance. We will continue to strengthen this demo in the next knot.

3, Basic implementation based on data hijacking

After a brief understanding, we will soon find that the so-called two-way binding in 2.2 seems to have no practical value. At least we need to apply publish and subscribe.

3.1 implementation ideas

  1. Using Proxy or object The Observer generated by defineproperty "hijacks" the properties of the object / object and notifies the subscriber when the properties change
  2. The parser Compile parses the directive in the template, collects the methods and data that the instruction depends on, waits for the data to change, and then renders
  3. Watcher belongs to the bridge between Observer and Compile. It will receive the data changes generated by Observer and render the view according to the instructions provided by Compile, so that the data changes promote the view changes

solve the problem:

  1. Observer: flexible hijacking (proxy) data changes
  2. Dep: dependency manager, which is responsible for collecting all dependencies on data (subscribers) and notifying all subscribers that the data has changed at a specific time
  3. Watcher: subscriber, responsible for accepting changes and updating views

So we have the following figure:

3.2 dependency Manager - Dep

Therefore, we need to create a dependency manager, which is responsible for:

  1. Collection dependency: whoever relies on this data will collect it, that is, when Getter
  2. Notification update: when the data changes, start to notify the dependent, that is, when the Setter changes

To sum up, collect dependencies in getter s and notify dependency updates in setter s.

Therefore, we set up a dependency manager for each to manage all dependencies of this data:

// Dependency Manager
let uid = 0;
class Dep {
    constructor() {
        this.id = uid++; // Distinguish as identification
        this.subs = []; // Whoever relies on this data will be saved for notification
    }
    // Add dependent method
    addSub(sub) {
        this.subs.push(sub)
    }
    // Trigger add
    depend() {
        // Why do you do this? Let's introduce in the next summary
        // For the time being, our method of obtaining subscribers
        if(Dep.target){
            Dep.target.addDep(this) // Add dependency Manager
        }
    }
    // Notify all dependent updates
    notify() {
        const subs = this.subs.slice();
        // Here we trigger the update
        subs.forEach(sub => sub.update())
    }
}
// Set a static property for Dep class, which is null by default and points to the current Watcher when working
// Here I see the use of window Target to save the temporary watcher, is it both or there is another mystery? I don't know if there is a big man to solve the puzzle
Dep.target = null; // Initialize to null

3.3 listener - Observer

Here we hope to hijack the changes of data and notify the dependency manager

// Listening class
class Observer {
    constructor(value) {
        this.value = value;
        if (Array.isArray(value)) {

        } else {
            this.walk(value)
        }
    }
    walk(value) {
        const keys = Object.keys(value);
        keys.forEach(key => {
            this.convert(key, value[key])
        });

    }
    convert(key, val) {
        defineReactive(this.value, key, val)
    }
}

function defineReactive(obj, key, val) {
    const dep = new Dep();
    // Add listeners to child elements
    let chlidOb = observe(val);
    Object.defineProperty(obj, key, {
        get() {
            // If the dep class has a target attribute, add it to the sub array of the dep instance
            // target points to a Watcher instance, and each Watcher is a subscriber
            // During the instantiation process of Watcher instance, it will read a property in data and trigger the current get method
            if (Dep.darget) {
                dep.depend();
            }
            console.log('obtain')
            return val;
        },
        set(newValue) {
            if (val === newValue) {
                return;
            }
            console.log('modify')
            val = newValue;
            chlidOb = observe(newValue);
            // Notify all subscribers of updates through the dependency manager
            dep.notify();
        }
    })
}
// Add listening
function observe(value) {
    // When the value does not exist or is not a complex data type, it is no longer necessary to continue deep listening
    if (!value || typeof value !== 'object') {
        return;
    }
    return new Observer(value);
}

You can add the following test code to view the console effect

const obj = new Observer({
    name: 'Yu Guang',
    age: '24'
})
obj.value.name // The name attribute has been read
obj.value.name = 100; // The name attribute has been modified

3.4 subscriber - watcher

Examples of Watcher class are:

  1. We create a Watcher instance for those who use the data and depend on them;
  2. When the data changes later, we do not directly notify the dependent update, but notify the corresponding Watch instance of the dependency;
  3. Then the Watcher instance notifies the real view.
// Implement a subscriber, the "dependency"
class Watcher {
    constructor(vm, expOrFn, cb) {
        this.depIds = {}; // hash stores the id of the subscriber to avoid duplicate subscribers
        this.vm = vm; // The subscribed data must come from the current Vue instance
        this.cb = cb; // What you want to do when data is updated
        this.expOrFn = expOrFn; // Subscribed data, data key 𞓜 path
        this.val = this.get(); // Maintain data before update
    }
    // The exposed interface is used to be called by the subscriber administrator (Dep) when the subscribed data is updated
    update() {
        this.run();
    }
    addDep(dep) {
        // If there is no current id in the hash of depIds, it can be judged that it is a new Watcher, so it can be added to the dep array for storage
        // This judgment is to avoid the Watcher with the same id being stored multiple times
        if (!this.depIds.hasOwnProperty(dep.id)) {
            dep.addSub(this);
            this.depIds[dep.id] = dep;
        }
    }
    run() {
        // Execute the get method once
        const val = this.get();
        console.log(val);
        if (val !== this.val) {
            this.val = val;
            //
            this.cb.call(this.vm, val);
        }
    }
    get() {
        // When the current subscriber (Watcher) reads the latest updated value of the subscribed data, it notifies the subscriber administrator to collect the current subscriber
        Dep.target = this; // Assign to Dep.target
        // This code needs to be combined with context, which means to actively trigger get and add the subscriber to the dependency manager
        const val = this.vm._data[this.expOrFn]; // Active trigger
        // Empty for the next Watcher
        Dep.target = null;
        return val;
    }
}

analysis

When instantiating Watcher class:

  1. Its constructor will be executed first:
  2. The this. is called in the constructor. Get() instance method;
    • 2.1 first, Dep.target = this (assign itself to a globally unique object Dep.target);
    • 2.2 then through this vm._ Data [this. Exporfn] get the dependent data. The purpose of getting the dependent data is to trigger the getter above the data. As we said above, dep.dependent() will be called in the getter to collect the dependency, and the mounted window will be obtained in dep.dependent() The value on target and store it in the dependency array;
    • 2.3 release Dep.target at the end of get() method.
  3. When the data changes, it triggers the setter of the data and calls the dep.notify() method in setter. In the dep.notify() method, it traverses all the dependency (i.e. watcher instances) and executes the dependent update() method, that is, update() in the Watcher class, and calls the updated callback function of the data in the update() method to update the view.

3.5 mount to Vue

class Vue {
    constructor(options = {}) {
        // Simplified handling of $options
        this.$options = options;
        // Simplify the processing of data
        let data = this._data = this.$options.data;
        // Proxy all data outermost attributes to Vue instances
        Object.keys(data).forEach(key => this._proxy(key));
        // Monitor data
        observe(data);
    }
    // The external exposure calls the interface of the subscriber, and the internal use of the subscriber is mainly in the instruction
    $watch(expOrFn, cb) {
        new Watcher(this, expOrFn, cb);
    }
    _proxy(key) {
        Object.defineProperty(this, key, {
            configurable: true,
            enumerable: true,
            get: () => this._data[key], // Take options data
            set: val => {
                this._data[key] = val;
            },
        });
    }
}

// Test code
let demo = new Vue({
    data: {
        text: '',
    },
});

const p = document.getElementById('p');
const input = document.getElementById('input');

input.addEventListener('keyup', function(e) {
  demo.text = e.target.value;
});

demo.$watch('text', str => p.innerHTML = str);

So far, we have disassembled part of the code to see its actual effect?

See the Pen RwKJrbq by Jiang Bojian( @webbj97) on CodePen.

Review the following figure again:

3.5 defineProperty with a long way to go

Object. The first defect of defineproperty is that it cannot listen for array changes.

However, Vue's document mentioned that Vue can detect array changes, but there are only the following eight methods, VM This kind of [indexvalue = items] cannot be detected.

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

In fact, the author used some tricks here to hack out the case that the array cannot be monitored. The following is a method example.

const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = [];

aryMethods.forEach((method)=> {
    // Here is the prototype method of the native Array
    let original = Array.prototype[method];

   // Define the encapsulated methods such as push and pop on the attribute of the object arrayaugmentation
   // Note: This is a property, not a prototype property
    arrayAugmentations[method] = function () {
        console.log('I've been changed!');

        // Call the corresponding native method and return the result
        return original.apply(this, arguments);
    };

});

let list = ['a', 'b', 'c'];
// Point the prototype pointer of the array we want to listen to to to the empty array object defined above
// Don't forget that the attributes of this empty array define our encapsulated push and other methods
list.__proto__ = arrayAugmentations;
list.push('d');  // I've been changed! four

// list2 here is not redefined as the prototype pointer, so it is output normally
let list2 = ['a', 'b', 'c'];
list2.push('d');  // 4

We should note that in the above implementation, we use the traversal method to traverse the properties of the object many times, which leads to object The second defect of defineproperty is that it can only hijack the properties of objects. Therefore, we need to traverse each property of each object. If the property value is also an object, it needs deep traversal. Obviously, it is a better choice to hijack a complete object.

Object.keys(value).forEach(key => this.convert(key, value[key]));

4, Data detection (Vue 3.x)

Proxy can intercept the reading of the target object, function call and other operations, and then conduct operation processing. It does not directly operate the object, but like the proxy mode, it operates through the proxy object of the object. When these operations are carried out, some additional operations can be added.

Briefly introduce why proxy can replace property:

Pre knowledge:

  • proxy also has set and get methods to code the specified data
    *The method of the Reflect object corresponds to the method of the Proxy object one by one. As long as it is the method of the Proxy object, you can find the corresponding method on the Reflect object.

4.1 reconstruction of minimalist bidirectional binding

We still use object Take the minimalist bidirectional binding implemented by defineproperty as an example, and rewrite it with Proxy.

const input = document.getElementById('input');
const p = document.getElementById('p');
const obj = {};

const newObj = new Proxy(obj, {
    get: function(target, key, receiver) {
        console.log(`getting ${key}!`);
        return Reflect.get(target, key, receiver);
    },
    set: function(target, key, value, receiver) {
        console.log(target, key, value, receiver);
        if (key === 'text') {
            input.value = value;
            p.innerHTML = value;
        }
        return Reflect.set(target, key, value, receiver);
    },
});

input.addEventListener('keyup', function(e) {
    newObj.text = e.target.value;
});

We can see that Proxy can directly hijack the whole object and return a new object, which is much better than object in terms of operation convenience and underlying functions defineProperty.

4.2 advantages of proxy

Proxy has up to 13 interception methods, not limited to apply, ownKeys, deleteProperty, has, etc. it is object Defineproperty does not have.

Proxy returns a new object. We can only operate on the new object to achieve the purpose, but object Defineproperty can only traverse object properties and modify them directly.

As a new standard, Proxy will be continuously optimized by browser manufacturers, that is, the performance bonus of the legendary new standard.

Of course, the disadvantage of Proxy is the compatibility problem, and it can't be polished with polyfill. Therefore, the author of Vue declared that it can't be rewritten with Proxy until the next major version (3.0).

Write at the end

reference resources:

Interviewer: what are the advantages and disadvantages of implementing two-way binding Proxy over defineproperty?

Vue's article is the first one in my series, but I hope it's my chance to write more than one word, but I don't know it

JavaScript series:

  1. "JavaScript internal skill Advanced Series" (finished)
  2. JavaScript special series (continuously updated)
  3. ES6 basic series (continuously updated)

About me

  • Flower name: Yu Guang (addicted to JS and learning with an open mind)
  • WX: j565017805

Other precipitation

If you see the end and have any suggestions for the article, you can leave a message in the comment area

This is Portal of GitHub warehouse where the article is located , if it really helps you, I hope you can click star, which is my greatest encouragement ~

Keywords: Vue source code

Added by impfut on Sat, 05 Mar 2022 02:59:03 +0200