review

Before the source code implementation, a brief review of Vue. Vue is a responsive data framework that can be structured into an MVVM framework project, where views are separated from business logic and then manipulated with a data model, which avoids a lot of DOM manipulation and allows developers to focus more on business logic. Use a simple example to illustrate this concept.

<! DOCTYPE html> <html lang="cn"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, Initial =1.0"> <meta http-equiv=" x-UA-compatible "content=" IE =edge"> <title>Vue infrastructure </title> </head> <body> <div Id = "app" > < h1 > difference expression < / h1 > < h3 > {{MSG}} < / h3 > < h3 > {{count}} < / h3 > < h1 > v - text < / h1 > < div v - text = "MSG" > < / div > <h1>v-model</h1> <input type="text" v-model="msg"> <input type="text" v-model="count"> </div> <script src="./js/vue.js"></script> <script> let vm = new Vue({ el: '#app', data: { msg: 'Hello Vue', count: 20, items: ['a', 'b', 'c'] } }) </script> </body> </html>Copy the code

The above example is a simple vUE example. Any developer who has used VUE knows that in the browser, interpolation expressions in HTML such as {{MSG}}, {{count}} are replaced with data in vue. Data. If we modify the data in data, the corresponding values in the page are also changed. The element with V-Model will achieve bidirectional binding. If the data is modified in the page, the corresponding data in the VUE will be modified, and the data in the VUE will be modified, and the value display of the page will be changed. That’s the data response.

The source code to achieve

vue

We are now ready to implement a simple VUE framework. We want the framework to be able to respond to data as well. First we create a Vue class and its constructor takes an object.

class Vue {
  constructor(options){
    this.$options = options || {}
}
} 
Copy the code

Vue then parses the Options object, taking options.el and options.data as the values of el and el and el and data properties

class Vue {
  constructor(options){
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
}
} 
Copy the code

If el is a string, use it to find the corresponding element in the DOM tree. Another point is that we can get data directly from VUE, which means that VUE already has properties in Data, so we need to write a method to inject the properties of Data into the Vue instance.

_proxyData (data) {
    Object.keys(data).forEach(key= > {
      Object.defineProperty(this, key, {
        enumerable: true.configurable: true,
        get () {
          return data[key]
        },
        set (newValue) {
          if (newValue === data[key]) {
            return
          }
          data[key] = newValue
        }
      })
    })
  }
}
Copy the code

Object. DefineProperty Constructs a property of the class. The first argument is the injected class, in this case Vue, the key is the property name, which is the property in data, and the last argument is the constructed property. In a nutshell, the method is to inject the properties of data into the Vue instance, and then add getters and setters for each property. Finally, call it in the constructor:

class Vue {
  constructor(options){
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    this._proxyData(this.$data)
}
}
Copy the code

observer

However, Vue now only has getters and setters for data; the data itself is not reactive. How can I set the data to be responsive? . In responsivity, when data is modified, the location of the previously received data is changed accordingly. This also means that one is when receiving data somewhere, Vue to put a “receiver” in this place, and record the location, if the source data changes, also should make corresponding action “receiver”, 2 it is to modify the data, to be a “signal transmitter”, is notified to all the “receiver”, it has modified the data.

This is called the observer model. Draw a graph to show the relationship above:

An observer may have one or more, and it or they observe whether the publisher changes and, if so, make changes.

The Observer class code looks like this:

class Observer {
  constructor (data) {
    this.walk(data)
  }
  walk (data) {
    Object.keys(data).forEach(key= > {
      this.defineReactive(data, key, data[key])
    })
  }
  defineReactive (obj, key, val) {

    let dep = new Dep()

    Object.defineProperty(obj, key, {
      enumerable: true.configurable: true,
      get () {

        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set (newValue) {
        if (newValue === val) {
          return
        }
        val = newValue
        dep.notify()
      }
    })
  }
}
Copy the code

The above code is similar to the previous _proxyData code in that the Observer class takes a data object and injects getters and setters into it, except that there is a Dep class, regardless of how it is implemented. It plays the role of signal receiver/transmitter, and through closures, getters and setters can reference the Dep class. Dep.target && dep.addSub(dep.target) is the code that records the “position” used by the getter, and dep.notify() is the “emitted signal”.

I’m just going to briefly talk about what these two pieces of code do for now, and I’ll go into more detail about what they do later. The Observer is basically implemented, but there are still some problems. What if the value of the data property is an object? The code above only implements a reactive effect on raw data, or in the case of an object, only references to it, so use recursion.

  walk (data) {
    if(! data ||typeofdata ! = ='object') {
      return
    }

    Object.keys(data).forEach(key= > {
      this.defineReactive(data, key, data[key])
    })
  }

  defineReactive (obj, key, val) {
    let that = this
    let dep = new Dep()
    this.walk(val)
    Object.defineProperty(obj, key, {
      enumerable: true.configurable: true,
      get () {
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set (newValue) {
        if (newValue === val) {
          return
        }
        val = newValue
        that.walk(newValue)
        dep.notify()
      }
    })
  }
Copy the code

In defineReactive, the first walk makes the object reactive, while the second walk makes the value reactive recursively considering that the changed value might be an object.

We refer to an Observer instance with that because when getters and setters are called, this does not refer to an Observer.

Dep

Dep specifically stores the observer, and when data is modified, it calls the methods of the stored observer.

class Dep {
  constructor () {
    this.subs = []
  }

  addSub (sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }

  notify () {
    this.subs.forEach(sub= > {
      sub.update()
    })
  }
}
Copy the code

The implementation of Dep is simple: store and call.

Compiler

Before building the observer class, we need to consider where the observer actually appears, otherwise we won’t know how to write the class. Once again, review Vue. We write vUE code, mount the VUE class to an element in the DOM tree, and reference vUE data or methods somewhere in the HTML. So we need a class to process the page, change the location of the reference to the vUE’s data, and add observers to the reference node to make the data responsive.

How do I write this class? {{MSG}} or

class Compiler {
  constructor (vm) {
    this.el = vm.$el
    this.vm = vm
    this.compile(this.el)
  }
  // Compile templates to handle text nodes and element nodes
  compile (el) {
    let childNodes = el.childNodes  // Get the child node
    Array.from(childNodes).forEach(node= > {  / / traverse
      // Process text nodes
      if (this.isTextNode(node)) {
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        // Process element nodes
        this.compileElement(node)
      }

      // Determine whether node has children. If there are children, recursively call compile
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }
Copy the code

In simple terms, the DOM tree is traversed, divided into cases, if the child node has its own node, then recursive processing.

Then the above code number method implementation. Implement the judgment methods first, because they are the easiest.


  isDirective (attrName) {
    return attrName.startsWith('v-')
  }

  isTextNode (node) {
    return node.nodeType === 3
  }

  isElementNode (node) {
    return node.nodeType === 1
  }
Copy the code

IsDirective determines whether an element attribute is a vue directive. If the attribute begins with ‘V -‘, isTextNode determines whether the element is a text node, and isElementNode determines whether the element is an element.

Fetch the node’s text, find the variable that references vue data, and replace it.

  compileText (node) {
    let reg = / \ {\ {(. +?) \} \} /
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.vm[key])
    }
  }
Copy the code

Use regular expressions to find vUE data references and replace them with VUE data.

CompileElement is tricky because it handles different instructions. The most direct way to do this is to use if, get the vue instruction, and call different methods depending on the instruction, but writing this will produce a lot of if… else if.. In fact, we can use the properties of the object itself. Since the instruction itself is a string, we can directly use the instruction to call the relevant method, that is, directly use this[” XXX “] to call the method.

  compileElement (node) {
    Array.from(node.attributes).forEach(attr= > {
      let attrName = attr.name
      if (this.isDirective(attrName)) {
        attrName = attrName.substr(2)
        let key = attr.value
        this.update(node, key, attrName)
      }
    })
Copy the code

Iterate through the node properties to see if there are any vUE directives. If there are, take out the ‘V -‘ suffix and put the node, attribute values and directive names into the update function as parameters.

Update centrally handles vUE directives.

  update (node, key, attrName) {
    let updateFn = this[attrName + 'Updater']
    updateFn && updateFn.call(this, node, this.vm[key], key)
  }
Copy the code

See if there are any instruction related methods and call them if there are. Here we simply implement v-text and V-model methods.

  // Process the V-text instruction
  textUpdater (node, value, key) {
    node.textContent = value
  }

  // v-model
  modelUpdater (node, value, key) {
    node.value = value

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

ModelUpdater is a two-way binding. On the one hand, when the value of vue is changed, the value of input also needs to be changed, so node.value = value. On the other hand, when the value of input is changed, the data of Vue must be changed. So the relevant input element registers the event and calls the callback function when the input occurs, so

    node.addEventListener('input'.() = > {
      this.vm[key] = node.value
    })
Copy the code

The Compiler class is almost complete. However, if you test now and change the data in the VUE, the view will not change, because the node has no observer and cannot receive signals that the data has changed. So we have to add observers at the same time we compile.

Watcher

Finally started writing about observers. Before writing, we add Watcher to the Compiler class at its compile location. The question is, what parameters do we pass to Watcher? Or should we wonder, what does Watcher really need to know? It must know its location first, so it must have node references, and then vue instances, otherwise how can it be observed? There must also be a data property to observe, and finally a callback function to call when the data changes. Put Watcher in place and implement it later.

  textUpdater (node, value, key) {
    node.textContent = value
    new Watcher(this.vm, key, (newValue) = > {
      node.textContent = newValue
    })
  }
 

  modelUpdater (node, value, key) {
    node.value = value
    new Watcher(this.vm, key, (newValue) = > {
      node.value = newValue
    })
    node.addEventListener('input'.() = > {
      this.vm[key] = node.value
    })
  }

  compileText (node) {
    let reg = / \ {\ {(. +?) \} \} /
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.vm[key])

      new Watcher(this.vm, key, (newValue) = > {
        node.textContent = newValue
      })
    }
  }
Copy the code

The argument to the Watcher class is nothing but a reference to the node in the callback function. Next, high energy ahead, high energy ahead, high energy ahead. First, the code, then the picture, to explain.

class Watcher {
  constructor (vm, key, cb) {
    this.vm = vm
    // Attribute name in data
    this.key = key
    // The callback is responsible for updating the view
    this.cb = cb

    Dep.target = this
    this.oldValue = vm[key]
    Dep.target = null
  }
  // Update the view when data changes
  update () {
    let newValue = this.vm[this.key]
    if (this.oldValue === newValue) {
      return
    }
    this.cb(newValue)
  }
}
Copy the code

Forget the last three lines of the constructor and go back to the Dep class:

class Dep {
  constructor () {
    this.subs = []
  }

  addSub (sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }

  notify () {
    this.subs.forEach(sub= > {
      sub.update()
    })
  }
}
Copy the code

The Dep class manages the Watcher methods. When a view references vue data, it constructs a Watcher, and the Watcher tells the associated Dep to store itself in an array. When the data is modified, the Dep traverses the array and calls the Update method of the Watcher instance. The question now is when exactly does the so-called “relevant Dep” appear? orz Let’s go back to the Observer class and look at its defineReactive method:

  defineReactive (obj, key, val) {

    let dep = new Dep()

    Object.defineProperty(obj, key, {
      enumerable: true.configurable: true,
      get () {

        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set (newValue) {
        if (newValue === val) {
          return
        }
        val = newValue
        dep.notify()
      }
    })
  }
}
Copy the code

Notice in the second line that a Dep instance is generated when the data for each Vue instance becomes reactive. When the getter method is called, that is, when vue data is referenced in the Compiler class, the Dep instance checks for a static property of the DEP.target and stores it in an array if it does. When did this target appear? When a Watcher instance is constructed, it is the last three lines of the Watcher constructor:

    Dep.target = this
    this.oldValue = vm[key]
    Dep.target = null
Copy the code

Generate a static property for Target that refers to the current Watcher instance, and then oldValue for the instance that refers to vUE related data. This invokes the getter for the related data, the getter for the related data, and the getter for the related data (important things three times). The getter for the related data stores the Watcher instance (dep.target) into the array of the Dep.

Draw and explain:

Generate Watcher instance, dep. target = this

Finally, change dep. target to NULL to prevent references elsewhere. The process is a bit convoluted, read it a few times, or on paper to sort out the program’s references.

Setters that modify data take the Watcher instances in the DEP and call their update method.

Finally, refactor the vue constructor:

  constructor (options) {
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    this._proxyData(this.$data)
    new Observer(this.$data)
    new Compiler(this)}Copy the code