Simulated Vue.js response principle

Keywords: Vue.js

Data driven

In the development process, we only need to focus on the data, not how to render the relational data to the view

  • Data responsive: when data is modified, the view will be updated to avoid DOM operation
  • Two way binding: data change and view change; View change, data change

Publish subscribe mode and observer mode

It defines a one to many dependency between objects. When the state of the target object changes, all objects that depend on it will be notified.

Observer mode

  • Publisher: it has a notify method, which calls the update method of the observer object when changes occur
  • Observer: with update method
  class Subscriber {
    constructor () {
      this.subs = []
    }

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

    notify() {
      this.subs.forEach(handler => { handler.update() })
    }
  }

  class Observer {
    constructor (name) {
      this.name = name;
    }

    update() {
      console.log('Receive notice', this.name);
    }
  }

  const subscriber = new Subscriber();
  const jack = new Observer('jack');
  const tom = new Observer('tom');

  subscriber.add(jack)
  subscriber.add(tom)

  subscriber.notify()

Publish subscribe mode

Different from the observer mode, the publish subscribe mode introduces the event center between the publisher and the observer. So that the target object does not directly notify the observer, but sends the notification through the event center.

  class EventController {
    constructor() {
      this.subs = {}
    }

    subscribe(key, fn) {
      this.subs[key] = this.subs[key] || []
      this.subs[key].push(fn)
    }

    publish(key, ...args) {
      if (this.subs[key]) {
        this.subs[key].forEach(handler => { handler(...args) });
      }
    }
  }

  const event = new EventController()
  event.subscribe('onWork', time => { console.log('go to work', time) });
  event.subscribe('offWork', time => { console.log('Are you off duty?', time); });
  event.subscribe('onLaunch', time => { console.log('ate', time); });

  event.publish('offWork', '18:00:00');
  event.publish('onLaunch', '20:00:00');

summary

  • The observer mode is scheduled by specific targets. When an event is triggered, the publisher will actively call the observer's methods, so there is a dependency between the publisher and the observer
  • The publish subscribe mode is uniformly scheduled by the event center because there is no strong dependency between the publisher and the subscriber

Data responsive core principle

Vue2

When you pass an ordinary JavaScript object into the Vue instance as the data option, Vue will traverse all the properties of the object and use Object.defineProperty to convert all these properties into getters / setters. Object.defineProperty is a feature in ES5 that cannot shim, which is why Vue does not support IE8 and earlier browsers.

  function proxyData(data) {
    // Traverse all properties of the data object
    Object.keys(data).forEach(key => {
      // Convert the attribute in data into vm setter/setter
      Object.defineProperty(vm, key, {
        enumerable: true,
        configurable: true,
        get () {
          console.log('get: ', key, data[key])
          return data[key]
        },
        set (newValue) {
          console.log('set: ', key, newValue)
          if (newValue === data[key]) {
            return
          }
          data[key] = newValue
          // Data changes, updating DOM values
          document.querySelector('#app').textContent = data[key]
        }
      })
    })
  }

Due to JavaScript limitations, Vue cannot detect changes in the following arrays:

  • When an array item is set directly by index, for example: vm.items[indexOfItem] = newValue
  • When modifying the length of an array, for example: vm.items.length = newLength

The performance of Object.defineProperty in the array is consistent with that in the object. The index of the array can be regarded as the key in the object.

  • getter and setter methods can be triggered when accessing or setting the value of the corresponding element through the index
  • The index will be added by push ing or unshift ing. The newly added attributes need to be manually initialized before they can be observe d.
  • Deleting an element through pop or shift will delete and update the index, and trigger setter and getter methods.

These two points are briefly summarized in the official documents as "unable to be implemented due to JavaScript limitations". In fact, the reason is not because of the vulnerability of Object.defineProperty, but because of performance problems.

Vue3

  • Proxy is a new syntax in ES 6, which is not supported by IE, and the performance is optimized by the browser.
  • Proxy listens directly to an object, not a property. defineProperty listens to a property in the object.
  let vm = new Proxy(data, {
    // Functions that perform proxy behavior
    // When a member accessing the vm executes
    get (target, key) {
      console.log('get, key: ', key, target[key])
      return target[key]
    },
    // When the vm member is set, the
    set (target, key, newValue) {
      console.log('set, key: ', key, newValue)
      if (target[key] === newValue) {
        return
      }
      target[key] = newValue
      document.querySelector('#app').textContent = target[key]
    }
  })

Implementation process

Vue

  • Receive the initialized parameters and inject them into the Vue real column $options attribute
  • Inject the attribute in data into the $data attribute of Vue real column and generate getter/setter
  • Call observer to listen for property changes in data
  • Call compiler to parse instruction / interpolation expression
class Vue {
  constructor (options) {
    // 1. Save the data of options through attributes
    this.$options = options || {};
    this.$data = options.data || {};
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
    // 2. Convert the members in data into getter s and setter s and inject them into vue instances
    this._proxyData(this.$data);
    // 3. Call the observer object to listen for data changes
    new Observer(this.$data);
    // 4. Call the compiler object to parse the instruction and difference expression
    new Compiler(this);
  }

  _proxyData(data) {
    Object.keys(data).forEach(key => {
      // Inject the data attribute into the vue instance
      Object.defineProperty(this, key, {
        configurable: true,
        enumerable: true,
        get () {
          return data[key];
        },
        set (newValue) {
          if (newValue === data[key]) {
            return;
          }
          data[key] = newValue;
        }
      })
    })
  }
}

Observer

  • Convert attributes in data into responsive data
  • Send notification of data change
class Observer {
  constructor(data) {
    this.walk(data);
  }

  walk(data) {
    // 1. Judge whether data is an object
    if (!data || typeof data !== 'object') {
      return
    }
    // 2. Traverse all the attributes of the data object
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }
  defineReactive(data, key, val) {
    // If val is an object, convert the attributes inside val into responsive data
    this.walk(val)
    const that = this;
    // Collect dependencies and send notifications
    const dep = new Dep();
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        // Collection dependency
        Dep.target && dep.addSubs(Dep.target);
        return val;
      },
      set(newVal) {
        if (newVal === val) { return; }
        val = newVal;
        // If newValue is an object, convert the properties inside newValue into responsive data
        that.walk(newVal);
        // Send notification
        dep.notify();
      }
    })
  }
}

Compiler

  • Compile template, parse instruction
  • Page rendering, re render the view when the data changes
class Compiler {
  constructor(vm) {
    this.el = vm.$el;
    this.vm = vm;
    this.compile(this.el);
  }

  // Compile templates to handle text nodes and primitive ancestor nodes
  compile (el) {
    Array.from(el.childNodes).forEach(node => {
      // Processing text nodes
      if (this.isTextNode(node)) {
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        // Processing element nodes
        this.compileElement(node)
      }

      // Judge whether the node has child nodes. If there are child nodes, call compile recursively
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }

  // Compile the text node and process the difference expression
  compileText (node) {
    const reg = /\{\{(.+?)\}\}/
    if (reg.test(node.textContent)) {
      const key = RegExp.$1.trim();
      node.textContent = node.textContent.replace(reg, this.vm[key]);

      // Create a watcher object to update the view when the data changes
      new Watcher(this.vm, key, (newValue) => { node.textContent = newValue });
    }
  }

  // Compile element nodes, process instructions
  compileElement (node) {
    Array.from(node.attributes).forEach(attr => {
      if (this.isDirective(attr.name)) {}
        const attrName = attr.name.substr(2);
        const key = attr.value;
        const updateFn = this[attrName + 'Updater']
        updateFn && updateFn.call(this, node, this.vm[key], key)
    })
  }

  // v-text
  textUpdater (node, value, key) {
    node.textContent = value;

    // Create a watcher object to update the view when the data changes
    new Watcher(this.vm, key, (newValue) => { node.textContent = newValue });
  }
  // v-model
  modelUpdater (node, value, key) {
    node.value = value;

    // Create a watcher object to update the view when the data changes
    new Watcher(this.vm, key, (newValue) => { node.value = newValue });

    // Bidirectional binding
    node.addEventListener('input', () => { this.vm[key] = node.value });
  }

  // Determine whether the element attribute is an instruction
  isDirective (attrName) {
    return attrName.startsWith('v-');
  }

  // Judge whether the node is a text node
  isTextNode (node) {
    return node.nodeType === 3;
  }

  // Determine whether the node is an element node
  isElementNode (node) {
    return node.nodeType === 1;
  }
}

Dep

  • Publisher
  • Add observers in getter s and send notifications in setter s
class Dep {
  constructor() {
    // Store all observers
    this.subs = [];
  }
  // Add observer
  addSubs(sub) {
    if (sub && sub.update) {
      this.subs.push(sub);
    }
  }
  // Send notification
  notify() {
    this.subs.forEach(sub => { sub.update() });
  }
}

Watcher

  • Add to dep when instantiating
  • When the data changes, dep notifies all watcher s to update the view
class Watcher {
  constructor(vm, key, cb) {
    // vue real column
    this.vm = vm;
    // Attribute name in data
    this.key = key;
    // The callback function updates the view
    this.cb = cb;
    // Record the watcher object to the static attribute target of the Dep class
    Dep.target = this;
    // Trigger the get method, and addSub will be called in the get method
    this.oldValue = vm[key];
    // Leave blank to prevent other properties from being affected
    Dep.target = null;
  }

  // Update the view when the data changes
  update() {
    const newValue = this.vm[this.key]
    if (this.oldValue === newValue) {
      return
    }
    this.cb(newValue);
  }
}

Bidirectional binding

  • Listen to the textbox input event and update the node value
  // Bidirectional binding
  node.addEventListener('input', () => { this.vm[key] = node.value });

Posted by jscofield on Mon, 29 Nov 2021 19:15:56 -0800