The preparatory work

To get a sense of these:

  • Data driven
  • Responsive core principles
  • Publish subscribe mode and observer mode

Data driven

  • Data responsiveness: The data model is a normal JS object, but when the data is modified, the view is updated with it, avoiding manual work by the developerDOMoperation
  • Two-way data binding: When data changes, so does the view; As the view changes, so does the data. Embodiment is for form elements, you can usev-modelDirective to create a two-way data binding
  • Data-driven: Development only needs to focus on the data itself, rather than how the data should be rendered

Responsive core principles

In VUe2, responsivity is implemented based on Object.defineProperty:

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">msg</div>
<script>
  let data = {
    msg: 'hello world'
  }

  let vm = {}

  Object.defineProperty(vm, 'msg', {
    / / can be enumerated
    enumerable: true./ / can be configured
    configurable: true.// this is triggered when reading vm. MSG and data. MSG is read
    get() {
      return data.msg
    },
    // This is triggered when the value of vm. MSG is set, and dom manipulation can be done here to modify the view
    set(v) {
      if (v === data.msg) return
      data.msg = v
      document.getElementById('app').textContent = data.msg
    }
  })
  // When the vm. MSG is reassigned, the view will also re-render the new value
</script>
</body>
</html>
Copy the code

This method requires traversing properties in the object to set the getter and setter

In VUe3, you use a proxy, which listens on the entire object rather than adding a getter and setter to each property separately.

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">msg</div>
<script>
    const data = {
      msg: 'hello world'.count: 0
    }

    // proxy is a constructor
    // The first parameter is the proxied object and the second parameter is the handler
    // new returns the proxy object
    const vm = new Proxy(data, {
      get(target, key) {
        return target[key]
      },
      set(target, key, value) {
        if (target[key] === value) return
        target[key] = value
        document.getElementById('app').textContent = target[key]
      }
    })
</script>
</body>
</html>
Copy the code

Publish subscribe mode and observer mode

  1. Publish subscribe pattern: This pattern has three objects: publisher, subscriber, and message center (event center). When an event is triggered, the publisher passes a signal to the message center, and all tasks subscribed to the message center begin to execute upon receiving the signal.

In VUE, you can experience the publish-subscribe model by customizing events:

// Create an empty vue instance
// Message center
const vm = new Vue()

/ / subscriber
vm.$on('change'.() = > {
    console.log('Event triggerred')})/ / publisher
vm.$emit('change')
Copy the code

Sibling components in VUE can communicate using this method

The custom event mechanism can be easily simulated:

class EventEmitter {
    This class contains two methods, $emit and $ON, as well as an object that can hold published events
    constructor() {
      // Create the object that holds the event
      this.subs = {}
    }

    $on- subscriber
    // Type is the event name and handler is the event handler function
    $on(type, handler) {
      // Determine if the event is present in subs, and if not, assign it to an empty array to store multiple events
      this.subs[type] = this.subs[type] || []
      this.subs[type].push(handler)
    }

    // $emit- publisher$emit(type, ... args) {// Execute the event if it exists
      if (this.subs[type]) {
      this.subs[type].forEach(handler= >{ handler(... args) }) } } }Copy the code

Let’s test it again:

const event = new EventEmitter()

event.$on('change'.(arg1, arg2) = > {
  console.log(arg1, arg2)
})

event.$emit('change'.1.2)
Copy the code

There will also be printing on the console

  1. The Observer pattern: Compared to the publish-subscribe pattern, the Observer pattern has fewer message centers. Within the publisher (target)subsArray to store all the subscribers (observers),addSubFunction add observer,notifyFunction to call the subscriber (observer) when the event is triggeredupdateFunction to perform the corresponding task.That is, unlike the “zero coupling” of publisher and subscriber in the publish-subscribe model, there is a dependency between target and observer in the observer model.

Simple implementation (no consideration of parameter passing)

// target - publisher
class Dep {
  constructor() {
    // Initialize an array of subs to store observers
    this.subs = []
  }
  // addSub method - adds the observer to subs
  addSub(sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // notify - Call the update method for all observers
  notify() {
    this.subs.forEach(sub= > {
      sub.update()
    })
  }
}
// Observer - subscriber
class Watcher {
  update() {
    // The corresponding task for the observer to perform
    console.log('updated')}}Copy the code

Test it out:

const dep = new Dep()
const watcher = new Watcher()

dep.addSub(watcher)
dep.notify()
Copy the code

To sum up:

The observer mode is scheduled by the target, and there is a dependent relationship between the observer and the target. The publish subscribe model is uniformly scheduled by the message center, with no connection between publishers and subscribers.

Simulate the Vue responsive principle

The overall structure is as follows

Now implement these five aspects step by step (mini-version)

Vue

Main functions of Vue:

  1. Receives initialization parameters
  2. The data in data is injected into the VUE instance and converted into getters and setters
  3. Call observer to listen for changes in data
  4. The Compiler is called to resolve the difference expressions and instructions

Take a look at the class diagram:

Implement it according to the function:

class Vue {
  constructor(options) {
    // Save the data in options
    // 1. Save options to $options
    this.$options = options || {}
    // 2. Save options.data to $data
    this.$data = options.data || {}
    // 3. Save options.el to $el
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el

    // Convert data into getters and setters and inject them into vue instances
    this._proxyData(this.$data)
    // Call observer to monitor data changes
    // Call compiler to parse instructions and differential expressions
  }
  _proxyData(data) {
    / / traverse data
    Object.keys(data).forEach(key= > {
      // Inject attributes from data into vue instances
      Object.defineProperty(this, key, {
        enumerable: true.configurable: true.get() {
          return data[key]
        },
        set(newVal) {
          if (newVal === data[key]) return
          data[key] = newVal
        }
      })
    })
  }
}
Copy the code

Also reference this in the template HTML file:

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</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>
    const vm = new Vue({
      el: '#app'.data: {
        msg: 'hello'.count: 1}})console.log(vm)
</script>
</body>
</html>
Copy the code

The printed result is

See that $data, $EL, $options and getters and setters are already in the vue instance.

Observer

The Main functions of the Observer:

  1. Convert properties in data to responsive data (if a property is also an object, convert it to responsive object)
  2. Send notifications when data changes (with observer mode)

Observer class diagram:

Implementation:

class Observer {
  constructor(data) {
    // Call walk to convert the getter setter
    this.walk(data)
  }
  walk(data) {
    // Determine if data is an object
    if(! data ||typeofdata ! = ='object') return
    // Iterate over the attributes in data
    Object.keys(data).forEach(key= > {
      this.defineReactive(data, key, data[key])
    })
  }
  defineReactive(obj, key, val) {
    // Call the walk method on a value to fix the bug that val cannot be converted to a getter for its internal properties when it is an object
    this.walk(val)
    Object.defineProperty(obj, key, {
      enumerable: true.configurable: true.get() {
        return val
      },
      set(newVal) {
        if (newVal === val) return
        val = newVal
        // When assigning a value to a property in data, if the value is an object, convert its internal property to a getter setter
        that.walk(newVal)
        // Send notifications}}}})Copy the code

DefineReactive returns val directly when converting the getter because if obj[key] is returned, a recursive call is generated and an infinite loop is generated:

Also create an Observer instance in vue.js and pass in $data

  constructor(options) {
    // Save the data in options
    // 1. Save options to $options
    this.$options = options || {}
    // 2. Save options.data to $data
    this.$data = options.data || {}
    // 3. Save options.el to $el
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el

    // Convert data into getters and setters and inject them into vue instances
    this._proxyData(this.$data)
    // Call observer to monitor data changes
    new Observer(this.$data)
    // Call compiler to parse instructions and differential expressions
  }
Copy the code

In the HTML file, we introduce observer.js before we introduce vue.js, open the browser and print the VM to see that the properties in $data have been converted to getters and setters

Compiler

Compiler’s main functions:

  1. Compile templates, parse instructions, and differential expressions
  2. Render the page and render the data again after the data is updated

Class diagram:

Implementation:

class Compiler {
  // The constructor receives the vue instance
  constructor(vm) {
    this.el = vm.$el
    this.vm = vm
    this.compiler(this.el)
  }

  // Compile templates, handle text nodes and element nodes
  compiler(el) {
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node= > {
      // Determine the node type and operate on the corresponding node
      if (this.isTextNode(node)) {
        this.compilerText(node)
      } else if (this.isElementNode(node)) {
        this.compilerElement(node)
      }
      // Determine whether there are child nodes, if there are recursive
      if (node.childNodes && node.childNodes.length) {
        this.compiler(node)
      }
    })
  }

  // Process element nodes
  compilerElement(node) {
    // Iterate over all attributes
    Array.from(node.attributes).forEach(attr= > {
      let attrName = attr.name
      // Determine if this attribute is an instruction
      if (this.isDirective(attrName)) {
        attrName = attrName.substr(2)
        let key = attr.value
        this.update(node, key, attrName)
      }
    })
  }

  update(node, key, attrName) {
    const updateFn = this[attrName + 'Updater']
    updateFn && updateFn(node, this.vm[key])
  }
  textUpdater(node, value) {
    node.textContent = value
  }
  modelUpdater(node, value) {
    node.value = value
  }

  // Process text nodes
  compilerText(node) {
    const reg = / \ {\ {(. +?) \} \} /
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      // replace {{XXX}} with the value of the variable
      node.textContent = value.replace(reg, this.vm[key])
    }
  }

  // Determine if it is a command
  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

Dep

Function:

  1. Collect dependencies and add observers
  2. Notify all observers

The class diagram

implementation

class Dep {
  constructor() {
    // subs is used to store all observers
    this.subs = []
  }

  // Add an observer
  addSubs(sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }

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

Dep.js is introduced at the top of the HTML file import JS file, then a Dep object is created in the defineReactive method in the Observer class, and dependencies are collected and notifications are sent in the GET and set methods

  defineReactive(obj, key, val) {
    // Call the walk method on a value to fix the bug that val cannot be converted to a getter for its internal properties when it is an object
    this.walk(val)
    const that = this

    // Create a DEP object to collect dependency + send notifications
    const dep = new Dep()
    Object.defineProperty(obj, key, {
      enumerable: true.configurable: true.get() {
        // Collect dependencies
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set(newVal) {
        if (newVal === val) return
        val = newVal
        // When assigning a value to a property in data, if the value is an object, convert its internal property to a getter setter
        that.walk(newVal)
        // Send notifications
        dep.notify()
      }
    })
  }
Copy the code

watcher

Watcher and DEP diagram:

Function:

  1. When data changes trigger dependencies, DEP notifies all Watcher instances to update the view
  2. Add yourself to the DEP object when instantiating itself

The class diagram

implementation

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm
    // The attribute name in data
    this.key = key
    // Update the view callback function
    this.cb = cb

    // Log the watcher instance to the Dep static attribute target
    Dep.target = this
    // Old value -- This triggers the get method, which calls addSubs
    this.oldValue = vm[key]
    / / clear the target
    Dep.target = null
  }
  // Update the view
  update() {
    let newValue = this.vm[this.key]
    if (newValue === this.oldValue) return
    this.cb(newValue)
  }
}
Copy the code

Then modify the text node processing method and instruction processing method in the Compiler class:

  // Process text nodes
  compilerText(node) {
    const reg = / \ {\ {(. +?) \} \} /
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      // replace {{XXX}} with the value of the variable
      node.textContent = value.replace(reg, this.vm[key])

      // Create a watcher instance
      new Watcher(this.vm, key, newValue= > node.textContent = newValue)
    }
  }

  update(node, key, attrName) {
    const updateFn = this[attrName + 'Updater']
    // Use call to change the direction of this inside a method call to the current Compiler instance
    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)
  }
Copy the code

This way, when the data changes, the view updates with it:

After the update:

Two-way binding

Effect: When the data in the text box changes, the data and views in the VM are updated

In the v-model handler, add an input event to Node: After triggering the input, change the value of the corresponding attribute in the VM to the current value:

  modelUpdater(node, value, key) {
    node.value = value
    new Watcher(this.vm, key, newValue= > node.value = newValue)
    // Implement bidirectional binding
    node.addEventListener('input'.() = > {
      this.vm[key] = node.value
    })
  }
Copy the code

Reassignment triggers the set method, which in turn calls the notify method of the Dep instance to update the corresponding view