After learning about accessor properties in the previous article, object.difineProperty () is the key to implementing two-way data binding, since it is often used for data hijacking. So let’s move on to reactive systems.
1. Object.difineproperty () implements a simple two-way data binding
<input id="input" />
<span id="span"></sapn>
Copy the code
const obj = {}
Object.defineProperty(obj, 'text', {
get: function() {
console.log('get val'); },set: function(newVal) {
console.log('set val:' + newVal);
document.getElementById('input').value = newVal;
document.getElementById('span').innerHTML = newVal; }})const input = document.getElementById('input');
input.addEventListener('keyup'.function(e){
obj.text = e.target.value;
})
Copy the code
1.1 Problems with the above large pile of code
Similarly, after you enter content in the input, the span tag that needs that content will be displayed in response. But this so-called bidirectional binding doesn’t seem to work…
1.2 Reasons:
-
- We only listen for one property, an object can’t just have one property, we need to listen for every property of the object.
-
- In violation of the open and closed principle, we need to enter the method every time we modify, which needs to be resolutely put an end to.
- The code is heavily coupled; our data, methods, and DOM are all coupled together, the legendary noodle code.
1.3 How to solve
The operation of Vue is to add publish-subscribe mode and data hijacking with Object.defineProperty to achieve high availability of bidirectional binding.
First of all, let’s look at the publish-subscribe perspective. In the first part of the above, we wrote a big chunk of code, and found that its listening, publishing, and subscribing are all written together. The first thing we need to do is decouple
1.3.1 Decoupling — Message Manager (Dep)
We’ll start with a subscription publishing center, the Message Manager (Dep), which stores subscribers and distributes messages that both subscribers and publishers depend on.
The JOB of the Dep is simply to collect the Watcher everywhere that depends on the current data
let uid = 0;
// Used to store subscribers and publish messages
class Dep {
constructor() {
// Set the id to distinguish between a new Watcher and a new Watcher created by changing the property value only
this.id = uid++;
// Store an array of subscribers
this.subs = [];
}
// Triggers the addDep method in Watcher on target, taking the instance of deP itself
depend() {
Dep.target.addDep(this);
}
// Add subscribers
addSub(sub) {
this.subs.push(sub);
}
notify() {
// Notify all the subscribers (Watcher) and trigger the corresponding logic processing of the subscribers
this.subs.forEach(sub= >sub.update()); }}// Set a static property for the Dep class, null by default, pointing to the current Watcher at work
Dep.target = null;
Copy the code
1.3.2 Special listener of data attribute changes — Observer
// listener to listen for changes in object property values
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
// Iterate over property values and listen
walk(value) {
Object.keys(value).forEach(key= > this.convert(key, value[key]));
}
// Execute the specific method of listening
convert(key, val) {
defineReactive(this.value, key, val); }}function defineReactive(obj, key, val) {
const dep = new Dep();
// Add a listener to the value of the current attribute
let chlidOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: () = > {
// If the Dep class has a target attribute, add it to the subs array of the Dep instance
// Target points to an instance of Watcher. Each Watcher is a subscriber
// The Watcher instance reads an attribute in data during its instantiation, which triggers the current GET method
if (Dep.target) {
dep.depend();
}
return val;
},
set: newVal= > {
if (val === newVal) return;
val = newVal;
// Listen for the new value
chlidOb = observe(newVal);
// Notify all subscribers that the value has been changeddep.notify(); }}); }function observe(value) {
// If the value does not exist or is not a complex data type, no further listening is required
if(! value ||typeofvalue ! = ='object') {
return;
}
return new Observer(value);
}
Copy the code
1.3.3 Subscriber Watcher
The Watcher creation process is accompanied by Vue rendering, which reads data from data.
class Watcher {
constructor(vm, expOrFn, cb) {
this.depIds = {}; // Hash stores the subscriber ID to avoid duplicate subscribers
this.vm = vm; // The subscribed data must be from the current Vue instance
this.cb = cb; // What you want to do when data is updated
this.expOrFn = expOrFn; // The subscribed data
this.val = this.get(); // Maintain the data before update
}
// The exposed interface is invoked by the subscriber administrator (Dep) when the subscribed data is updated
update() {
this.run();
}
addDep(dep) {
// If the depIds hash does not have the current ID, it is a new Watcher, so it can be added to the deP array
// This judgment is to avoid the same id Watcher is stored more than once
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depIds[dep.id] = dep; }}run() {
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, the subscriber administrator is notified to collect the current subscriber
Dep.target = this;
const val = this.vm._data[this.expOrFn];
// empty for the next Watcher
Dep.target = null;
returnval; }}Copy the code
1.3.4 Mounting a Vm to a Vue
class Vue {
constructor(options = {}) {
// Simplifies handling of $options
this.$options = options;
// Simplify the processing of data
let data = (this._data = this.$options.data);
// Proxy all data outermost properties to Vue instances
Object.keys(data).forEach(key= > this._proxy(key));
// Listen for data
observe(data);
}
// Externally exposes the interface that invokes the subscriber, and internally uses the subscriber mainly in directives
$watch(expOrFn, cb) {
new Watcher(this, expOrFn, cb);
}
_proxy(key) {
Object.defineProperty(this, key, {
configurable: true.enumerable: true.get: () = > this._data[key],
set: val= > {
this._data[key] = val; }}); }}Copy the code
The final result
Object. DifineProperty defects
- 1. Adding new attributes to hijacked objects is undetectableVue is initialized to the data in data and props
Getter/setter
That is, the Observer is called at initialization. Vue does not allow dynamic root-level responsive properties to be added to already created instances. However, you can add responsive properties to nested objects using the vue.set (Object, propertyName, value) method. For example, for:
Vue.set(vm.someObject, 'b'.2)
Copy the code
You can also use the vm.$set instance method, which is also an alias for the global vue.set method:
this.$set(this.someObject,'b'.2)
Copy the code
Sometimes you may need to assign multiple new properties to existing objects, such as object.assign () or _.extend(). However, new properties that are thus added to the object do not trigger updates. In this case, you should create a new object with the property of the original object and the property of the object to be mixed in.
// Replace 'object. assign(this.someObject, {a: 1, b: 2})'
this.someObject = Object.assign({}, this.someObject, { a: 1.b: 2 })
Copy the code
- 2. Object.defineproperty’s second defect is that it cannot listen for array changes
Example:
let demo = new Vue({
data: {
list: [1],}});const list = document.getElementById('list');
const btn = document.getElementById('btn');
btn.addEventListener('click'.function() {
demo.list.push(1);
});
const render = arr= > {
const fragment = document.createDocumentFragment();
for (let i = 0; i < arr.length; i++) {
const li = document.createElement('li');
li.textContent = arr[i];
fragment.appendChild(li);
}
list.appendChild(fragment);
};
// Listen to the array, each change in the array triggers the render function, however... Can't listen
demo.$watch('list'.list= > render(list));
setTimeout(
function() {
alert(demo.list);
},
5000,);Copy the code
However, the Vue documentation mentions that array changes can be detected, with only the following seven
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
Copy the code
The idea is that when we call the seven methods of the array, Vue will modify the methods. It will also execute the logic of the methods internally, but with some additional logic: take the increased value, make it reactive, and then manually start dep.notify()
const aryMethods = ['push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'];
const arrayAugmentations = [];
aryMethods.forEach((method) = > {
// Here is the prototype method for the native Array
let original = Array.prototype[method];
// Encapsulate methods such as push and pop on properties of arrayAugmentations
// Note: attributes, not stereotype attributes
arrayAugmentations[method] = function () {
console.log('I'm changed! ');
// Call the corresponding native method and return the result
return original.apply(this.arguments);
};
});
let list = ['a'.'b'.'c'];
// Point the prototype pointer to the array we want to listen to to the empty array object defined above
// Don't forget that the empty array property defines methods such as push that we wrapped
list.__proto__ = arrayAugmentations;
list.push('d'); // I am changed! 4
// List2 is not redefined as a prototype pointer, so it prints normally
let list2 = ['a'.'b'.'c'];
list2.push('d'); / / 4
Copy the code
The second flaw with Object.defineProperty is that we can only hijack the properties of an Object, so we need to traverse every property of every Object. If the property value is also an Object then it requires deep traversal, obviously hijacking a whole Object is a better option.
2. Vue3.0 implements bidirectional data binding using Proxy
Proxy was officially released in ES2015 specification. It sets up a layer of “interception” before the target object, and external access to the object must pass this layer of interception. Therefore, it provides a mechanism. You can filter and rewrite external access, and we can think of Proxy as a fully enhanced version of Object.defineProperty
Rewrite the minimalist bidirectional binding implemented by Object.defineProperty
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;
});
Copy the code
As you can see, the Proxy can directly hijack the entire Object and return a new Object, which is much better than Object.defineProperty in terms of ease of operation and underlying functionality.
2.1 Proxy Can directly monitor array changes
When we operate on an array (push, shift, splice, etc.), the corresponding method name and length changes will be triggered. We can use this as an example of the list rendering above where Object.defineProperty does not work.
const list = document.getElementById('list');
const btn = document.getElementById('btn');
// Render the list
const Render = {
/ / initialization
init: function(arr) {
const fragment = document.createDocumentFragment();
for (let i = 0; i < arr.length; i++) {
const li = document.createElement('li');
li.textContent = arr[i];
fragment.appendChild(li);
}
list.appendChild(fragment);
},
// We only consider the increment case as an example
change: function(val) {
const li = document.createElement('li'); li.textContent = val; list.appendChild(li); }};// The initial array
const arr = [1.2.3.4];
// Listen for arrays
const newArr = new Proxy(arr, {
get: function(target, key, receiver) {
console.log(key);
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(target, key, value, receiver);
if(key ! = ='length') {
Render.change(value);
}
return Reflect.set(target, key, value, receiver); }});/ / initialization
window.onload = function() {
Render.init(arr);
}
/ / push the Numbers
btn.addEventListener('click'.function() {
newArr.push(6);
});
Copy the code
Obviously, a Proxy doesn’t need that many hacks (even hacks aren’t perfect for listening) to listen for array changes without pressure, and as we all know, standards always take precedence over hacks.
2.2 Other Advantages of Proxy
Proxy has up to 13 intercepting methods, not limited to apply, ownKeys, deleteProperty, has, and so on that Object. DefineProperty does not have.
Proxy 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 Object attributes.
Proxy as the new standard will be subject to ongoing performance optimization by browser vendors, which is also known as the performance bonus of the new standard.
Of course, the downside of Proxy is compatibility issues that can’t be smoothed out with polyfill, which is why Vue’s authors have stated that they will have to wait until the next big release (3.0) to rewrite Proxy.