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:
- Know that the specified attribute is obtained
- 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:
- Monitoring: define obj variable and specify "hijack" obj Text attribute
- Update: input monitors keyboard events and modifies obj in real time when entering content Text properties
- 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
- Using Proxy or object The Observer generated by defineproperty "hijacks" the properties of the object / object and notifies the subscriber when the properties change
- 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
- 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:
- Observer: flexible hijacking (proxy) data changes
- 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
- 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:
- Collection dependency: whoever relies on this data will collect it, that is, when Getter
- 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:
- We create a Watcher instance for those who use the data and depend on them;
- When the data changes later, we do not directly notify the dependent update, but notify the corresponding Watch instance of the dependency;
- 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:
- Its constructor will be executed first:
- 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.
- 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:
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:
- "JavaScript internal skill Advanced Series" (finished)
- JavaScript special series (continuously updated)
- 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 ~