This is the 10th day of my participation in Gwen Challenge

This article is about Vue source study notes, here to make a summary and share, there are shortcomings also hope to be corrected ~

Learning goals

I wrote a simple Vue library to realize the recognition of double braces, two-way binding of data (processing of V-model instructions) and the realization of Watch

Handwritten implementation

Vue class creation

When we create a new Vue instance, we do the following six things (simple version)

  1. The options object that is passed to the Vue instance by the user is assigned to the instance itself for easy access by the user
  2. In the optionsdataAdded to the Vue instance_dataAttribute to facilitate subsequent operations
  3. thedataAdd Vue data to Vue instance for easy user access
  4. thedataChange the data to reactive
  5. call_initWatch()Handle user definitions in optionswatchThe contents of an object (similarly, the contents defined in a lifecycle function, e.gcreatedConstructor ()
  6. Compile the incoming EL content

Note that the import observe and Watcher in the following files are the previous ones. Please refer to Data Responsive Principles-01, Data Responsive Principles-02 and Data Responsive Principles-03 for details.

// Vue.js
import Compile from './Compile.js'
import observe from './observe.js'
import Watcher from './Watcher.js'

export default class Vue {
  constructor(options) {
    this.$options = options || {} // Let the user who created the Vue instance get options
    this._data = options.data || undefined
    // Initialize data defined on options
    this._initData()
    // Data becomes responsive data
    observe(this._data)
    // Handle the user-defined watch in options
    this._initWatch() 
    // Template compilation
    new Compile(options.el, this)}// Define data to vue instances so that attributes can be obtained directly from vm.xx in index. HTML
  _initData() {
    Object.keys(this._data).forEach(item= > {
      Object.defineProperty(this, item, {
        configurable: true.enumerable: true.get() {
          return this._data[item]
        },
        set(newVal) {
          this._data[item] = newVal
        }
      })
    })
  }

  _initWatch() {
    const watch = this.$options.watch
    Object.keys(watch).forEach(item= > {
      // The first argument can be this, which is the vue instance
      // this is because _initData already defines all the data in the data to the vue instance
      new Watcher(this, item, watch[item])
    })
  }
}
Copy the code

The fragments generated

In fact, vUE does this with the help of the AST

For an introduction to AST, see the AST Abstract Syntax Tree

For convenience, we use Document Fragments for compilation processing, which is better than manipulating the real DOM directly. The code looks like this, putting all the nodes of the passed EL into the document fragment.

node2Fragment(el) {
  const fragment = document.createDocumentFragment()
  let child
  while (child = el.firstChild) {
    fragment.appendChild(child)
  }
  return fragment
}
Copy the code

Then we compile the document fragment, fetch all the child nodes of the string template (the content of the EL), check whether they are element nodes (call compileElement) or text nodes by nodeType, and if they are text nodes, check whether they are the contents of the double curly braces. If so, call compileText

compile(el) {
  const nodeList = el.childNodes // The result is a NodeList collection, not an array, but can be iterated with forEach()
  const regExp = / \ {\ {(. *) \} \} / // Get mustache content
  nodeList.forEach(item= > {
    const text = item.textContent // All items retrieved are nodes, not strings, so we use.textContent to retrieve text
    if (item.nodeType === 1) { // Element node
      this.compileElement(item)
    } else if (item.nodeType === 3 && regExp.test(text)) { // Text node
      const dataName = text.match(regExp)[1].trim()
      this.compileText(item, dataName)
    }
  })
}
Copy the code

V – the realization of the model

V-models are usually bound to the input. Handling v-models is done in the compileElement method. Node. attributes collect all the attributes of the element node and pass them through the array. See if the attribute name starts with a V -, if it does, the attribute is an instruction. See if v- is model. If so, add watcher to the value of this property and look for the corresponding value in data. Assign the value to the input. Listen for the user to change the input value and change the property value in data in real time.

To sum up, Compile. Js file is as follows

// Compile.js
import Watcher from './Watcher.js'

export default class Compile {
  constructor(el, vue) {
    this.$vue = vue / / the vue instance
    this.$el = document.querySelector(el) / / the mount point
    // If the user uploads a mount point
    if (this.$el) {
      // Call the function to turn nodes into fragments, like mustache
      // Vue source code actually uses ast
      const $fragment = this.node2Fragment(this.$el)
      // Compile the fragment, which is better than compiling the actual DOM node (this.$el) directly
      this.compile($fragment)
      // Add the document fragment to the DOM tree
      this.$el.appendChild($fragment)
    }
  }
  node2Fragment(el) {
    const fragment = document.createDocumentFragment()
    let child
    while (child = el.firstChild) {
      fragment.appendChild(child)
    }
    return fragment
  }

  compile(el) {
    const nodeList = el.childNodes // The result is a NodeList collection, not an array, but can be iterated with forEach()
    const regExp = / \ {\ {(. *) \} \} / // Get mustache content
    nodeList.forEach(item= > {
      const text = item.textContent // All items retrieved are nodes, not strings, so we use.textContent to retrieve text
      if (item.nodeType === 1) { // Element node
        this.compileElement(item)
      } else if (item.nodeType === 3 && regExp.test(text)) { // Text node
        const dataName = text.match(regExp)[1].trim()
        this.compileText(item, dataName)
      }
    })
  }

  compileElement(node) {
    const attrs = node.attributes Attrs is a NamedNodeMap object, not an array
    / / Array. Prototype. Slice. Call (attrs) to convert attrs Array
    // Slice returns a new array object, a shallow copy of the original array; Call allows Attrs to use array. prototype's slice method
    Array.prototype.slice.call(attrs).forEach(item= > {
      // Get name and value
      const name = item.name 
      const value = item.value
      if (name.indexOf('v-') = = =0) { // Attributes that begin with v- are directives
        const directiveName = name.substring(2)
        if (directiveName === 'model') {
          // Add watcher to respond in real time whenever the value of this property in the V-Model binding is changed
          new Watcher(this.$vue, value, newVal= > {
            node.value = newVal
          })
          // Get the value of this value in data
          const v = this.getDataValue(this.$vue, value)
          // We only deal with the case where the node is input, so we use the value attribute directly
          node.value = v
          // If the input value changes
          node.addEventListener('input'.e= > {
            // Change the value of the property in data to the value of the input
            this.setDataValue(this.$vue, value, e.target.value)
          })
        }
      }
    })
  }

  compileText(node, dataName) {
    // Get the value of the variable in data and put it in the corresponding node
    node.textContent = this.getDataValue(this.$vue ,dataName)
    new Watcher(this.$vue, dataName, newVal= > {
      node.textContent = newVal
    })
  }

  getDataValue(obj, dataName) {
    const nameArr = dataName.split('. ')
    const value = nameArr.reduce((acc, cur) = > {
      return acc[cur]
    }, obj)
    return value
  }
  
  // Set the value of a property in data
  setDataValue(obj, dataName, dataValue) {
    const nameArr = dataName.split('. ')
    let val = obj
    nameArr.forEach((item, index, arr) = > {
      if (index < arr.length - 1) {
        val = val[item]
      } else {
        val[item] = dataValue
      }
    })
  }
}
Copy the code

One More Thing

DocumentFragment introduction

DocumentFragments are DOM nodes. They are not part of the main DOM tree. The usual use case is to create a document fragment, attach elements to the document fragment, and then attach the document fragment to the DOM tree. In the DOM tree, the document fragment is replaced by all of its child elements.

Because the document fragment exists in memory, not in the DOM tree, inserting child elements into the document fragment does not cause page backflow (calculations of element position and geometry). Therefore, using document fragments generally results in better performance.