Big front-end encyclopedia, front-end encyclopedia, record the front-end related field knowledge, convenient follow-up access and interview preparation

Key words: publish subscribe, Observer, data hijacking, dependency collection, Object.defineProperty, Proxy, Observer, Compiler, Watcher, Dep…

  • How does vUE’s bidirectional binding work?
  • How does Vue collect dependencies?
  • Describe the process from Vue Template to render

How does vUE’s bidirectional binding work?

Vue. js adopts data hijacking combined with publiser-subscriber mode. It hijabs (monitors) the setter and getter of each attribute through object-defineProperty () method provided by ES5, releases messages to subscribers when data changes, and triggers corresponding listening callback. And because synchronization is triggered on different data, changes can be sent precisely to the bound view, rather than checking all data at once.

Specific steps:

  1. You need the observer to recursively traverse the data objects, including the attributes of the child property objects, with getters and setters so that if you assign a value to that object, the setter will be triggered, and you’ll be listening for changes in the data

  2. Compile parses the template instructions, replaces variables in the template with data, initializes the render page view, and binds the corresponding node of each instruction to update function, adds subscribers to listen to the data, receives notification once the data changes, and updates the view

  3. Watcher subscribers serve as a bridge between the Observer and Compile. They do the following:

    • Add yourself to the attribute subscriber (DEP) during self instantiation
    • There must be an update() method itself
    • If you can call your own update() method and trigger the callback bound with compile when the dep.notify() property changes, you are done
  4. MVVM, as a data binding entry, integrates observer, Compile and Watcher, monitors its model data changes through observer, and compiles template instructions through compile. Finally, use the communication bridge between the Observer and compile built by Watcher to achieve data changes –> attempt to update; View interactive Changes (INPUT) –> Bidirectional binding effect of data model changes

Compare versions

  • Vue is bidirectional binding based on dependency collection
  • Before version 3.0, object.definePropetry was used
  • New in version 3.0 uses Proxy
  • 1. Advantages of bidirectional binding based on data hijacking/dependency collection

    • Without the need for display calls, Vue uses data hijacking + publish-subscribe to directly notify changes and drive views
    • Get the exact change data directly, hijacking the property setter. When the property value changes, we can get the exact change newValue without additional diff operations
  • 2. The Object. DefineProperty shortcomings

    • Cannot listen on arrays: because arrays have no getters and setters, and because arrays are of indeterminate length, too long is too much of a performance burden
    • Only properties can be listened on, not the entire object, and loop properties need to be traversed
    • You can only listen for attribute changes, not for attribute deletions
  • 3. The benefits of the proxy

    • You can listen to arrays
    • Listening on the entire object is not a property
    • 13 ways to intercept, much more powerful
    • It is more immutable to return a new object rather than modify the original;
  • 4. The disadvantage of the proxy

    • Poor compatibility and can not be smoothed by polyfill;

How does Vue collect dependencies?

When each VUE component is initialized, the component’s data is initialized to change the normal object into a responsive object, and the dependency collection logic is performed during this process

function defieneReactive(obj, key, val){
    const dep = new Dep();
    / /...
    Object.defineProperty(obj, key,{
        / /...
        get: function reactiveGetter(){
            if(Dep.target){
                dep.depend();
                / /...
            }
            return val
        }
    })
}
Copy the code

Const dep=new dep () instantiates an instance of dep, which can then be collected in get via dep.depend()

Dep

Dep is the core of the entire dependency collection

class Dep {
  static target;
  subs;

  constructor () {...this.subs = [];
  }
  addSub (sub) { / / add
    this.subs.push(sub)
  }
  removeSub (sub) { / / remove
    remove(this.sub, sub)
  }
  depend () { / / target to add
    if(Dep.target){
      Dep.target.addDep(this)
    }
  }
  notify () { / / response
    const subs = this.subds.slice();
    for(let i = 0; i < subs.length; i++){ subs[i].update() } } }Copy the code

Dep is a class containing a static property that refers to a globally unique Watcher, ensuring that only one Watcher is evaluated at a time. Subs is a Watcher array

watcher

class Watcher { getter; .constructor (vm, expression){
    ...
    this.getter = expression;
    this.get();
  }
  get () {
    pushTarget(this);
    value = this.getter.call(vm, vm)
    ...
    return value
  }
  addDep (dep){
        ...
    dep.addSub(this)}... }function pushTarget (_target) {
  Dep.target = _target
}
Copy the code

Watcher is a class that defines methods. The dependency collection related functions are get and addDep

process

When instantiating Vue, rely on the collection related procedures

Initialize the state initState, which transforms the data into a reactive object via defineReactive, where the getter part is collected.

Initialization ends with the mount process, which instantiates watcher and executes the this.get() method inside watcher

updateComponent = () = >{
    vm._update(vm._render())
}

new Watcher(vm,updateComponent)
Copy the code

PushTarget in the get method essentially assigns dep. target to the current watcher,this.getter.call(vm,vm), where the getter executes vm._render(), which triggers the getter for the data object

The getter for each object value holds a DEP. Dep.depend () is called when the getter is triggered. Dep.target.adddep (this) is called.

Dep.addsub () will subscribe the current watcher to the subs of the Dep held by the Dep. This is in order to notify the subs of the Dep when the data changes

So in vm._render(), all the getters for the data are fired, and a dependency collection is completed

Describe the process from Vue Template to render

Process analysis

Vue templates are compiled as follows: template-ast-render function

Vue converts the template to the render function by executing compileToFunctions in the template compilation

Main cores for compileToFunctions:

  1. Call the parse method to convert template to an AST tree (abstract syntax tree)

The purpose of parse is to convert a template into an AST tree, which is a way to describe the entire template in the form of A JS object.

Parsing process: The template is parsed sequentially using regular expressions. When the start tag, close tag, and text are parsed, the corresponding callback functions are executed respectively to form the AST tree

There are three types of AST element nodes (type) : normal element –1, expression –2, and plain text –3

  1. Optimize static nodes

This process mainly analyzes which are static nodes and makes a mark on them, which can be directly skipped for later update rendering, and static nodes are optimized

Walk through the AST in depth to see if the node element of each subtree is a static node or a static node root. If they are static nodes, the DOM they generate never changes, which is optimized for runtime template updates

  1. Generate code

Compile the AST abstract syntax tree into a Render string, put the static part into a staticRender, and finally generate the Render Function with new Function(Render)

Implement two-way data binding

Index. Js

  • Data hijacking _proxyData to synchronize data to vm: this.data.name => this.name
  • New Observer data hijacking
  • Compiler generates the render function string with(code)…

observer.js

  • Recursive traversal, data hijacking defineReactive, create dep = new dep ()
  • Object. Property get attribute added to subs of deP instance: dep.addSub(dep.target)
  • Dep. notify notifies all Watcher of subs to update => patch and dom update

compiler.js

  • Compilation of the template
  • For loop traverses childNodes (text, element nodes)
  • Create an observer new Watcher() and mount the instance to dep.target

watcher.js

  • Dep.target = this, put the watcher on dep. target
  • Dom update method, followed by patch method

Dep. Js subscribers

  • AddSub, push watcher into subs
  • Notify notifies all watchers in subs and calls the update method

The demo code

index.html

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <p>MSG: {{MSG}}</p>
      <p>Age: {{age}}</p>
      <div v-text="msg"></div>
      <input v-model="msg" type="text" />
    </div>
    <script type="module">
      import Vue from "./js/vue.js";

      let vm = new Vue({
        el: "#app".data: {
          msg: "123".age: 21,}});window.vm = vm
    </script>
  </body>
</html>
Copy the code

vue.js

import Observer from "./observer.js";
import Compiler from "./compiler.js";

export default class Vue {
  constructor(options) {
    this.$options = options || {};
    this.$el =
      typeof options.el === "string"
        ? document.querySelector(options.el)
        : options.el;
    this.$data = options.data || {};
    // See why there are two repeated operations here.
    // Repeat twice to convert data to reactive
    // In obsever. Js, all the attributes of data are added to the data itself in response to the getter
    // In vue. Js, all attributes of data are added to vue so that aspect operations can be accessed directly from vue instances or by using this in vue
    this._proxyData(this.$data);
    // Use Obsever to convert data into responsive form
    new Observer(this.$data);
    // Compile the template
    new Compiler(this);
  }
  // Register attributes in data to Vue
  _proxyData(data) {
    Object.keys(data).forEach((key) = > {
      // Data hijacking
      // Convert each data attribute to add to Vue into a getter setter method
      Object.defineProperty(this, key, {
        // The Settings can be enumerated
        enumerable: true.// The Settings can be configured
        configurable: true.// Get data
        get() {
          return data[key];
        },
        // Set the data
        set(newValue) {
          // Determine whether the new value is equal to the old value
          if (newValue === data[key]) return;
          // Set the new valuedata[key] = newValue; }}); }); }}Copy the code

observer.js

import Dep from "./dep.js";

export default class Observer {
  constructor(data) {
    // To iterate over data
    this.walk(data);
  }
  // Iterate over data to be responsive
  walk(data) {
    // Determine whether data is null and object
    if(! data ||typeofdata ! = ="object") return;
    / / traverse data
    Object.keys(data).forEach((key) = > {
      // Change to responsive
      this.defineReactive(data, key, data[key]);
    });
  }
  // Change to responsive
  // The difference between vue.js and vue
  // In vue. Js, attributes are given to vue to be converted into getter setters
  // Turn a property in data into a getter
  defineReactive(obj, key, value) {
    // If it is an object type, walk will be called in response. If it is not an object type, walk will be returned
    this.walk(value);
    const _this = this;
    // Create a Dep object
    let dep = new Dep();
    Object.defineProperty(obj, key, {
      // The Settings are enumerable
      enumerable: true.// The Settings are configurable
      configurable: true./ / get the value
      get() {
        // Add the observer object dep. target to represent the observer
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      / / set the value
      set(newValue) {
        // Determine whether the old value is equal to the new value
        if (newValue === value) return;
        // Set the new value
        value = newValue;
        // If newValue is an object, the properties in the object should also be set to reactive
        _this.walk(newValue);
        // Trigger notification to update viewdep.notify(); }}); }}Copy the code

compiler.js

import Watcher from "./watcher.js";

export default class Compiler {
  // VM refers to the Vue instance
  constructor(vm) {
    / / get the vm
    this.vm = vm;
    / / get the el
    this.el = vm.$el;
    // Compile the template
    this.compile(this.el);
  }
  // Compile the template
  compile(el) {
    // Get the child node if forEach traversal is used to convert the pseudo-array to a real array
    let childNodes = [...el.childNodes];
    childNodes.forEach((node) = > {
      // Compile according to different node types
      // Nodes of text type
      if (this.isTextNode(node)) {
        // Compiles the text node
        this.compileText(node);
      } else if (this.isElementNode(node)) {
        // Element node
        this.compileElement(node);
      }
      // Determine if there are still children to consider recursion
      if (node.childNodes && node.childNodes.length) {
        // Continue to compile the template recursively
        this.compile(node); }}); }// Determine if it is a text node
  isTextNode(node) {
    return node.nodeType === 3;
  }
  // Compile text nodes (simple implementation)
  compileText(node) {
    // The core idea is to find the variables inside the regular expression by removing {{}} from the regular expression
    // Go to Vue to find this variable and assign it to Node.textContent
    let reg = / \ {\ {(. +?) \} \} /;
    // Get the text content of the node
    let val = node.textContent;
    // Check whether there is {{}}
    if (reg.test(val)) {
      // get the contents of {{}}
      let key = RegExp.$1.trim();
      // Make the substitution and assign to node
      node.textContent = val.replace(reg, this.vm[key]);
      // Create an observer
      new Watcher(this.vm, key, newValue= >{ node.textContent = newValue; }); }}// Check if it is an element node
  isElementNode(node) {
    return node.nodeType === 1;
  }
  // Determine if the attribute of the element is a vue directive
  isDirective(attr) {
    return attr.startsWith("v-");
  }
  // Compile element nodes only handle instructions here
  compileElement(node) {
    // Get all the attributes above the element node! [...node.attributes].forEach((attr) = > {
      // Get the attribute name
      let attrName = attr.name;
      // check if the command starts with v-
      if (this.isDirective(attrName)) {
        // Remove v- easy to operate
        attrName = attrName.substr(2);
        // Get the value of the instruction as MSG in v-text = "MSG"
        // MSG as the key goes to Vue to find this variable
        let key = attr.value;
        // Instruction operations execute instruction methods
        // A lot of vue directives. In order to avoid a lot of if judgments, write a uapdate method here
        this.update(node, key, attrName); }}); }// Add instruction methods and execute
  update(node, key, attrName) {
    // Add textUpdater to handle v-text methods
    // We should call a built-in textUpdater method
    // It doesn't matter what suffix you add but define the corresponding method
    let updateFn = this[attrName + "Updater"];
    // The built-in method can be called if it exists
    updateFn && updateFn(node, key, this.vm[key], this);
  }
  // Specify the corresponding method in advance such as v-text
  // Use the same as Vue
  textUpdater(node, key, value, context) {
    node.textContent = value;
    // Create observer 2
    new Watcher(context.vm, key, (newValue) = > {
      node.textContent = newValue;
    });
  }
  // v-model
  modelUpdater(node, key, value, context) {
    node.value = value;
    // Create an observer
    new Watcher(context.vm, key, (newValue) = > {
      node.value = newValue;
    });
    // Two-way binding is implemented here to listen for input events to modify properties in data
    node.addEventListener("input".() = > {
      console.log('+ + + + + + + + +', node.value) context.vm[key] = node.value; }); }}Copy the code

watcher.js

import Dep from "./dep.js";

/** * Update */
export default class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    // key is the key in data
    this.key = key;
    // Call the callback function to update the view
    this.cb = cb;
    // Store the observer in dep.target
    Dep.target = this;
    // Old data should be compared when updating the view
    // The vm[key] triggers the get method
    // The observer was added to the dep.subs in get via dep.addSub(dep.target)
    this.oldValue = vm[key];
    // dep. target does not need to exist because the above operation is already saved
    Dep.target = null;
  }
  // The required methods in the observer are used to update the view
  update() {
    // Get a new value
    let newValue = this.vm[this.key];
    // Compare the old and new values
    if (newValue === this.oldValue) return;
    // Call the specific update method
    this.cb(newValue); }}Copy the code

dep.js

export default class Dep {
  constructor() {
    // Store the observer
    this.subs = [];
  }
  // Add an observer
  addSub(sub) {
    // Check whether the observer exists and whether the friend update method is used
    if (sub && sub.update) {
      this.subs.push(sub); }}// Notification method
  notify() {
    // Trigger the update method for each observer
    this.subs.forEach((sub) = >{ sub.update(); }); }}Copy the code