The premise

There are many ways to achieve bidirectional binding, Vue uses data hostage combined with publish and subscribe design pattern to achieve.

Use Object.defineProperty() to hijack the setter and getter for each property, publish messages to subscribers when data changes, and trigger corresponding listening callbacks.

Object. DefineProperty () is introduced

Function: More commonly known as manipulation of object properties

Official: Directly define a new property on an object, or modify an existing property of an object, and return the object.

This method takes three arguments:

Object.defineproperty (obj, prop, Descriptor)

Obj Specifies the object for which attributes are to be defined.

Prop The name or Symbol of the property to define or modify.

Descriptor Property descriptor to define or modify.

Has the following properties:

  • Any additional attributes of the descriptor can be modified without any additional control

  • Enumerable: A is an attribute that can be found by for

  • Writable: obj. A = 1

  • Value: Indicates the value of the attribute

  • Get: is a function that is automatically called when accessing the property and returns the value of the property

  • Set: is a function that is automatically called when the property is modified. The function takes one and only argument, the assigned value

* The value property writable and get property set property are mutually exclusive

Property held hostage

Vue data capture is to use get,set attributes to achieve. The get function fires when an object property is read, and the set function fires when an assignment is made

* Get can’t have a read operation, otherwise the loop will be endless, so we always need to use a variable to get set

Example:

let obj = {};

let name = "xx";
Object.defineProperty(obj, "n", {
  get() {
    console.log("Read", name);
    return name;
  },
  set(newName) {
    console.log("Settings", newName); value = newName; }});// Trigger the get function. The return value of get is the property value
console.log(obj.n);
// The set function is triggered, and the value of value is changed to XXXX
obj.n = "xxxx";
console.log(obj.n);

Copy the code

Object. DefineProperty () extension

Back in Vue development, we often came across a scenario where the value of a modified array couldn’t be bidirectional because the defineProperty() get set couldn’t detect new modifications to the object array.

Vue monitors array changes by finding ways to change the original array and then hijacking those methods

The general steps are as follows: Array => New prototype (perform detection behavior) => Array prototype

In a scenario where an array is an array, the idea of recursion is to solve this problem

This is why vUE has set/set/set/ delete and arrays can only be detected using specific methods

The whole idea of bidirectional binding

1. Implement data listener, the main role is to listen to all attributes, Vue called Observer

2. Then notify updates by subscribing to publish design ideas

(The subscription publishing pattern defines a one-to-many dependency, one for the publisher, such as a topic object, and more for the subscriber, the dependency is the subscriber’s dependence on the publisher; Multiple subscribers listen on a topic object simultaneously. When the state of the publisher, the topic object, changes, the subscriber is notified of the change, and the subscriber updates his status accordingly.)

3. Define a subscriber Dep to collect changes to these properties to notify subscribers

4. For different events, what processing needs to be done, so we also need a ComPile(instruction parser), such as drop down operations, input operations, etc

5. Finally implement a Watcher(subscriber), mainly to receive different operations, for those objects, update the view

Implement an Observer

function observe(data){
	if(data && typeof data === "object") {Object.keys(data).forEach((key) = >{ defineReactive(data, key, data[key]); }}})function defineReactive(data,key,val){
	// Recursively, listen for child objectsObserve (val)Object.defineProperty(data, key, {
    enumerable: true.configurable: false.get: function () {
      return val;
    },
    set: function (value) { val = value; }}); }Copy the code

Implementing subscribers

//Dep
function Dep() {
  this.subs = [];
}
Dep.prototype = {
  addSub: function (sub) {
    this.subs.push(sub);
  },
  notify: function () {
    this.subs.forEach(function (sub) { sub.update(); }); }};Copy the code

Stick it in the monitor

function defineReactive(data,key,val){
	
	
	  var dep = new Dep()

    // Recursively, listen for child objectsObserve (val)Object.defineProperty(data, key, {
    enumerable: true.configurable: false.get: function () {
      return val;
    },
    set: function (value) {
		
		dep.notify()
		
    },
  });

}
Copy the code

Implement Watcher (Subscriber)

2. Implement update() 3. When notified, the update() method is called, triggering the compiler’s comPlie callback 4. The end of the

// Watcher
function Watcher(vm, exp, cb) {
  this.cb = cb;
  this.$vm = vm;
  this.exp = exp;
  // Add yourself to the DEp by triggering the getter for the property
  this.value = this.get(); // Add yourself to the subscriber operation
}
Watcher.prototype = {
  update: function () {
    this.run(); // Attribute value changes are notified
  },
  run: function () {
    var value = this.get(); // Get the latest value
    var oldVal = this.value;// The value of the set function has not changed
    if(value ! == oldVal) {this.value = value;
      this.cb.call(this.$vm, value, oldVal); // Execute the callback bound in Compile to update the view}},get: function () {
    Dep.target = this; // Point the current subscriber to yourself, cache
    var value = this.$vm[this.exp]; // Force the listener getter to add itself to the property subscriber
    Dep.target = null; // Reset release
    returnvalue; }};Copy the code

The corresponding get in the defineReactive method should also be modified

function defineReactive(data, key, val) {
  var dep = new Dep()
  observe(val); // Listen for child attributes
  Object.defineProperty(data, key, {
    ....
    get: function() {
      // Since we need to add watcher to the closure, we can define a global target attribute in the Dep to temporarily store the watcher and remove it after adding it
      Dep.target && dep.addDep(Dep.target);
      returnval; },... }); }Copy the code

Simple integration

<div id="name"></div>
<script>
  function Vue(data, el, exp) {
    this.data = data;
    observe(data);
    el.innerHTML = this.data[exp]; // Initializes the value of template data
    new Watcher(this, exp, function (value) {
      el.innerHTML = value;
    });
    return this;
  }
  var ele = document.querySelector("#name");
  var vue = new Vue(
    {
      name: "hello world",
    },
    ele,
    "name"
  );
  setInterval(function () {
    vue.data.name = "chuchur " + new Date(*)1;
  }, 1000);
</script>
Copy the code

XXX, not this.data. XXX, so we need to add a proxy Vm proxy Vm. Data

function Vue(options) {

  this.$options = options || {};
  this.data = this.$options.data;
  XXX -> vm.data.xxx
  var self = this;
  Object.keys(this.data).forEach(function(key) {
    //this.data ==>this
    self.proxy(key); // Bind the proxy properties

  });
  observe(this.data, this);​
  el.innerHTML = this.data[exp]; // Initializes the value of template data
  new Watcher(this, exp, function(value) {

    el.innerHTML = value;

  });
  return this;

}

// Also uses the defineProperty () method
Vue.prototype = {

  proxy: function(key) {
    var self = this;
    Object.defineProperty(this, key, {
      enumerable: false.configurable: true.get: function proxyGetter() {
        return self.data[key];
      },
      set: function proxySetter(newVal) { self.data[key] = newVal; }}); }}Copy the code

Finally, the compiler Compile is implemented

Here’s the idea:

  1. Parse the template instructions, replace the template data, and initialize the view
  2. Initialize the corresponding subscriber by binding the node corresponding to the template instruction to the corresponding update function
Compile.prototype = {

  ......
  isDirective: function(attr) {
    return attr.indexOf('v-') = =0;
  },

  isEventDirective: function(dir) {
    return dir.indexOf('on:') = = =0;
  },

  // Process the V-instruction
  compile: function(node) {

    var nodeAttrs = node.attributes,
      self = this;
    [].slice.call(nodeAttrs).forEach(function(attr) {
      // Directive named after V-xxx
       the command is V-text
      var attrName = attr.name; // v-text
      if (self.isDirective(attrName)) {
        var exp = attr.value; // content
        var dir = attrName.substring(2); // text
        if (self.isEventDirective(dir)) {
          // Event directives, such as V-on :click
          self.compileEvent(node, self.$vm, exp, dir);
        } else {
          // Common instructions such as v-model, v-html, currently only v-model
          self.compileModel(node, self.$vm, exp, dir);
        }
        // Execute v-on:, V-model and other element attributes
        node.removeAttribute(attrName)
      }
    });

  },

  compileEvent: function(node, vm, exp, dir) {

    var eventType = dir.split(':') [1];
    var cb = vm.$options.methods && vm.$options.methods[exp];
    if (eventType && cb) {
      node.addEventListener(eventType, cb.bind(vm), false); }},compileModel: function(node, vm, exp, dir) {

    var self = this;
    var val = this.$vm[exp];
    this.updaterModel(node, val);
    new Watcher(this.$vm, exp, function(value) {
      self.updaterModel(node, value);
    });
    node.addEventListener('input'.function(e) {
      var newValue = e.target.value;
      if (val === newValue) {
        return;
      }
      self.$vm[exp] = newValue;
      val = newValue;
    });

  },

  updaterModel: function(node, value, oldValue) {
    node.value = typeof value == 'undefined' ? ' ': value; }},Copy the code

The last

Hope XDM gives you a thumbs up if this article helps you a little bit