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:

    1. We only listen for one property, an object can’t just have one property, we need to listen for every property of the object.
    1. 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 propsGetter/setterThat 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.