The starting

When using VUE, data is bi-directional bound, interpolation syntax… Wait, a dizzying array of operations. Learn how to build a wheel as a user, not to build your own wheel, but to use the wheel better.

Although vuE3 is now available, many companies are still using Vue2.x.

use

When we use it, we usually do the initialization in the main file, usually

new Vue({
  router,
  store,
  .......
  render: h= > h(App)
}).$mount('#app')
Copy the code

For me, a front-end cutout would certainly not be as complex as we were in the project. For vue, we have another way to use it, to introduce vue.js in an HTML file, such as this

new Vue({
    el: '#app'.data: {
      number: 1,},methods: {
      add() {
        this.number++
      },
      changeInput() {
        console.log('changeInput'); }}})Copy the code

As such, today’s implementation is used in this way. In a real use of Vue, there would be three render, Template, and EL, with the same order of priority.

Analysis of the

First of all, what kind of class are we implementing

  • First it takes an object, el,data,methods…..
  • Data in data requires responsive processing
  • Parse template processing, which is the {{number}} we put in the div
  • Responsive updates, and most importantly, notify the view to update when the data changes
  • Handle events and instructions such as V-Model,@click, etc

Start to implement

First create the vue class

class Vue{
  constructor(options) {
    // Save the incoming
    this.$options = options;
    // Pass in data and save it in $data
    this.$data = options.data
  }
}
Copy the code

$data = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx = this.xxx Roperty can do that, so let’s do that.

function proxy(vm) {
  // The vm needs an instance of vue itself
  Object.keys(vm.$data).forEach(key= > {
    Object.defineProperty(vm, key, {
      get: () = > vm.$data[key],
      set: (v) = > vm.$data[key] = v
    })
  })
}
Copy the code

This method is so simple that we don’t need to go into too much detail. Let’s use it in the vue class so that we can get the values in the data from the instance

class Vue{
  constructor(options) {
    this.$options = options;
    this.$data = options.data
+   proxy(this)}Copy the code

So we can use this.xx to get, because we haven’t implemented method yet so we can experiment with the example obtained by new Vue

const app = new Vue({
    el: '#app'.data: {
      counter: 1,}})console.log(app.number);
Copy the code

We can see in the console that the numbe value was successfully output

The proxy for Data is then successful.

The template parsing

For template parsing, you need to handle the interpolation written (custom instructions and events written, which will be resolved later)

First parse the interpolation in the template with braces that don’t fill the screen when the page opens.

class Compile {
  // We need to pass el and vue themselves to parse the template, EL needs to be used to get the element,vue itself needs data,methds...
  constructor(el, vm) {
    this.$vm = vm
    // Get the element we parsed
    this.$el = document.querySelector(el)

    if(this.$el) {
      // Write a function to parse the template
      this.compile(this.$el)
    }
  }
  compile(el) {
    // Iterates over the children of el to determine their type and handle them accordingly
    const childNodes = el.childNodes
    if(! childNodes)return;
    childNodes.forEach(node= > {
      if(node.nodeType === 1) {
        // The element handles instructions and events (to be handled later)
      } else if(this.isInter(node)) {
        / / text
        this.compileText(node)
      }
      // Recursion is required when there are child elements
      if(node.childNodes) {
        this.compile(node)
      }
    })
  }
  
  // Compile the text
  compileText(node) {
      node.textContent = this.$vm[RegExp.$1]
  }

  // Whether to interpolate
  isInter(node) {
    return node.nodeType === 3 && / \ {\ {(. *) \} \} /.test(node.textContent)
  }
}
Copy the code

Then use it in vue.

class Vue{
  constructor(options) {
    this.$options = options;
    this.$data = options.data
    proxy(this)
+   new Compile(options.el, this)}}Copy the code

The interpolation in the page can be replaced with the data in the data, but the data is not changed when we change it outside through the vue instance.

Implement data response

In implementing data responsiveness, we use the source code idea of using an Observer for responsiveness, a watcher and a DEP for notification of updates. In vUE, there is only one watcher per component, and our granularity cannot be compared. After all, it is a simple version of VUE that is only partially implemented. Write the Observer first

The Observer to write

// Iterate obj to do the response
function observer(obj) {
  if(typeofobj ! = ='object' || obj === null) {
    return
  }
  // Iterate over all keys of obj to make the response
  new Observer(obj);
}
// Iterate over all keys of obj to make the response
class Observer {
  constructor(value) {
    this.value = value
    if(Array.isArray(this.value)) {
      // TODO does nothing but overwrite the seven methods of the array
    } else {
      this.walk(value)
    }
  }
  // Object response
  walk(obj) {
    Object.keys(obj).forEach(key= > {
      // This item will look familiar to many people. Of course, it's just the name
      defineReactive(obj, key, obj[key])
    })
  }
}
Copy the code

DefineReactive here is a reactive handler for data, and before you write this function, you need to write watcher first

Watcher to write

// Listener: responsible for dependency updates
const watchers = []; // Use an array to collect watcher updates without dep
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn
    watchers.push(this)}// The future is called by Dep
  update() {
    // Perform the actual update operation
    // Since we need to get the latest value when watcher updates, we need to pass it here as an argument to the function we are collecting
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}
Copy the code

After having watcher, we need to change the Compile class we wrote before to collect Watcher. At the same time, we need to change the Compile method to v-model and V-test at the same time… Make the groundwork

The Compile of evolution

class Compile {
  // We need to pass el and vue themselves to parse the template, EL needs to be used to get the element,vue itself needs data,methds...
  constructor(el, vm) {
    this.$vm = vm
    // Get the element we parsed
    this.$el = document.querySelector(el)

    if(this.$el) {
      // Write a function to parse the template
      this.compile(this.$el)
    }
  }
  compile(el) {
    // Iterates over the children of el to determine their type and handle them accordingly
    const childNodes = el.childNodes
    if(! childNodes)return;
    childNodes.forEach(node= > {
      if(node.nodeType === 1) {
        // The element handles instructions and events (to be handled later)
      } else if(this.isInter(node)) {
        / / text
        this.compileText(node)
      }
      // Recursion is required when there are child elements
      if(node.childNodes) {
        this.compile(node)
      }
    })
  }
  
  // Add a new function
  //node for the modified element exp for the key that gets the value inside the braces dir for this custom operation
  update(node, exp, dir) {
    / / initialization
    const fn = this[dir + 'Update']
    fn && fn(node, this.$vm[exp])
    // Update this is where the watcher is created and the updated function is passed in. The val is the latest value passed in when the watcher fires the update function
    new Watcher(this.$vm, exp, function(val) {
      fn && fn(node, val)
    })
  }
  // Add a new function
  textUpdate(node, val) {
     node.textContent = val
   }
  // Compile the text
  compileText(node){-//node.textContent = this.$vm[RegExp.$1]
  +   this.update(node, RegExp. $1,'text')}// Whether to interpolate
  isInter(node) {
    return node.nodeType === 3 && / \ {\ {(. *) \} \} /.test(node.textContent)
  }
}
Copy the code

DefineReactive write

Let’s write defineReactive to actually update the view

/** * Can sense when the key changes *@param {*} Object obj *@param {*} Key Key * to intercept@param {*} Val initial value */
 function defineReactive(obj, key, val) {
  / / recursion
  observer(val);
  Object.defineProperty(obj, key, {
    get() {
      return val
    },
    set(newVal) {
      console.log('set', newVal);
      if(newVal ! = val) { observer(newVal) val = newVal// We will add deP later to make the implementation more refined
        watchers.forEach(w= > w.update())
      }
    }
  })
}
Copy the code

Used in the vue class

class Vue{
  constructor(options) {
    this.$options = options;
    this.$data = options.data
+   observer(this.$data)
    proxy(this)
    new Compile(options.el, this)}}Copy the code

Next, modify the page we use

const app = new Vue({
    el: '#app'.data: {
      number:10,}})console.log(app.number);
  setTimeout(() = > {
    app.counter = 100
  }, 1000)
Copy the code

I can see in the page that after a second the view changes towards the desired result, which is what I want, but there is a problem with this, which is that when we define a lot of properties in the data, we use them in the page. When we change one, we all the watcher will perform again, use the page all data will be updated, the question I’m sure don’t want to, so you need to dep, we modify one of the values, the only change in the changes and this will be a little better. The function of this class is very simple. A DEP manages a piece of data in data, collects watcher related to that piece of data, and notifishes updates when changes occur

Dep. Write

class Dep{
  constructor() {
    this.deps = []
  }
  addDep(dep) {
    this.deps.push(dep)
  }
  notify() {
    this.deps.forEach(dep= > dep.update())
  }
}
Copy the code

Watcher needs to be modified when DEP is required, and the Watchers array is no longer needed to manage it.

Watcher evolution

// Listener: responsible for dependency updates
- //const watchers = []; // Use an array to collect watcher updates without dep
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn
-    watchers.push(this)
    // Trigger dependency collection
+   Dep.target = this
    // defineReactive is used by defineReactive
+   this.vm[this.key]
+   Dep.target = null
  }

  // The future is called by Dep
  update() {
    // Perform the actual update operation
    // Since we need to get the latest value when watcher updates, we need to pass it here as an argument to the function we are collecting
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}
Copy the code

Write Watcher and then re-method it to use DEP to manage updates, rather than triggering all Watcher updates

DefineReactive evolution

/** * Can sense when the key changes *@param {*} Object obj *@param {*} Key Key * to intercept@param {*} Val initial value */
 function defineReactive(obj, key, val) {
  / / recursion
  observer(val);
   // Create a Dep instance
  // Each item in data is entered here, creating a Dep
+  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
       // Dependency collection
      // Dep will collect updates only if the target is present at the time of invocation (set the static target to watcher at initialization)
      // We get the current watcher value once when the watcher comes in, and then the wacher sets itself as the target of the Dep, which is used here for collecting updates, but sets the dep.target to null after the trigger, making us flat Do not do this operation when reading from
+     Dep.target && dep.addDep(Dep.target)
+     return val
    },
    set(newVal) {
      console.log('set', newVal);
      if(newVal ! = val) { observer(newVal) val = newVal// We will add deP later to make the implementation more refined
-       watchers.forEach(w= > w.update())
        // Notifies all of their own watcher updates when it is changed
        // A Dep can refer to multiple watcher updates
+       dep.notify()
      }
    }
  })
}
Copy the code

Next, modify the HTML file used to see the effect

  const app = new Vue({
    el: '#app'.data: {
      counter: 1.number:10,}})console.log(app.number);
  setTimeout(() = > {
    app.counter = 100
  }, 2000)
Copy the code

This way we don’t generate DOM updates where we don’t use the data. There is no change where number is used

Next, the instructions and events are compiled, mainly to modify the Compile class to be processed when the template is parsed. Before the processing, we first modify the Vue class and save the methods

class Vue{
  constructor(options) {
    this.$options = options;
    this.$data = options.data
+   this.$methods = options.methods
    observer(this.$data)
    proxy(this)
    new Compile(options.el, this)}}Copy the code

Compile is finally formed

Next, modify the Compile class to handle instructions and events while the template is parsed

class Compile {
  // We need to pass el and vue themselves to parse the template, EL needs to be used to get the element,vue itself needs data,methds...
  constructor(el, vm) {
    this.$vm = vm
    // Get the element we parsed
    this.$el = document.querySelector(el)

    if(this.$el) {
      // Write a function to parse the template
      this.compile(this.$el)
    }
  }
  compile(el) {
    // Iterates over the children of el to determine their type and handle them accordingly
    const childNodes = el.childNodes
    if(! childNodes)return;
    childNodes.forEach(node= > {
      if(node.nodeType === 1) {
        // The element handles instructions and events
+       const attrs = node.attributes
+       Array.from(attrs).forEach(attr= >{+// v-xxx="abc"
+         const attrName = attr.name
+         const exp = attr.value
+         // When an attribute value starts with v-, it is considered an instruction
+         if(attrName.startsWith('v-')) {+const dir = attrName.substring(2)
+           this[dir] && this[dir](node, exp)
+         // Event handling is also very simple+}else if(attrName.startsWith(The '@')) {+const dir = attrName.substring(1)
+           this.eventFun(node, exp, dir)
+         }
+       })
      } else if(this.isInter(node)) {
        / / text
        this.compileText(node)
      }
      // Recursion is required when there are child elements
      if(node.childNodes) {
        this.compile(node)
      }
    })
  }
+ // A new function is added to handle events
  eventFun(node, exp, dir) {
    node.addEventListener(dir, this.$vm.$methods[exp].bind(this.$vm))
  }
  //node for the modified element exp for the key that gets the value inside the braces dir for this custom operation
  update(node, exp, dir) {
    / / initialization
    const fn = this[dir + 'Update']
    fn && fn(node, this.$vm[exp])
    // Update this is where the watcher is created and the updated function is passed in. The val is the latest value passed in when the watcher fires the update function
    new Watcher(this.$vm, exp, function(val) {
      fn && fn(node, val)
    })
  }
  textUpdate(node, val) {
     node.textContent = val
   }
+ // A new function is added to handle v-text
  text(node, exp) {
    this.update(node, exp, 'text')} +// Added a new function to handle V-HTML
  html(node, exp) {
    this.update(node, exp, 'html')} +// Add a new function
  htmlUpdate(node, val) {
    node.innerHTML = val
  }
  // Compile the text
  compileText(node){-//node.textContent = this.$vm[RegExp.$1]
  +   this.update(node, RegExp. $1,'text')} +// A new function has been added to handle v-model
  model(node, exp) {
    // console.log(node, exp);
    this.update(node, exp, 'model')
    node.addEventListener('input'.(e) = > {
      // console.log(e.target.value);
      // console.log(this);
      this.$vm[exp] = e.target.value
    })
  }
  // Whether to interpolate
  isInter(node) {
    return node.nodeType === 3 && / \ {\ {(. *) \} \} /.test(node.textContent)
  }
}
Copy the code

Next, modify the HTML file to see the effect

<body>
  <div id="app">
    <p @click="add">{{counter}}</p>
    <p>{{counter}}</p>
    <p>{{counter}}</p>
    <p>{{number}}</p>
    <p v-text="counter"></p>
    <p v-html="desc"></p>
    <p><input type="text" c-model="desc"></p>
    <p><input type="text" value="changeInput" @input="changeInput"></p>
  </div>
</body>
<script>
  const app = new Vue({
    el: '#app'.data: {
      counter: 1.number:10.desc: `<h1 style="color:red">hello CVue</h1>`
    },
    methods: {
      add() {
        console.log('add'.this);
        this.counter++
      },
      changeInput() {
        console.log('changeInput'); }}})</script>
Copy the code

At this point, the writing is done, and the page has achieved the desired effect.

Experience url: codesandbox. IO/s/dazzling -…

As my own learning notes, there are also a lot of negligence in the code, borrowed from other people’s code to achieve, but learned is their own.

The last

Click like before you go!