preface

We may often wonder how Vue implements data updates and view changes. The view changes and the data can be updated as well.

Response principle

So, let’s look at the following picture first:

Overall process:

  1. We need to create a VUE object. Option to record the parameters of the options passed through _proxyData data attributes in the injection Vue instances, you can pass this. Call XXX
  2. Create an observer for data hijacking to convert properties of data to GET and set. When the data changes, trigger the change and tell the Dep: I have changed the data and need to call your notify method
  3. And the notify method of Dep is going to call the updater method of Watcher, which is going to send a notification saying, my data has changed, you’re updating the view, and Watcher’s updater is going to go back and update the view
  4. When we create a Watcher object, we also add the current Watcher object to the SUBs array of the Dep. That is, collect the dependencies and let Dep record them
  5. After the observer is created, Compiler objects are also created to parse instructions, differential expressions, etc. When the page is loaded for the first time, methods in Compiler are called to update the view. Also, subscribe to data changes (Dep notifies Watcher when data changes) and bind the update view (the callback function passed when Watcher was created to update the view).
  6. In general: The Compiler updates the view when the page is first loaded, and Watcher updates the view when the data changes

Create a VUE object

Record the parameter options, through _proxyData data attributes in the injection Vue instances, can pass this. Call XXX

class Vue {
  constructor(options) {
    // 1. Save the data of the option through properties
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    // 2. Convert data members into getters and setters and inject them into vue instances
    this._proxyData(this.$data)
    // 3. Call an Observer to listen for data changes
    new Observer(this.$data)
    // 4. Invoke the Compiler object to parse instructions and differential expressions
    new Compiler(this)
  }
  _proxyData (data) {
    // Iterate over all attributes in data
    Object.keys(data).forEach(key= > {
      // This is injected into the vue instance for later use
      Object.defineProperty(this, key, {
        enumerable: true.configurable: true.get() {
          return data[key]
        },
        set(newval) {
          if (newval === data[key]) return
          data[key] = newval
        }
      })
    })
    // Inject the data attribute into the vue instance}}Copy the code

Create the observer

It’s data hijacking, converting properties in data to GET and set

class Observer {
  constructor(data) {
    this.walk(data)
  }
  walk(data) {
    // 1. Determine whether data is an object to ensure code robustness
    if(! data ||typeofdata ! = ='object') return
    // 2. Iterate over all properties of the data object
    Object.keys(data).forEach(key= > {
      this.defineReactive(data, key, data[key])
    })
  }
  defineReactive(obj, key, val) {
    const that = this
    // Collect dependencies and send notifications
    let dep = new Dep()
    // If val is an object, recursive traversal is required
    this.walk()
    // This.$data is injected into this
    Object.defineProperty(obj, key, {
      enumerable: true.configurable: true.get() {
        // Collect dependencies
        Dep.target && dep.addSub(Dep.target)
        // if obj[key] is passed, the get method will be triggered, which will cause stack overflow
        return val
      },
      set(newval) {
        if (newval === val) return 
        val = newval
        that.walk()
        // Send notifications
        dep.notify()
      }
    })
  }
}
Copy the code

Create Dep:

Store all observers and send notifications

class Dep {
  constructor () {
    // Store all observers
    this.subs = [] 
  }
  // Add an observer
  addSub (sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // Send notifications
  notify() {
    this.subs.forEach(sub= > {
      sub.update()
    })
  }
}
Copy the code

Create a Watcher object:

When the data changes, the callback function is called to update the view

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm
    // Attributes in data
    this.key = key
    // Call back to update the view
    this.cb = cb

    // Record the watcher object to the Dep static property target
    Dep.target = this
    // Trigger the get method, in which addSub is called
    this.oldValue = vm[key]
    Dep.target = null // Needs to be reset to prevent future reassignments
  }

  // Update the view when data changes
  update() {
    // when update is called, the data has changed, so newValue is the newValue
    let newValue = this.vm[this.key]
    if (newValue === this.oldValue) return 
    this.cb(newValue)
  }
}
Copy the code

Create the compiler:

Compile templates, work with text nodes and element nodes

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
    Array.from(childNodes).forEach(node= > {
      if (this.isTextNode(node)) {
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        this.compileElement(node)
      }
      // Check whether the current node has child nodes, if so, recurse
      const flag = node.childNodes && node.childNodes.length
      if (flag) this.compile(node)
    })
  }

  // Compile element node, process instruction
  compileElement(node) {
    // Iterate over all attribute nodes
    Array.from(node.attributes).forEach(attr= > {
      // Determine if it is a command
      let attrName = attr.name
      if (this.isDirective(attrName)) {
        attrName = attrName.substr(2) // The suffix name of the directive
        const key = attr.value
        this.update(node, key, attrName)
      }
    })
  }

  // Process all instructions
  update(node, key, attrName) {
    const updateFn = this[attrName + 'Updater']
    updateFn && updateFn.call(this, node, this.vm[key], key)
  }

  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
    })

    // Bidirectional binding
    node.addEventListener('input'.() = > {
      console.log(value, node.value)
      // Assign a value to the current form
      this.vm[key] = node.value
    })
  }



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

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

  // Determine if the element attribute is an instruction
  isDirective (attrName) {
    return attrName.startsWith('v-')}// Determine whether it is a text node
  isTextNode(node) {
    return node.nodeType === 3
  }

  // Determine if it is an element node
  isElementNode(node) {
    return node.nodeType === 1}}Copy the code

Two-way binding

  • With the reactive principle explained above, the principle of bidirectional binding is easy to understand
  • For example, for forms, bind an input event to the form, obtain the changed value of the view through Node. value, reassign the value to the attribute value on the VUE object, and then trigger the reactive mechanism to complete the bidirectional binding