Before we implement Vue mini, let’s take a look at some concepts

1. Data-driven

  • Data driven
  • The core principles of responsiveness
  • Publish subscribe mode and observer mode

Data driven

  • Data responsive, bidirectional binding, data driven

  • Data response

  • The data model is just JavaScript for ordinary objects, and when we modify the data, the view is updated, avoiding tedious DOM manipulation and improving development efficiency

  • Two-way binding

  • The data changes, the view changes, and so does the data

  • We can use v-Models to create two-way data binding on form elements

Data-driven is one of the most unique features of Vue

  • How do you render to a view when you don’t need relational data

2, the core principle of data response Vue2

let data = {
  msg: 'Cabbage'
}
// Simulate the Vue instance
let vm = {}

// Data hijacking, when accessing or setting a vm member, does some interference
Object.defineProperty(vm, 'msg', {
  / / can be enumerated
  enumerable: true.// configurable (you can use delete fir tree or redefine via defineProperty)
  configurable: true,
  get () {
    return data.msg
  },
  set (newValue) {
    if (newValue === data.msg) {
      return 
    }
    data.msg = newValue
    // Data changes, update the DOM value
    document.querySelector("#app").textContent = data.msg
  }
})

/ / test
vm.msg = 'Hello word'
console.log(vm.msg)


// Multiple attributes
let vm = {}
proxyData(data)

function proxyData(data) {
  Object.keys(data).forEach(key= > {
    Object.defineProperty(vm, key, {
      enumerable: true.configurable: true,
      get () {
        console.log('set:', key, data[key])
        return data[key]
      },
      set(newValue) {
        if (newValue === data[key]) {
          return 
        }
        data[key] = newValue
        document.querySelector("#app").textContent = data[key]
      }
    })
  })
}
// vm. MSG = 'Chinese'
// set: MSG
Copy the code

3, Vue response type principle Vue3

  • MDN Proxy
  • Listen directly on objects, not properties
  • New in ES6, not supported by IE, performance is optimized by browser
let data = {
  msg: 'hello'
}

let vm = new Proxy(data, {
  get(target, key) {
    return target[key]
  },
  // Setting the vm's membership will be performed
  set(target, key, newValue) {
    console.log('set', key, newValue)
    if (target[key] === newValue) {
      return 
    }
    target[key] = newValue
    document.querySelector('#app').textContent = target[key]
  }
})
vm.msg = 'Cabbage'
console.log(vm.msg)
Copy the code

4. Publish and subscribe

  • Publish/subscribe
  • The subscriber
  • The publisher
  • The signal center

We assume that there is a “signal center” that publishes a signal when a task completes, and that other tasks subscribe to the signal center to know when they can start executing. This is called a “publish/schedule pattern” (publish-subscribe pattern)

let vm = new Vue()
// Register events (subscribe messages)
vm.$on('dataChange'.() = > {
  console.log('dataChange')
})

vm.$on('dataChange'.() = > {
  console.log('dataChange1')})// Trigger events (publish messages)

// Custom events
class EventEmitter {
  constructor () {
    this.subs = Object.create(null)}// Register events
  $on (eventType, handler) {
    this.subs[eventType] = this.subs[eventType] || []
    this.subs[eventType].push(handler)
  }
  // Trigger the event
  $emit (eventType) {
    if (this.subs[eventType]) {
      this.subs[eventType].forEach(handler= > {
        handler()
      })
    }
  }
}

// Test it
let em = new EventEmitter()
em.$on('click'.() = > {
  console.log('click1')
})
em.$on('click'.function() {
  console.log('click2')
})

em.$emit('click')
Copy the code

5. Observer mode

  • Observer (subscriber) — Watcher
  • Upload (): Specific things to do when an event occurs
  • Target (publisher) –Dep
    • Subs array: Stores all observers
    • AddSub (): Adds an observer
    • Notify (): When an event occurs, call the upload() method for all observers
  • No event center
class Dep {
  constructor () {
    // Record all subscribers
    this.subs = []
  }
  // Add an observer
  addSub() {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  notify () {
    this.subs.forEach(sub= > {
      sub.update()
    })
  }
}

class Watcher {
  update () {
    console.log('update')}}// Test it
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)
dep.notify()

Copy the code
  • The observer mode is scheduled by a specific target, and the Dep goes back and calls the observer’s methods, so there is a dependency between the observer mode’s subscribers and publishers
  • The publish/subscribe pattern is invoked by the unified dispatch center, so publishers and subscribers do not need to be aware of each other’s existence
  • As shown in figure

6. Simulation of Vue response type principle – analysis

  • Vue basic structure
  • Print Vue instance observation
  • As shown in figure

Vue

  • Inject the members in data into the Vue instance and convert the members in data into getters/setters

Observer

  • The ability to listen for all attributes of the data object, get the latest values and notify Dep if there are changes

Ok, so much, finally to how to implement the mini version of vUE, now we will step by step to implement the mini version of vUE

7, Vue

  • Responsible for receiving initialization parameters (options)
  • Is responsible for injecting properties from Data into Vue instances and converting them into getters/setters
  • Responsible for calling the Observer to listen for changes to all properties in data
  • Responsible for calling compiler parse instructions/differential expressions

structure

  • + $options
  • + $el
  • + $data
  • + _proxyData

First add a vue.js file to the file

class Vue {
  constructor (options) {
    // 1. Save the data of the option through properties
    // 2. Convert data members into getters and setters and inject them into Vue instances
    // 3. Call observer to listen for data defense
    // 4. Invoke the Compiler object to parse instructions and differential expressions
  }
  // Proxy data
  _proxyData (data) {

  }
}
Copy the code

The complete code

// vue.js
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 observer to listen for data defense
	new Observer(this.$data)
    // 4. Invoke the Compiler object to parse instructions and differential expressions
  }
  _proxyData (data) {
    // Iterate over all attributes in data
    Object.keys(data).forEach(key= > {
      // Inject the data attribute into the Vue instance
      Object.defineProperty(this, key, {
        enumerable: true.configurable: true,
        get () {
          return data[key]
        },
        set(newValue) {
          if (newValue === data[key]) {
            return 
          }
          data[key] = newValue
        }
      })
    })
  }
}
// Test it
// Console type vm to see if there are instances
Copy the code

At this point we need to use vue.js in index. HTML

<script src="vue.js"></script>

let vm = new Vue({
  el: '#app'.data: {
    msg: 'Hello Vue'.count: 100}})console.log(vm.msg)
Copy the code

Enter vm test on the console and you can see the member we just added

8 the Observer.

  • Is responsible for converting properties in the Data option into responsive data
  • A property in data is also an object, and that property is converted into responsive data
  • Send notifications of data changes

structure

  • + walk(data)
  • + defineReactive(data, key, value)

Create a new observer.js and import it in index.html

// +walk(data)
// + defineReactive(data, key, value)
/ / structure
class Observer {
  walk (data) {

  }
  defineReactive (obj, key, val) {
    
  }
}
Copy the code

Complete code

class Observer {
  constructor (data) {
    this.walk(data)
  }
  walk (data) {
    // 1. Check whether data is an object
    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) {
    let that = this
    // If val is an object, convert the attributes inside val to responsive data
    this.walk(val)
    Object.defineProperty(obj, key, {
      enumerable: true.configurable: true,
      get () {
        return val
      },
      set (newValue) {
        if (newValue === val) {
          return
        }
        val = newValue
        that.walk(newValue)
        // Send notifications}}}})Copy the code

Test the

<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Vue</title>
</head>

<body>
  <div id="app">
    <h1>Chinese cabbage</h1>
    <h1>{{ msg }}</h1>
    <h1>{{ count }}</h1>
    <div>{{ name }}</div>
    <h1>v-text</h1>
    <div v-html="htmlStr"></div>
    <div v-text="msg"></div>
    <input type="text" v-model="msg">
    <input type="text" v-model="count">
  </div>
  <script src="observer.js"></script>
  <script src="vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app'.data: {
        msg: 'Hello Vue'.count: 100.person: {
          name: 'zzzz'}}})console.log(vm.msg)
    vm.msg = { test: 'Chinese cabbage' }
  </script>
</body>
</html>
Copy the code

Remember to call Observer in step 3 of vue.js

new Observer(this.$data)
Copy the code

Enter VM on the console and see MSG as a responsive object

9, the Compiler

The Compiler function

  • Responsible for compiling templates and parsing instruction/differential expressions
  • Responsible for the first rendering of the page
  • Re-render the view when the data changes

The Compiler structure

  • + el
  • + vm
  • + compile(el)
  • + compileElement(node)
  • + compileText(node)
  • + isDirective(arrrNode)
  • + isTextNode(node)
  • + isElementNode(node)

The new compiler. Js

// The compiler. Js method is implemented step by step
class Compiler {
  constructor (vm) {
    this.el = vm.$el
    this.vm = vm
  }
  // Compile templates to handle text nodes and element nodes
  compile (el) {

  }
  // Compile the element node to generate the instruction
  compileElement (node) {

  }
  // Compile the text node to interpolate the expression
  compileText (node) {

  }
  // Determine if element attributes are directives
  isDirective(attrName) {
    return attrName.startsWith('v-')}// Determine whether the node is a text node
  isTextNode (node) {
    return node.nodeType === 3
  }
  // Check whether the node is an element node
  isElementNode (node) {
    return node.nodeType === 1}}Copy the code

Compiler compile

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= > {
      // 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

Compiler compileText

 // Compile the text node to get the difference
 compileText (node) {
   // console.dir(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

Compiler compileElement

 // Compile the element node to generate the instruction
 compileElement (node) {
   console.log(node.attributes)
   // Iterate over all attribute nodes
   Array.from(node.attributes).forEach(attr= > {
     // Check if it is a command
     let attrName = attr.name
     if (this.isDirective(attrName)) {
       // v-text --> text
       attrName = attrName.substr(2)
       let key = attr.value
       this.update(node, key, attrName)
     }
   })
 }

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

10, Dep

function

  • Collect dependencies and add observers
  • Notify all observers

Dep structure

  • + subs
  • + addSubs(sub)
  • + notify

New Dep. Js

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

After completing dep.js, we need to create the DEP object in defineReactive in observer.js

defineReactive (obj, key, val) {
  let that = this
  // Collect dependencies and send notifications
  let dep = new Dep()
  // If it is val, convert the attributes inside val to a responsive object
  that.walk(val)
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true,
    get () {
      // Collect dependencies
      Dep.target && dep.addSub(Dep.target)
      return val
    },
    set (newValue) {
      if (newValue === val) {
        return
      }
      val = newValue
      that.walk(newValue)
      // Send notifications
      dep.notify()
    }
  })
}
Copy the code

11, Watcher

function

  • When data changes start dependent, DEP notifies all Watcher instances of update attempts
  • Add yourself to the DEP object when instantiating itself

Watcher

  • + vm
  • + key
  • + cb
  • + oldValue
  • + update

Create a new watcher.js file

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;
    
    // Record the Watcher object to the Dep class's static property target
    Dep.target = this;
    // Trigger the get method and call addSub in the get method
    this.oldValue = vm[key];
    Dep.target = null
  }
  // Update the view when data changes
  update () {
    let newValue = this.vm[this.key];
    // Determine whether the new value is equal to the old value
    if (this.oldValue === newValue) {
      return
    }
    this.cb(newValue)
  }
}
Copy the code

We need compileText, textUpdater, modelUpdater in compile.js to create watcher objects

 // Compile the text node to get the difference
 compileText (node) {
   // console.dir(node)
   let reg = / \ {\ {(. +?) \} \} /
   let value = node.textContent
   if (reg.test(value)) {
     let 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
     })
   }
 }

 // Process the V-text instruction
 textUpdater (node, value, key) {
   node.textContent = value
   new Watcher(this.vm, key, (newValue) = > {
     node.textContent = newValue
   })
 }
 // v-model
 modelUpdater (node, value, key) {
   node.value = value;
   new Watcher(this.vm, key, (newValue) = > {
     node.value = newValue
   })
 }
 / / to the key
 update (node, key, attrName) {
   let updateFn = this[attrName + 'Updater']
   updateFn && updateFn.call(this, node, this.vm[key], key)
 }
Copy the code

At this point, the compile object is called in step 4 of vue.js, parsing the instructions and interpolation expressions

// 4. Invoke the Compiler object to parse instructions and differential expressions
new Compiler(this)
Copy the code

Open the console and see the following. At this point, the Mini version of vUE is almost complete

12. Bidirectional binding

Register events for input to implement bidirectional binding

 // v-model
 modelUpdater (node, value, key) {
   node.value = value;
   new Watcher(this.vm, key, (newValue) = > {
     node.value = newValue
   })
   // Bidirectional binding
   node.addEventListener('input'.() = > {
     this.vm[key] = node.value
   })
 }
Copy the code

13, summary

Create a Vue object, record the options passed from options in the Vue constructor, and then call a Proxy data, that is, inject data into the Vue instance, create Observer and Compiler. The function of the Observer is data hijacking. In the Observer, the data property is converted into getters and setters. When the data changes, that is, when the set is triggered, the change needs to be notified and the notify method of the Dep is called. And the notify method of Dep calls the Update method in Watcher to notify Watcher that the data has changed and you’re going to update the view, and then the Update method in Watcher is going to update the view only when the data has changed, When the Watcher object is created, the current Watcher object is added to the SUBs array of the Dep to collect the dependencies, so that the Dep records the Watcher and then creates the Compiler object, which parses instructions, differential expressions, and when the page is first loaded. The relevant methods in Compiler are called to update the view, while the Compiler also subscribing to data changes and binding update functions. When creating a Watcher object, a callback function is passed to update the view. Note that the view is updated in the Compiler for the first time. Watcher updates the view when data is sent for changes.

The completed code has been submitted to Github

Please click here

Thank you for your

Thank you for taking the time to read this article. If you find it helpful, please give it a thumbs up. (Thanks for your encouragement and support 🌹🌹🌹)