“This is my fourth day of participating in the First Challenge 2022. For more details: First Challenge 2022.”

This article implements a custom VUE,Gradually achieveBidirectional binding of data, that is, data-driven view, view-driven data

There's a summary at the end

Custom VUE classes

  • Vue requires at least two parameters: template and data

  • The Compiler object is created to render the data into the template and mount it to the specified node

class MyVue {
  // 1, takes two parameters: template (root node), and data object
  constructor(options) {
    // Save the template and the data object
    if (this.isElement(options.el)) {
      this.$el = options.el;
    } else {
      this.$el = document.querySelector(options.el);
    }
    this.$data = options.data;
    // 2. Render to the root node according to the template and data object
    if (this.$el) {
      // Listen to get/set for all attributes of data
      new Observer(this.$data);
      new Compiler(this)}}// Check if it is a DOM element
  isElement(node) {
    return node.nodeType === 1; }}Copy the code

Implement data rendering to the page for the first time

Compiler

1. The node2Fragment function extracts template elements into memory, so that data can be rendered to the template and then mounted to the page at once

2. After the template is extracted into memory, use the buildTemplate function to iterate over the template element

  • Element nodes

    • Use the buildElement function to check for attributes on elements that begin with v-
  • Text node

    • Use the buildText function to check the text for {{}} content

Create class CompilerUtil to handle vue directives and {{}} to render data

4. This completes the first data rendering, and then automatically updates the view when the data changes.

class Compiler {
  constructor(vm) {
    this.vm = vm;
    // 1. Place the elements of the page in memory
    let fragment = this.node2fragment(this.vm.$el);
    // 2. Compile elements in memory with the specified data
    this.buildTemplate(fragment);
    // 3. Re-render the compiled content on the web page
    this.vm.$el.appendChild(fragment);
  }
  node2fragment(app) {
    Create an empty document fragment object
    let fragment = document.createDocumentFragment();
    // 2. The compile loop fetches each element
    let node = app.firstChild;
    while (node) {
      // Note: as soon as an element is added to the document fragment object, it will automatically disappear from the page
      fragment.appendChild(node);
      node = app.firstChild;
    }
    // 3. Return the document fragment object where all elements are stored
    return fragment;
  }
  buildTemplate(fragment) {
    let nodeList = [...fragment.childNodes];
    nodeList.forEach(node= > {
      // We need to determine whether the node currently traversed is an element or a text
      if (this.vm.isElement(node)) {
        // Element node
        this.buildElement(node);
        // Process child elements
        this.buildTemplate(node);
      } else {
        // Text node
        this.buildText(node); }})}buildElement(node) {
    let attrs = [...node.attributes];
    attrs.forEach(attr= > {
      // v-model="name" => {name:v-model value:name}
      let { name, value } = attr;
      // v-model / v-html / v-text / v-xxx
      if (name.startsWith('v-')) {
        // v-model -> [v, model]
        let [_, directive] = name.split(The '-');
        CompilerUtil[directive](node, value, this.vm); }})}buildText(node) {
    let content = node.textContent;
    let reg = / \ {\ {+? \}\}/gi;
    if (reg.test(content)) {
      CompilerUtil['content'](node, content, this.vm); }}}Copy the code
let CompilerUtil = {
  getValue(vm, value) {
    // Parse the this.data.aaa.bbb.ccc attribute
    return value.split('. ').reduce((data, currentKey) = > {
      return data[currentKey.trim()];
    }, vm.$data);
  },
  getContent(vm, value) {
    // Parse the variables in {{}}
    let reg = / \ {\ {(. +?) \}\}/gi;
    let val = value.replace(reg, (. args) = > {
      return this.getValue(vm, args[1]);
    });
    return val;
  },
  // Parse the V-model directive
  model: function (node, value, vm) {
    // Create Wather for the DOM and assign watcher.target before triggering the getter
    new Watcher(vm, value, (newValue, oldValue) = > {
      node.value = newValue;
    });
    let val = this.getValue(vm, value);
    node.value = val;
  },
  // Parse the V-HTML directive
  html: function (node, value, vm) {
    // Create Wather for the DOM and assign watcher.target before triggering the getter
    new Watcher(vm, value, (newValue, oldValue) = > {
      node.innerHTML = newValue;
    });
    let val = this.getValue(vm, value);
    node.innerHTML = val;
  },
  // Parse the V-text instruction
  text: function (node, value, vm) {
    // Create Wather for the DOM and assign watcher.target before triggering the getter
    new Watcher(vm, value, (newValue, oldValue) = > {
      node.innerText = newValue;
    });
    let val = this.getValue(vm, value);
    node.innerText = val;
  },
  // Parse the variables in {{}}
  content: function (node, value, vm) {
    let reg = / \ {\ {(. +?) \}\}/gi;
    let val = value.replace(reg, (. args) = > {
      // Create Wather for the DOM and assign watcher.target before triggering the getter
      new Watcher(vm, args[1].(newValue, oldValue) = > {
        node.textContent = this.getContent(vm, value);
      });
      return this.getValue(vm, args[1]); }); node.textContent = val; }}Copy the code

Implement data-driven views

Observer

1. Use defineRecative function to process object. defineProperty on data, so that every data in data can be monitored by GET /set

2. How to update the view contents after listening for changes in data values? Using the Observer design pattern, create the Dep and Wather classes.

class Observer {
  constructor(data) {
    this.observer(data);
  }
  observer(obj) {
    if (obj && typeof obj === 'object') {
      // Iterate over all attributes of the passed object, adding get/set methods to all attributes traversed
      for (let key in obj) {
        this.defineRecative(obj, key, obj[key])
      }
    }
  }
  // obj: the object to operate on
  // attr: Attributes that need to be added to get/set methods
  // value: specifies the value to be added for the get/set attribute
  defineRecative(obj, attr, value) {
    // If the value of an attribute is an object, add get/set methods to all attributes of that object
    this.observer(value);
    // Step 3: Put all observer objects of the current property into the publish and subscribe object of the current property
    let dep = new Dep(); // A publish subscription object belonging to the current property is created
    Object.defineProperty(obj, attr, {
      get() {
        // Collect dependencies here
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set: (newValue) = > {
        if(value ! == newValue) {// If the new value assigned to an attribute is an object, add get/set methods to all attributes of that object as well
          this.observer(newValue);
          value = newValue;
          dep.notify();
          console.log('Listen for changes in data'); }})}}Copy the code

Using the Observer design pattern, create the Dep and Wather classes

1. The purpose of using the observer design pattern is:

  • The template is parsed to collect a dom node set of data that is used in the template. When the data changes, the DOM node set is updated to achieve the data update.

  • Dep: Collects a collection of DOM nodes that a data attribute depends on and provides an update method

  • Watcher: Wrap object for each DOM node

    • Attr: The data property used by the DOM
    • Cb: A callback that modifies the DOM value and is received at creation time

2. By now, I feel that I have no problem with my thinking and the victory is in hand. So what about Dep and Watcher?

  • Add a DEP for each attribute to collect the dependent DOM

  • The DOM is collected here because the data data is read when the page is first rendered, and the getter for that data is triggered

  • We create Wather, add a static property to the DOM for Watcher, and then fetch the static variable from the getter and add it to the dependency. That’s a collection. Because the static variable is assigned each time the getter is fired, there is no case for collecting incorrect dependencies.

class Dep {
  constructor() {
    // This array is designed to manage all observer objects for a property
    this.subs = [];
  }
  // Subscribe to the observation method
  addSub(watcher) {
    this.subs.push(watcher);
  }
  // Publish subscription methods
  notify() {
    this.subs.forEach(watcher= >watcher.update()); }}Copy the code
class Watcher {
  constructor(vm, attr, cb) {
    this.vm = vm;
    this.attr = attr;
    this.cb = cb;
    // Get the current old value when the observer object is created
    this.oldValue = this.getOldValue();
  }
  getOldValue() {
    Dep.target = this;
    let oldValue = CompilerUtil.getValue(this.vm, this.attr);
    Dep.target = null;
    return oldValue;
  }
  // Define an updated method to determine whether the new value is the same as the old value
  update() {
    let newValue = CompilerUtil.getValue(this.vm, this.attr);
    if (this.oldValue ! == newValue) {this.cb(newValue, this.oldValue); }}}Copy the code

3, when data binding is implemented here, the view automatically updates, originally wanted to code step by step implementation, but found difficult to handle, so I posted the complete class.

Implement view-driven data

Listen for input and change events in the input box. Modify the model method to CompilerUtil. The specific code is as follows

model: function (node, value, vm) {
    new Watcher(vm, value, (newValue, oldValue) = >{
        node.value = newValue;
    });
    let val = this.getValue(vm, value);
    node.value = val;
	/ / look here
    node.addEventListener('input'.(e) = >{
        let newValue = e.target.value;
        this.setValue(vm, value, newValue); })},Copy the code

conclusion

Vue bidirectional binding principle

Vue receives a template and data parameters. 1. First recursively traverse the data in data, execute Object.defineProperty for each attribute, and define get and set functions. And add a DEP array for each attribute. When get executes, a Watcher is created for the dom node called and stored in the array. When the set executes, it reassigns and calls notify of the DEP array to notify all watcher usage and update the contents of the corresponding DOM. 2. When the template is loaded into memory and the element in the recursive template is detected to have a command starting with V – or an instruction in double curly braces, the corresponding value will be taken from data to modify the template content. At this time, the DOM element will be added to the DEP array of the attribute. This enables a data-driven view. When processing v-model instructions, add an input event (or change) to the DOM and modify the value of the corresponding property as input, implementing page-driven data. 3. After binding the template to the data, add the template to the real DOM tree.

How do I put watcher in a DEP array?

When parsing the template, the corresponding data attribute value will be obtained according to the V-instruction. At this time, the get method of the attribute will be called. We first create the Watcher instance and obtain the attribute value inside it, and store it as the old value inside Watcher. Add the property Watcher. Target = this; And then the value, we’re going to say watcher. target = null; So get gets the Watcher instance object from Watcher. Target when it is called.

The principle of the methods

When creating a VUE instance, receive the methods parameter

Encountered v-ON instruction while parsing template. $methods[value]. Call (vm, e); this: vm.$methods[value].

The principle of the computed

When you create a VUE instance, you receive computed parameters

When initializing the VUE instance, handle object.defineProperty for computed keys and add the GET attribute.

Don’t get lost