Implement the mini version of Vue

preface

Vue framework has been used for more than two years. I found that I don't have a deep understanding of Vue, and I only stay where I can use Vue. I don't know much about some details. I recently watched the open class of Lubai at station b. combined with the understanding of the source code of Vue official website, I plan to imitate and write a mini version of Vue framework myself, It is convenient for you to deepen your impression. If you are interested, you can also go to station b to watch the video or watch the official website.
b station link: https://www.bilibili.com/video/BV1Lo4y1277S
Official website link: https://vue-js.com/learn-vue/start

Content combing

The vue of mini version implemented this time mainly includes the following points that we are more concerned about:
1. Bidirectional binding of data
2. Data hijacking is to realize dependency collection
3. Template compilation mainly implements several simple commands, such as v-html, v-text, v-model and click events

The general content is the above points. In addition, we also need to understand the contents of several documents

The above figure is our project directory. The project name is myvue, index Html is our entry file. There are several files under modules
1,complier.js for template compilation
2. dep.js is used for dependency collection
3,index.js is also an entry file in index HTML import
4,observe.js is used to implement bidirectional data binding
5,vue.js contains the classes of Vue
6,watcher.js is used to inform the observer, that is, the dependency of data

Here you should have a general understanding. Next is the coding link

index.html

index.html is an ordinary html page. The content has only one root node with id app

<!DOCTYPE html>
<html lang="cn">
<head>
</head>
<body>
  <div id="app">
  </div> 
</body>
</html>

vue.js

vue.js is our class, which needs to accept an options parameter. This options parameter is passed in when we new Vue(). What are the specific contents? We can think of a Vue page as an instance of Vue, which includes the following contents:
1. el is our root node
2. Data is the data in our common vue page
3. Methods some events, triggered methods

Of course, there must be many complete vue s. Today we only implement some of them. Therefore, our options include the above contents,

export default class Vue{
  constructor(options = {}){
    this.$options = options
    this.$data = options.data
    this.$methods = options.methods

    this.isElement(options.el)

    this._proxyData(this.$data)
  }

  isElement(el){
    if(typeof el == 'string'){
      this.$el = document.querySelector(el)
    }else if(el instanceof HTMLElement){
      this.$el = el
    }

    if(!this.$el){
      throw new Error('Please pass in the string node')
    }
  }

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

When we get the incoming options, we first save them in the current instance. Here, we want to judge whether the incoming node el is legal. If the transmitted node cannot be found or other types of data are transmitted, we need to prompt. Therefore, we use an isElement method to judge the legitimacy of El, If the passed in is not a string (id, class name, etc.) or an html node element, it will prompt that the passed in node is illegal, otherwise we will mount the obtained el to the current instance.
Secondly, we need to mount data on the instance with the help of object Defineproperty, here is a small partner who may ask why you want to mount data on the instance? Recall our development process in vue. If we set a variable in data, do we directly this XXX can get it. This step is to achieve this effect; Secondly, you may ask, if it's an object, don't you need to mount it recursively? In fact, we don't need it here. We don't implement two-way binding here. We just mount the things in data to the instance, so we don't need it.

index.js

We have written the content of the most basic vue class above. First, we put vue JS is introduced into our entry file and initialized to see what has been initialized.

import Vue from './vue.js'
const vm = new Vue({
  el: '#app',
  data: {
    msg: 'hello world',
  },
  methods: {
    handle(){
      alert(111)
    }
  }
})
console.log(vm)

Don't forget we're still in index index.html JS, in index Add this sentence to HTML

<script src="./modules/index.js" type="module"></script>


On the console, we can see that the vue instance has been initialized, and the msg of data is directly mounted on the instance, with msg get and set methods on it. If you want to try to pass the error el, you can try it by yourself. The initialization work here is almost done. The next part is our more troublesome part.

Clear the relationship

Here, let's take a look at the data flow, see the relationship between them, and talk about it one by one
1. dep.js is used to store dependencies, collect dependencies and notify dependencies. Now that we know the role, let's build a dep class first

export default class Dep{
  constructor(){
    this.subs = [] //Dependent collection
  }

  // The method of adding dependency watcher is the observer, that is, the data dependency
  addSubs(watcher){

  }

  // Used to notify watcher of updates
  notify(){

  }
}

2,wtacher. In fact, the finer data of JS is finally executed here. dep is responsible for notifying the watcher,

import Dep from './dep.js'
export default class Watch{
  /**
   * @description: 
   * @param {*} vm Current vue instance
   * @param {*} key data key of
   * @param {*} cb update Callback function
   * @return {*}
   */  
  constructor(vm, key, cb){
    this.vm = vm
    this.key = key
    this.cb = cb

    Dep.target = this
    this.oldValue = vm[key]
    Dep.target = null
  }

  update(){
    // Judge whether the new value is equal to the old value
    if(this.vm[this.key] === this.oldValue){
      return 
    }
    // Execute callback update
    this.cb(this.vm[this.key])
  }
}

Here, you may be a little confused like me. Let me tell you in detail that our Watcher will receive three parameters during initialization. vm is the current vue instance; Key is our dat ā All keys. For example, if you define an a variable in data, the key is a; cb is a callback function for updating data; During initialization, we first save these three variables into the watcher of a variable. Each variable has its own watcher and does not interfere with each other; There is another step more subtle here is to mount a target on DEP, because when we get the old value, we will trigger the get method of our variables. We need to collect the dependencies into dep. Some little partners may wonder that I can add dependencies without setting target. Yes, it is, but this may cause dependencies to be added many times, Our target is to ensure uniqueness. We only add it once, do not add it repeatedly, and set it to null immediately after adding.

At this time, it's easy to look back at dep.js and directly add the code

export default class Dep{
  constructor(){
    this.subs = [] //Dependent collection
  }

  // The method of adding dependency watcher is the observer, that is, the data dependency
  addSubs(watcher){
    if(watcher && watcher.update){
      this.subs.push(watcher)
    }
  }

  // Used to notify watcher of updates
  notify(){
  	this.subs.forEach(watcher => {
      watcher.update()
    })
  }
}

After the collection and update of dependencies are completed, the next step is to compile our template. First, we need to know the complier What is JS used for? For example, we have the following paragraph of template:

  <div id="app">
    <h3 v-html='msg'></h3>
    <h3 v-text='msg'></h3>
    <input type="text" v-model="msg">
    <button v-on:click="handle"></button>
  </div> 

First, how do we get this template,
1. First, we need to get the root node
2. Take the set of root nodes to traverse and judge whether it is a text node or a label node
3. If it is a text node, directly parse its nodeContent. If it is an expression, similar to {msg}}, directly replace the content with a regular expression
If it is a label node, get the attributes of the node for traversal. If it is an instruction starting with v -, proceed to the next step of instruction parsing.
4. If the instruction is v-html or v-text, we will intercept the second half of the instruction V -. If it is v-on:click, we will intercept the content after v-on:; The intercepted string is spliced into a corresponding event. For example, HTML defines an htmlHandle to deal with v-html instructions

The general steps are as follows. Next, we start coding. According to the above contents, when we need to initialize the complier, we need to pass in a vm instance, and then save the contents on the instance

import Watcher from './watcher.js'
export default class Complier{
  constructor(vm){  
    this.vm = vm
    this.data = vm.$data
    this.methods = vm.$methods

    this.initTemplate(vm.$el)
  }

  // Parsing template
  initTemplate(el){
    let childNodes = Array.from(el.childNodes)
    childNodes.forEach(node => {
      // Determine whether it is a text node
      if(this.isTextNode(node.nodeType)){
        this.initTextNode(node)
      }else if(this.isTagNode(node.nodeType)){
        // Label node
        this.initTagNode(node)
      }
      
      // If there are child nodes under the node, recursive traversal
      if(node.childNodes && node.childNodes.length > 0){
        this.initTemplate(node)
      }
    })
  }
  initTextNode(node){
    let reg = /\{\{(.+)\}\}/ // Regular matching {}
    let value = node.textContent // Get text node content
    if(reg.test(value)){ 
      let key = RegExp.$1.trim() // Gets the result of the first regular expression in the current context
      node.textContent = value.replace(reg, this.vm[key]) // Replace the contents of the node
    }
  }

  initTagNode(node){
    // v-html v-text  v-on:click
    let attrs = Array.from(node.attributes) // Get label node properties
    attrs.forEach(attr => {
      if(this.isDirective(attr.name)){ //Determine whether it is the instruction of vue
        // v-text v-on: click to intercept different contents according to different instructions
        let name = attr.name.indexOf(':') > -1 ? attr.name.slice(5) : attr.name.slice(2)
        // Generate fn
        let fn = this[ name + 'Handle']
        fn && fn.call(this, node, name, attr.value)
      }
    })
  }

  clickHandle(node, name, key){
    // click event adds a dom event directly to the node. The callback method is the method in methods. For example, v-on: click = "handle", key is handle
    node.addEventListener(name, this.methods[key])
  }

  textHandle(node, name, key){
    // v-text replaces the content directly
    node.textContent = this.data[key]
    // You need to add a watcher here, because {{msg}} is also a data dependency
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue
    })
  }

  htmlHandle(node, name, key){
    // v-html is similar to v-text
    node.innerHTML = this.data[key]
    new Watcher(this.vm, key, (newValue) => {
      node.innerHTML = newValue
    })
  }

  modelHandle(node, name, key){
    node.value = this.data[key]
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })

    // v-model also needs to manually update the value of data during input to achieve two-way data binding
    node.addEventListener('input', () => {
      this.vm[key] = node.value
    })

  }

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

  isTextNode(type){
    return type === 3
  }

  isTagNode(type){
    return type === 1
  }
}

complier.js has a lot of content, but it is not complex. It handles a lot of details and needs to be seen more.

The last one is observe JS, here we implement our two-way data binding

import Dep from './dep.js'
export default class Observer{
  constructor(vm){
    this.data = vm.$data
    this.vm = vm
    this.traverse(this.data)
  }
// Traversal data
  traverse(data){
    if(!data || typeof data != 'object'){
      return
    }
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }

  defineReactive(data, key, value){
    let dep = new Dep()
    let that = this
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get(){
        	// In combination with the paragraph of watcher, Dep.target will exist only when the watcher is initialized, so it needs to be added to the dependency when getting
          Dep.target && dep.addSubs(Dep.target)
          return value
        },
        set(newValue){
          if(newValue == value){
            return
          }
          value = newValue
          // When setting a new value, if the newValue is obj, you need to traverse it once
          that.traverse(newValue)
          // The data update notification dependency has changed
          dep.notify()
        }
      })
  }
}

Here we have finished writing the mini version of vue. You can run it again. In fact, you don't need to memorize it by rote. The most important thing is that you should understand what vue has done at each step. You may be asked during the interview when to collect the data? When and where to notify subscribers of data updates? How vue parses templates, and so on. After reading the article, I believe you have a certain understanding. Because my article is bad, I hope you can see more of the source code series on the official website and the tutorials of station b, which are linked on it. I also follow station b and write an article here to deepen my impression. I hope you can gain something like me, If there are mistakes in the article, you are welcome to point out and learn from each other.

Keywords: Javascript Vue

Added by VBAssassin on Mon, 31 Jan 2022 10:25:56 +0200