preface

This article is a summary of learning experience, not a tutorial, if you have questions welcome to discuss.


Ready to code

Before there is a summary of Vue bidirectional binding implementation, I suggest you go to see this article, this article is based on its expansion, so go to have a look, if you have seen it, copy the code, later to use.

Vue bidirectional binding analysis

Review of bidirectional binding

As mentioned earlier, bidirectional binding is the hijacking of an attribute, subscribing to it when it gets, and notifying it of updates when it sets.

The concept of both

If you think about the implementation of computed and Watch, my personal understanding is:

  • Computed is a subscriber subscribing to multiple attributes
  • Watch is a subscriber subscribing to a property and executing specific methods when it is updated

So that’s where the code starts.

implementation

computed

First modify the HTML file:


      
<html lang="en">
  <body>
    <div id="app">
      <input v-model="name" />
      <h1>{{ name }}</h1>
      <h1>{{ age }}</h1>
      <h1>{{ hello }}</h1>
      <button v-on:click="addAge">Chinese New Year, one year older</button>
      <button v-on:click="changeName">My name is li si</button>
    </div>

    <script src="./myvue.js"></script>
    <script>
      var vm = new MyVue({
        el: '#app'.data: {
          name: 'Joe'.age: 20
        },

        computed: {
          hello() {
            return 'Hello, I amThe ${this.name}This year,The ${this.age}Years old. `; }},methods: {
          addAge() {
            this.age++;
          },
          changeName() {
            this.name = 'bill'; }}});</script>
  </body>
</html>
Copy the code

Compared to before, you just add a computed property and render it on the page, nothing special, so let’s deal with the JS part.

First mount computed in the MyVue class constructor:

class MyVue {
  constructor({ el, data, computed, methods }) {
    let obs = new Observer(data);

    this.$el = el;
    this.$data = obs.data;
    this.$computed = computed; // add
    this.$methods = methods;

    Object.keys(this.$data).forEach(i= > {
      this.proxyKeys(i);
    });

    new Compile(this);
  },
  
  // code ... 
}
Copy the code

Node.nodetype === 3; Compile = node.nodeType == 3;

if (node.nodeType === 3) {
  let reg = / \ {\ {(. *) \} \} /;
  if (reg.test(node.textContent)) {
    / / here in the text may have multiple {{}}, {{}} might have expression, in this simple processing, he took a value
    let exp = reg.exec(node.textContent)[1].trim();

    // old code
    // let value = this.vm.$data[exp];

    // node.textContent = typeof value === 'undefined' ? '' : value;

    // new Watcher(this.vm.$data, exp, newVal => {
    // node.textContent = typeof newVal === 'undefined' ? '' : newVal;
    // });

    // new code
    if (this.vm.$data[exp]) {
      let value = this.vm.$data[exp];
      node.textContent = typeof value === 'undefined' ? ' ' : value;

      new Watcher(this.vm.$data, exp, newVal => {
        node.textContent = typeof newVal === 'undefined' ? ' ' : newVal;
      });
    } else if (this.vm.$computed[exp]) {
      let computed = new ComputedWatcher(this.vm, exp, newVal => {
          node.textContent = typeof newVal === 'undefined' ? ' ': newVal; }); node.textContent = computed.value; }}}Copy the code

In fact, when you evaluate, you see if you have data, if you don’t have it in computed, if you have it in computed, you use the computed attribute subscriber generator to generate computed attribute subscribers.

The structure is similar to that of Watcher, passing in the VM, keys that evaluate properties, and callback functions to update the contents of the node as it is updated.

So now let’s write the ComputedWatcher class, which looks a lot like Watcher:

class ComputedWatcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;

    Dep.target = this;
    this.value = this.vm.$computed[key].call(this.vm);
    Dep.target = null;
  }

  update() {
    let newVal = this.vm.$computed[this.key].call(this.vm);
    let oldVal = this.value;
    if(newVal ! == oldVal) {this.value = newVal;
      this.cb(newVal, oldVal); }}}Copy the code

In effect, it’s just Watcher copying it, using computed methods for the first time, so that every property used in a method adds the calculated property’s subscriber to its subscriber, so that the calculated property is notified whenever one of them changes.

The UPDATE method, on the other hand, performs a comparison of old values to determine whether to update nodes, thus achieving computed implementation.

watch

Watch is very simple. It listens to a certain attribute, updates and executes corresponding methods. In fact, it is the same as parsing DOM nodes.

So let’s revamp the HTML


      
<html lang="en">
  <body>
    <div id="app">
      <input v-model="name" />
      <h1>{{ name }}</h1>
      <h1>{{ age }}</h1>
      <h1>{{ hello }}</h1>
      <button v-on:click="addAge">Chinese New Year, one year older</button>
      <button v-on:click="changeName">My name is li si</button>
    </div>

    <script src="./myvue.js"></script>
    <script>
      var vm = new MyVue({
        el: '#app'.data: {
          name: 'Joe'.age: 20
        },

        computed: {
          hello() {
            return 'Hello, I amThe ${this.name}This year,The ${this.age}Years old. `; }},watch: {
          name() {
            alert('Hello, I amThe ${this.name}! `); }},methods: {
          addAge() {
            this.age++;
          },
          changeName() {
            this.name = 'bill'; }}});</script>
  </body>
</html>
Copy the code

Alert you when you make changes.

Since watch is page-independent, you just need to change MyVue as follows:

class MyVue {
    constructor({ el, data, computed, watch, methods }) {
    let obs = new Observer(data);

    this.$el = el;
    this.$data = obs.data;
    this.$computed = computed;
    this.$watch = watch; // add1
    this.$methods = methods;

    Object.keys(this.$data).forEach(i= > {
      this.proxyKeys(i);
    });

    new Compile(this);
    
    // add2
    Object.keys(this.$watch).forEach(key= > {
      new Watcher(this.$data, key, () => {
        this.$watch[key].call(this);
      });
    });
  },
  
  // code ... 
}
Copy the code
  • Add1: Mount watch
  • Add2: Generates each watch as a subscriber and executes the corresponding method in the callback function.

So, ok.


All codes:

  • index.html

      
<html lang="en">
  <body>
    <div id="app">
      <input v-model="name" />
      <h1>{{ name }}</h1>
      <h1>{{ age }}</h1>
      <h1>{{ hello }}</h1>
      <button v-on:click="addAge">Chinese New Year, one year older</button>
      <button v-on:click="changeName">My name is li si</button>
    </div>

    <script src="./myvue.js"></script>
    <script>
      var vm = new MyVue({
        el: '#app'.data: {
          name: 'Joe'.age: 20
        },

        computed: {
          hello() {
            return 'Hello, I amThe ${this.name}This year,The ${this.age}Years old. `; }},watch: {
          name() {
            alert('Hello, I amThe ${this.name}! `); }},methods: {
          addAge() {
            this.age++;
          },
          changeName() {
            this.name = 'bill'; }}});</script>
  </body>
</html>
Copy the code
  • myvue.js
// Subscriber generator
class Observer {
  constructor(data) {
    this.data = data;
    Object.keys(data).forEach(key= > {
      let value = data[key];
      let dep = new Dep();

      Object.defineProperty(data, key, {
        get() {
          Dep.target && dep.add(Dep.target);
          return value;
        },
        set(newVal) {
          if(newVal ! == value) { value = newVal; dep.notify(newVal); }}}); }); }}// Subscriber generator
class Watcher {
  constructor(data, key, cb) {
    this.data = data;
    this.cb = cb;
    this.key = key;

    Dep.target = this;
    this.value = data[key];
    Dep.target = null;
  }

  update(newVal) {
    let oldVal = this.value;
    if(newVal ! == oldVal) {this.value = newVal;
      this.cb(newVal, oldVal); }}}// Computes the attribute subscriber generator
class ComputedWatcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;

    Dep.target = this;
    this.value = this.vm.$computed[key].call(this.vm);
    Dep.target = null;
  }

  update() {
    let newVal = this.vm.$computed[this.key].call(this.vm);
    let oldVal = this.value;
    if(newVal ! == oldVal) {this.value = newVal;
      this.cb(newVal, oldVal); }}}// Subscribe library generator
class Dep {
  constructor() {
    this.subs = [];
  }

  add(sub) {
    this.subs.push(sub);
  }

  notify(newVal) {
    this.subs.forEach(sub= > {
      sub.update(newVal);
    });
  }
}
Dep.target = null;

// Fragment resolver
class Compile {
  constructor(vm) {
    this.vm = vm;

    let el = document.querySelector(this.vm.$el);
    let fragment = document.createDocumentFragment();

    if (el) {
      while (el.firstChild) {
        fragment.appendChild(el.firstChild);
      }

      // Compile the fragment
      this.compileElement(fragment);

      el.appendChild(fragment);
    } else {
      console.log('Mount element does not exist! ');
    }
  }

  compileElement(el) {
    for (let node of el.childNodes) {
      /* node.nodeType 1: element node 3: text node */
      if (node.nodeType === 1) {
        for (let attr of node.attributes) {
          let { name: attrName, value: exp } = attr;

          // v- indicates the presence of an instruction
          if (attrName.indexOf('v-') = = =0) {
            /* 
      
let [dir, value] = attrName.substring(2).split(':'); if (dir === 'on') { // take vm.methods and bind them let fn = this.vm.$methods[exp]; fn && node.addEventListener(value, fn.bind(this.vm), false); } else if (dir === 'model') { // Assign vm.data to input, and update vm.data when input let value = this.vm.$data[exp]; node.value = typeof value === 'undefined' ? ' ' : value; node.addEventListener('input', e => { if(e.target.value ! == value) {this.vm.$data[exp] = e.target.value; }});new Watcher(this.vm.$data, exp, newVal => { node.value = typeof newVal === 'undefined' ? ' ': newVal; }); }}}}else if (node.nodeType === 3) { let reg = / \ {\ {(. *) \} \} /; if (reg.test(node.textContent)) { / / here in the text may have multiple {{}}, {{}} might have expression, in this simple processing, he took a value let exp = reg.exec(node.textContent)[1].trim(); // old code // let value = this.vm.$data[exp]; // node.textContent = typeof value === 'undefined' ? '' : value; // new Watcher(this.vm.$data, exp, newVal => { // node.textContent = typeof newVal === 'undefined' ? '' : newVal; // }); // new code if (this.vm.$data[exp]) { let value = this.vm.$data[exp]; node.textContent = typeof value === 'undefined' ? ' ' : value; new Watcher(this.vm.$data, exp, newVal => { node.textContent = typeof newVal === 'undefined' ? ' ' : newVal; }); } else if (this.vm.$computed[exp]) { let computed = new ComputedWatcher(this.vm, exp, newVal => { node.textContent = typeof newVal === 'undefined' ? ' ' : newVal; }); node.textContent = computed.value; // Will computed do a proxy for this.xxx Object.defineProperty(this.vm, exp, { enumerable: false.configurable: true, get() { returncomputed.value; }}); }}}if (node.childNodes && node.childNodes.length) { this.compileElement(node); }}}}class MyVue { constructor({ el, data, computed, watch, methods }) { let obs = new Observer(data); this.$el = el; this.$data = obs.data; this.$computed = computed; this.$watch = watch; this.$methods = methods; Object.keys(this.$data).forEach(i= > { this.proxyKeys(i); }); new Compile(this); Object.keys(this.$watch).forEach(key= > { new Watcher(this.$data, key, () => { this.$watch[key].call(this); }); }); } proxyKeys(key) { let _this = this; Object.defineProperty(_this, key, { enumerable: false.configurable: true, get() { return_this.$data[key]; }, set(newVal) { _this.$data[key] = newVal; }}); }}Copy the code

PS: after the new ComputedWatcher, add a proxy method to proxy the calculation property to this, so as to call this.xxx.