preface

This paper is mainly a pit writing and pit filling process, so that the audience better understand how to achieve two-way binding, two-way binding logic direction, step by step to achieve a two-way binding. This is a class tutorial article, so it’s not too hard to follow along and observe each class in detail.

start

Opening mage πŸ§™ with a dog πŸ•!!

Doesn’t seem right

Start again with a picture πŸ–Ό

As you can see from the figure, new Vue() has two steps

  1. The proxy listens to all data and interacts withDepAssociation, passDepNotify subscribers of view updates
  2. Parse all templates, subscribe to the data used in the template, and bind an update function when the data changesDepNotify the subscriber to perform the update function.

Next is the analysis of how to implement, and all need to write, first look at a section of vUE basic code, we start from the beginning of the analysis

<div id="app">
  <input v-model="message" />
  <p>{{message}}</p>
</div>
Copy the code
let app = new Vue({
    el:"#app".data: {message:"Test this is a content."}})Copy the code

is the root node of the template rendered by Vue, so if Vue wants to render the page, it needs to implement a template parsing method Compile class. In addition to parsing the template, we also need to implement the data proxy, that is, to implement the Observer class

Implement the Vue class

The Vue class is instantiated as a proxy class, and a template class is instantiated as a proxy class.

class Vue {
  constructor(options) {
    // Proxy data
    new Observer(options.data)
    // Bind data
    this.data = options.data
    // Parse the template
    new Compile(options.el, this)}}Copy the code

Next we’ll write a Compile class that parses templates, and then we’ll analyze a wave. What does a template do

  1. It is impossible for us to continue to operate directly on the DOM to parse the template, so we need to create a document fragment (virtual DOM), and then copy the template DOM node into the virtual DOM node. After the virtual DOM node is parsed, the virtual DOM node is replaced with the original DOM node

  2. After the virtual node is copied, we need to traverse the whole node tree for parsing. In the parsing process, we will traverse the ATRR attribute of DOM to find Vue related instructions. In addition, we also need to parse the content of textContent node to determine whether there are double curly braces

  3. Make a subscription to the attributes used in the resolution

Implement template parsing Compile class

We will implement this step by step

  1. buildCompileClass, the first static node and Vue instance to get out, and then define a virtual DOM properties used to store the virtual DOM
class Compile {
  constructor(el, vm) {
    // Get the static node
    this.el = document.querySelector(el);
    / / the vue instance
    this.vm = vm 
    / / virtual dom
    this.fragment = null 
    // Initialize method
    this.init()
  }
}
Copy the code
  1. Implement the initialization methodinit(), this method is mainly used to create the virtual DOM and call the method to parse the template, and then replace the DOM node into the page after parsing
class Compile { 
  / /... Omit other code

  init() {
    // Create a new blank document fragment (virtual DOM)
    this.fragment = document.createDocumentFragment()
  	// Add all child nodes to the virtual DOM
    Array.from(this.el.children).forEach(child= > {
      this.fragment.appendChild(child)
    })
    // Parse the template
    this.parseTemplate(this.fragment)
    // Add to the page after parsing
    this.el.appendChild(this.fragment); }}Copy the code
  1. Implement parsing template methodsparseTemplate, mainly to traverse all the child nodes in the virtual DOM and parse, according to the child node type for different processing.
class Compile { 
  / /... Omit other code

  // Parse the template
  parseTemplate(fragment) {
    // Get the child node of the virtual DOM
    let childNodes = fragment.childNodes || []
    // Iterate over the node
    childNodes.forEach((node) = > {
      // Matches the bracketed regular expression
      var reg = / \ {\ {(. *) \} \} /;
      // Get the text of the node
      var text = node.textContent;
      if (this.isElementNode(node)) { // Check if it is an HTML element
        // Parse HTML elements
        this.parseHtml(node)
      } else if (this.isTextNode(node) && reg.test(text)) { // Determine whether a text node has a double curly brace
        // Parse the text
        this.parseText(node, reg.exec(text)[1])}// Recursive parsing, if there are child elements continue parsing
      if(node.childNodes && node.childNodes.length ! =0) {
        this.parseTemplate(node) } }); }}Copy the code
  1. Based on the above code we found that we need to implement two simple judgments, that is, to determine whether it is an HTML element and a text element, here by obtainingnodeTypeValue to distinguish, do not understand can directly look atPortal: node.nodeTypeHere’s an extensionisVueTagMethod for use later in the code
class Compile { 
  / /... Omit other code

	// Check whether v- is carried
  isVueTag(attrName) {
    return attrName.indexOf("v-") = =0
  }
  // Check if it is an HTML element
  isElementNode(node) {
    return node.nodeType == 1;
  }
  // Determine if it is a literal element
  isTextNode(node) {
    return node.nodeType == 3; }}Copy the code
  1. implementationparseHtmlMethod, parsing HTML code primarily iterates through attr attributes on HTML elements
class Compile {
  / /... Omit other code

  / / parse HTML
  parseHtml(node) {
    // Get the set of element attributes
    let nodeAttrs = node.attributes || []
    // The set of element attributes is not an array, so it is turned into an array and iterated again
    Array.from(nodeAttrs).forEach((attr) = > {
      // Get the attribute name
      let arrtName = attr.name;
      // Check whether the name has a v-
      if (this.isVueTag(arrtName)) {
        // Get the attribute value
        let exp = attr.value;
        // Cut the string after v-
        let tag = arrtName.substring(2);
        if (tag == "model") {
          // V-model instruction processing method
          this.modelCommand(node, exp, tag) } } }); }}Copy the code
  1. implementationmodelCommandMethod, in the template parsing phase, we just bind the value of data in the Vue instance to the element, and implement the listening input method to update the data.
class Compile {
	/ /... Omit other code
  
   // Process the model directive
  modelCommand(node, exp) {
    // Get data
    let val = this.vm.data[exp]
    // Bind data while parsing
    node.value = val || ""

    // Listen for input events
    node.addEventListener("input".(event) = > {
      let newVlaue = event.target.value;
      if(val ! = newVlaue) {// Update data
        this.vm.data[exp] = newVlaue
        // Update closure data to avoid bidirectional binding failures
        val = newVlaue
      }
    })
  }
}
Copy the code
  1. The Text element is relatively easy to handletextContentReplace the content with data
class Compile {
	/ /... Omit other code
  
  // Parse the text
  parseText(node, exp) {
    let val = this.vm.data[exp]
    // Parse the update text
    node.textContent = val || ""}}Copy the code

That’s itCompileClass preliminary writing, the test results are as follows, has been able to normal parsing template

Here is the flowchart section we have implemented so far

Pit point a:

  • At 6 PMmodelCommandMethod does not implement bidirectional binding, only one-way binding, which will need to be dealt with later when bidirectional binding is required

Pit point 2:

  • 7 pointsparseTextMethod does not subscribe to data changes in the code above, so the data is bound only once during template parsing

Implement the data broker Observer class

This is mainly used to proxy all data in data. There is an Object.defineProperty method used here. If you are not familiar with this method, take a look at the document portal: document

The Observer class is primarily a method that recursively iterates through all the properties in data and then performs a data proxy

Three parameters data, key, and val are passed in defineReactive

Data and key are both parameters to Object.defineProperty, and Val uses them as a closure variable for Object.defineProperty

/ / listener
class Observer {
  constructor(data) {
    this.observe(data)
  }
  // Recursive method
  observe(data) {
    Return an empty string if the data is empty and not of type object
    if(! data ||typeofdata ! ="object") {
      return ""
    } else {
      // Iterate over data for data proxy
      Object.keys(data).forEach(key= > {
        this.defineReactive(data, key, data[key])
      })
    }
  }

  // Proxy method
  defineReactive(data, key, val) {
    // Recursive subattributes
    this.observe(data[key])
    Object.defineProperty(data, key, {
      configurable: true.// Configurable properties
      enumerable: true.// Traversable properties
      get() {
        return val
      },
      set(newValue) {
        val = newValue
      }
    })
  }
}
Copy the code

Let’s test if the data broker is successfully implemented by printing the data in the Vue constructor

class Vue {
  constructor(options) {
    // Proxy data
    new Observer(options.data)
    console.log(options.data)
    // Bind data
    this.data = options.data
    // Parse the template
    new Compile(options.el, this)}}Copy the code

As a result, we can see that the data broker has been implemented.

The corresponding flow chart is shown below

Point three:

  • The data broker is implemented here, but according to the diagram, the manager needs to be introduced to notify the manager of the data change when the data changes, and the manager will notify the subscriber to update the view, which will be discussed later in the pit filling process.

Implementation manager Dep class

Above we have implemented template parsing to initialization views, as well as data proxies. The Dep class to be implemented below is mainly used to manage subscribers and notify subscribers, here will use an array to record each subscriber, and the class will also be given a notify method to call the update method of the subscriber, to realize the notification of the subscriber update function. A target attribute is also defined to store temporary subscribers for use when joining the manager.

class Dep {
  constructor() {
    // Record subscribers
    this.subList = []
  }
  // Add subscribers
  addSub(sub) {
    // Check the presence of the subscriber to prevent repeated additions
    if (this.subList.indexOf(sub) == -1) {
      this.subList.push(sub)
    }
  }
  // Notify subscribers
  notify() {
    this.subList.forEach(item= > {
      item.update() // The subscriber performs updates, where item is a subscriber and update is a method provided by the subscriber}}})// Dep global attribute, used to temporarily store subscribers
Dep.target = null
Copy the code

With the manager implementation complete, we have implemented the following sections of the flowchart. Note the following points

  • ObservernoticeDepMostly through callsnotifymethods
  • DepnoticeWatcherBasically, it’s calledWatcherIn the classupdatemethods


Implement the subscriber Watcher class

There is relatively little subscriber code, but it is a bit difficult to understand. There are two methods implemented in the Watcher class, the Update view method and the putIn method (I have read several articles defining the GET method, probably because I don’t understand it well enough).

  • Update: Mainly incoming callscbMethod body, used to update page data
  • PutIn: Mainly used to manually join inDepManager.
/ / subscriber
class Watcher {
  // vm: vue instance itself
  // exp: The attribute name of the proxy data
  // cb: Something to do when updating
  constructor(vm, exp, cb) {
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.putIn()
  }
  update() {
    // Call the cb method body, change the this pointer and pass in the latest data as an argument
    this.cb.call(this.vm, this.vm.data[this.exp])
  }
  putIn() {
    // Bind the subscriber itself to the Dep target global property
    Dep.target = this
    // Call the method to get the data to add the subscriber to the manager
    let val = this.vm.data[this.exp]
    // Clear the global properties
    Dep.target = null}}Copy the code

Point four: the pit

  • WatcherIn the classputInMethod is not added to the manager after the constructor call, but binds the subscriber itself totargetGlobal properties

Buried pit

We have built each class with the code above, as shown in the figure below, but there are still several problematic processes, namely the potholes above. So we’re going to fill in the pits

Pits 1 and 2

To complete pit one and pit two, add the instantiated subscriber code to the modelCommand and parseText methods and customize the methods to be executed when updating the values in the page

modelCommand(node, exp) {
  
  / /... Omit other code
  
  // Instantiate the subscriber and update the node value directly
  new Watcher(this.vm, exp, (value) = > {
    node.value = value
  })
}


parseText(node, exp) {
  
  / /... Omit other code
  
  // Instantiate the subscriber and update the text content directly when it is updated
  new Watcher(this.vm, exp, (value) = > {
    node.textContent = value
  })
}
Copy the code

Buried pit 3

Pit 3 is done mainly to introduce the manager and notify the manager of the change, mainly by calling the dep.notify() method in the Object.defineProperty set method

// Listen to the method
defineReactive(data, key, val) {
  // Instantiation manager -------------- add this line
  let dep = new Dep()
  
  / /... Omit other code
  
    set(newValue) {
      val = newValue
      // Notification manager changes -------------- to add this line
      dep.notify()
    }

}
Copy the code

Buried pit 4

Complete pit four, where main four adds subscribers to the manager

defineReactive(data, key, val) {
  / /... Omit other code
    get() {
      // Add subscribers to the manager -------------- add this paragraph
      if (Dep.target) {
        dep.addSub(Dep.target)
      }
      return val
    },
  / /... Omit other code
}
Copy the code

Target = Dep target = Dep target = Dep target = Dep target

Now that we’ve implemented a simple two-way binding, let’s test it out

End scatter flower πŸ’πŸ΅οΈŽπŸŒΉπŸŒΊπŸŽ‰πŸŽ‰πŸŽ‰πŸŽ‰πŸŽ‰

conclusion

This article does not explain much, so it is a kind of tutorial article, if readers do not understand the place can be in the comments to leave a message to discuss