The principle of analysis

Data bidirectional binding is realized through data hijacking

  • The listenerObserverIs used to hijack and listen to all attributes and notify the subscriber of any changes
  • The subscriberWatcherReceives notification of property changes and executes corresponding functions to update the view
    • Since there are multiple subscribers, a single message subscriber is adopted to facilitate unified managementDepSpecifically to collect subscribers
  • Instruction parserComplie.
    • Scan and parse the instructions for each node, initialize the template data and initialize the corresponding subscriber

As shown in figure:

Version 1.0

Implement the Observer

function Observer(data) {
  if(! data ||typeofdata ! = ='object') {
    return;
  }
  Object.keys(data).forEach(key= > {
    defineReactive(data, key, data[key]);
  });
}

function defineReactive(data, key, val) {
  // Recursive subattributes
  Observer(val);
  // Embed the message subscriber
  // Create a corresponding Dep object for each attribute, since each attribute may have multiple subscribers
  let dep = new Dep();
  // The subscriber Watcher is collected to the Dep in the getter, and all subscriber Watcher is notified by the Dep in the setter
  Object.defineProperty(data, key, {
    enumerable: true.configurable: true.get: function () {
      console.log(`getter -> key :: [[${key}]], value :: [[${val}]] `);
      if (Dep.target) { // Whether to add subscribers
        dep.addSub(Dep.target); / / subscriber
      }
      return val;
    },
    set: function (newVal) {
      val = newVal;
      console.log(`setter -> key :: [[${key}]], new value :: [[${newVal}]] `);
      dep.notify(); // Notify all subscribers subscribed to the property of updates}}); }function Dep() {
  this.subs = [];
}
Dep.prototype.addSub = function (watch) {
  this.subs.push(watch);
}
Dep.prototype.notify = function () {
  this.subs.forEach(watch= > watch.update());
}
Copy the code

Realize the Watcher

function Watcher(data, key, cb) {
  this.data = data;
  this.cb = cb;
  this.key = key;
  this.value = this.get();
}
Watcher.prototype = {
  get: function () {
    Dep.target = this; // Cache the current watcher instance into a global variable to collect the watcher instance into the Dep in the getter
    const value = this.data[this.key]; // Force the getter for the data:key property to fire
    Dep.target = null; / / release
    return value;
  },
  update: function () {
    this.cb(); }}Copy the code

demo

<body>
  <h1 id="name">name</h1>
  <script src="./myVue.js"></script>
  <script>
  
    let data = {
      name: 'zheling'}; observer(data);new Watcher(data, 'name'.function () {
      document.querySelector('#name').innerHTML = 'hello vue?? ';
    });

    setTimeout(function () {
      data.name = 'hello Vue'
    }, 1000);

  </script>
</body>
Copy the code

Version 2.0

Realize the Watcher 2.0

function Watcher(vm, key, cb) {
  this.vm = vm;
  this.data = this.vm.data;
  this.cb = cb;
  this.key = key;
  this.value = this.get();
}
Watcher.prototype = {
  get: function () {
    Dep.target = this; // Cache the current watcher instance into a global variable to collect the watcher instance into the Dep in the getter
    const value = this.data[this.key]; // Force the getter for the data:key property to fire
    Dep.target = null; / / release
    return value;
  },
  update: function () {
    let oldVal = this.value;
    let value = this.data[this.key];
    if(value ! == oldVal) {this.value = value;
      this.cb.call(this.vm, value, oldVal); }}}Copy the code

MyVue

Associate the Observer with Watcher

function MyVue(data, el, key) {
  this.data = data;
  this.el = el;
  observer(this.data); // Listen for attributes
  // Proxy attributes: vm.data.key -> vm.key
  Object.keys(this.data).forEach(key= > {
    this.proxyKeys(key);
  });
  new Watcher(this, key, function (value) {
    el.innerHTML = value;
  });
  return this;
}
MyVue.prototype.proxyKeys = function (key) {
  Object.defineProperty(this, key, {
    enumerable: true.configurable: true.get: function () {
      return this.data[key];
    },
    set: function (newVal) {
      this.data[key] = newVal; }}); }Copy the code

demo

<body>
  <h1 id="name">name</h1>
  <script src="./myVue.js"></script>
  <script>

    let data = {
      name: 'zheling'};let vm = new MyVue(
      data,
      document.querySelector('#name'),
      'name'
    );

    setTimeout(function () {
      vm.data.name = 'hello Vue'
    }, 1000);

  </script>
</body>
Copy the code

Version 3.0

MyVue

function MyVue(options) {
  let { el, data,methods } = options || {};
  this.data = data;
  this.el = el;
  this.methods = methods;
  observer(this.data);
  Object.keys(this.data).forEach(key= > {
    this.proxyKeys(key);
  });
  // Parse the DOM, create a subscriber and bind the corresponding update function to it
  new Compile(this);
  return this;
}
MyVue.prototype.proxyKeys = function (key) {
  Object.defineProperty(this, key, {
    enumerable: true.configurable: true.get: function () {
      return this.data[key];
    },
    set: function (newVal) {
      this.data[key] = newVal; }}); }Copy the code

Compile

// Implement the compiler that wraps the above example to initialize the subscriber and parse the DOM
function Compile(vm) {
  this.vm = vm;
  this.el = document.querySelector(vm.el);
  // Get all the nodes under the root container. Since frequent DOM manipulation is required for performance reasons, move them to a fragment for processing
  this.fragment = null;
  this.init();
}
Compile.prototype = {
  init: function () {
    this.fragment = this.nodeToFragment(this.el);
    this.compileElement(this.fragment);
    this.el.appendChild(this.fragment);
  },
  nodeToFragment: (el) = > {
    var fragment = document.createDocumentFragment();
    var child = el.firstChild;
    while (child) {
      // Move the DOM element into the fragment
      fragment.appendChild(child);
      child = el.firstChild
    }
    return fragment;
  },
  // traverses all nodes under el
  compileElement: function (el) {
    let childNodes = el.childNodes; // A pseudo-array object
    let reg = / \ {\ {(. +) \} \} /;
    Array.from(childNodes).forEach(node= > {
      let text = node.textContent;
      if (this.isTextNode(node) && reg.test(text)) { // Matches the {{}} directive
        console.log(node);
        this.compileText(node, reg.exec(text)[1]); // Get the key in {{key}}
      }
      // Iterate over the child nodes
      if (node.childNodes && node.childNodes.length) {
        this.compileElement(node); }}); },compileText: function (node, key) {
    // Initialize the data
    this.updateText(node, this.vm[key]);
    // Create a subscriber and bind the update function
    new Watcher(this.vm, key, (value) = > {
      this.updateText(node, value);
    });
  },
  updateText: function (node, text) {
    node.textContent = text ? text : ' ';
  },
  isTextNode: function (node) {
    return node.nodeType === 3; }},Copy the code

demo

<body>
  <div id="app">
    <h1>{{name}}</h1>
    <span>{{age}}</span>
  </div>
  <script src="./myVue.js"></script>
  <script>
    let vm = new MyVue({
      el: '#app'.data: {
        name: 'zheling'.age: 99}});setTimeout(function () {
      vm.name = 'hello Vue'
    }, 1000);
  </script>
</body>
Copy the code

Version 4.0

Add processing of V-ON/V-model instructions

Compile

Compile.prototype = {
  init: function () {
    this.fragment = this.nodeToFragment(this.el);
    this.compileElement(this.fragment);
    this.el.appendChild(this.fragment);
  },
  nodeToFragment: (el) = > {
    var fragment = document.createDocumentFragment();
    var child = el.firstChild;
    while (child) {
      // Move the DOM element into the fragment
      fragment.appendChild(child);
      child = el.firstChild
    }
    return fragment;
  },
  // traverses all nodes under el
  compileElement: function (el) {
    let childNodes = el.childNodes; // A pseudo-array object
    let reg = / \ {\ {(. +) \} \} /;
    Array.from(childNodes).forEach(node= > {
      let text = node.textContent;
      // Whether it is an element node
      if (this.isElementNode(node)) {
        this.compile(node);
      } else if (this.isTextNode(node) && reg.test(text)) { // Matches the {{}} directive
        this.compileText(node, reg.exec(text)[1]); // Get the key in {{key}}
      }
      // Iterate over the child nodes
      if (node.childNodes && node.childNodes.length) {
        this.compileElement(node); }}); },compileText: function (node, key) {
    // Initialize the data
    this.updateText(node, this.vm[key]);
    // Create a subscriber and bind the update function
    new Watcher(this.vm, key, (value) = > {
      this.updateText(node, value);
    });
  },
  updateText: function (node, text) {
    node.textContent = text ? text : ' ';
  },
  compile: function (node) {
    var nodeAttrs = node.attributes;
    var self = this;
    // Iterate through node attributes to find the directive starting with v-*
    Array.from(nodeAttrs).forEach(function (attr) {
      var attrName = attr.name;
      if (self.isDirective(attrName)) {
        var exp = attr.value;
        var dir = attrName.substring(2); // remove 'v-' from 'v- * '
        // Select model from model and on from model
        if (self.isEventDirective(dir)) {  // Event command
          self.compileEvent(node, self.vm, exp, dir);
        } else {  / / v - model instructionself.compileModel(node, exp); } node.removeAttribute(attrName); }}); },compileEvent: function (node, vm, key, dir) {
    var eventType = dir.split(':') [1];
    var cb = vm.methods && vm.methods[key];
    if (eventType && cb) {  // Bind (vm) can be used to quickly access MyVue instance
      node.addEventListener(eventType, cb.bind(vm), false); }},compileModel: function (node, key) {
    var self = this;
    var val = this.vm[key];
    // Initialize node.value
    node.value = typeof val == 'undefined' ? ' ' : val;
    // One of the bidirectional bindings: data.field -> input
    new Watcher(this.vm, key, function (value) {
      node.value = typeof value == 'undefined' ? ' ' : value;
    });
    // input -> data.field
    node.addEventListener('input'.function (e) {
      var newValue = e.target.value;
      if (val === newValue) {
        return;
      }
      self.vm[key] = newValue;
    });
  },
  isDirective: function (attr) {
    return attr.indexOf('v-') = =0;
  },
  isEventDirective: function (dir) {
    return dir.indexOf('on:') = = =0;
  },
  isTextNode: function (node) {
    return node.nodeType === 3;
  },
  isElementNode: node= > node.nodeType === 1,}Copy the code

demo

<body>
  <div id="app">
    <h2>{{title}}</h2>
    <input v-model="name">
    <h1>name : {{name}}</h1>
    <button v-on:click="clickMe">click me!</button>
  </div>

  <script src="./myVue.js"></script>
  <script>

    let vm = new MyVue({
      el: '#app'.data: {
        title: 'zheling'.name: 'kangshi',},methods: {
        clickMe: function () {
          this.title = 'new title'; }}});// Verify that the v-model instruction data.name -> input.value direction is properly synchronized
    setTimeout(function () {
      vm.name = 'hello Vue?? '
    }, 5000);

  </script>
</body>
Copy the code

Refer to the article

  • Vue bidirectional binding principle and implementation