Vue is now in version 3, so let’s review its development step by step. And analyze the core principles.

If you don’t know much about ES6, you can read the es6 introduction tutorial written by Ruan Yifeng

There is no virtual DOM in Vue1, and in this version we will only focus on the corresponding. The thing that implements bidirectional binding of arrays in VUE1 is Object.defineProperty. So let’s just do a little demo and create a new index.js

function defineReactive(obj, key, val){
    Object.defineProperty(obj, key, {
        get (){
            console.log('get',val)
            return val
        },
        set (newVal){
            if(newVal ! == val){console.log('set',newVal)
                val = newVal
            }
        }
    })
}


var  obj = {bar:123}
defineReactive(obj, bar, obj.bar)
obj.bar // The console prints get 123
obj.bar = 456 // The console prints set 456
Copy the code

I see that that triggers the setter and getter. But what if objects nested objects? Obviously our method is not sufficient for such a case. Should we see what its children are before we call it? Let’s see if that solves the problem.

function defineReactive (obj, key, val) {
  observer(val) 
  // If it is an object type, the re-execution listens at a deeper level
  Object.defineProperty(obj, key, {
    get () {
      console.log('get', val)
      return val
    },
    set (newVal) {
      if(newVal ! == val) {console.log('set', newVal)
        val = newVal
        observer(val) 
        // If an item in the object is changed to an array, listen again}}})}function observer (obj) {
  if (typeofobj ! = ='object'&& obj ! =null) {
    return
  } // If the object passed in is not an object, the return is not executed
  Object.keys(obj).forEach(key= > {
    defineReactive(obj, key, obj[key])
  })
}
var obj = { bar: 123.bar1: { a: 123 } }
observer(obj)
obj.bar1.a // get 123
obj.bar1.a = 465 // set 465
Copy the code

Data correspondence

Now let’s see how do we use vue

new Vue({
    el:'#app'.data(){
        return{... }},methods: {... }})Copy the code

We can see from calling this code that we first need a vue class (constructor) and then pass an object that has these properties el, data, methods. So these are sort of the configurations that we need to use this class, and some optional configurations. Next, let’s simply implement a vue class

class Vue{
    constructor(options){ // The parameters passed in
        this.$options = options // Save the parameters
        initData(this) // Execute the initialization function}}function initData(vm){
    let {data} = vm.$options // Separate the data
    // Check whether data exists
    if(! data){ vm_data={}// Create an empty object if the data option does not exist
    } else {
        vm_data = typeof= = ='function' ? data() : data
        Data (){return {}} = data(){return {}} = data(){return {}} If it is a method it executes one and returns an object.
    }
    // The important thing about this method is that we loop through _data and then conveniently hang up the agent so that we can access it directly instead of this.data. The variable is directly this. Variable
    Object.keys(vm._data).forEach(key= > {
        proxy(vm, '_data', key)
    })
    observe(vm._data)
}
/ / access to the target. The key is, in fact, return target [sourceKey] [key] here is equivalent to this. A visit is actually enclosing _data while forming. With a need to modify and change the target value
function proxy(target, sourceKey, key){
    Object.defineProperty(target, key,{
        get() {
            return target[sourceKey][key]
        },
        set(newVal){
            target[sourceKey][key] = newVal
        }
    })
}

function observe(value){
    if(typeofvalue ! = ='object'&& value ! =null) {return 
    }
    if(value.__ob__){
        return value.__ob__
    }
    new Observer(value)
}

class Observer{
    constructor(value){
        Object.defineProperty(value,'__ob__', {value:this.enumerable: false.writable: true.configurable: true
        })
        this.walk(value)
    }
    walk(obj){
        Object.keys(obj).forEach(key= > {
          defineReactive(obj, key, obj[key])
        })
    }
}

  function defineReactive(obj, key, val) {
      observe(val)
      Object.defineProperty(obj, key, {
        get() {
          return val
        },
        set(newVal) {
          if(newVal ! == val) { val = newVal observe(val) } } }) }Copy the code

Now we can implement a vUE that doesn’t focus on the view, and we can do the same thing with initData to execute the agent methods, props, and so on. DefineProperty can only proxy objects, so let’s do the same for arrays.

class Observer{
    constructor(value){
        Object.defineProperty(value, '__ob__', {
            value: this.enumerable: false.writable: true.configurable: true
        })
        if(Array.isArray(value)){
          // Process the array and reserve the location first
        } else {
            this.walk(value)
        }
    }
}
Copy the code

Page updates

So we’re almost done with the data form, and now we’re going to combine it with the page.There are a few things we need to know before we do that

Vue: the framework constructor

Observer: Performs data correspondence (analyzes whether the data is an object or an array)

Complie: compile templates, initialize attempts, collect dependencies (update functions, watcher create)

Watcher: Perform the update function (DOM update)

Dep: Manage watcher, batch update

Compiling templates – Complie compiles vue special syntax in templates, initializes views, and updates functions

class Compile{
    constructor(el, vm){
        this.$vm = vm
        this.$el = document.queryselector(el)
        if(this.$el){
            this.compile(this.$el)
        }
    }
    compile(el){
        el.childNodes.forEach(node= >{
            if(this.isElement(node)){
                this.compileElement(node)
            } else if(this.isInter(node)){
                this.complieText(node)
            }
            if(node.childNodes){
                this.compile(node)
            }
        })
    }
    
    isElement(node){
        return node.nodeType === 1
    }
    
    isInter(node){
        return node.nodeType === 3 && / \ {\ {(. *) \} \} /.test(node.textContent)
    }
    
    complieText(node){
        this.update(node, RegExp. $1,'text')}compileElement(){
        // Get the node attributes
        const nodeAttrs = node.attributes
        Array.from(nodeAttrs).forEach(attr= >{
            const attrName = attr.name
            const exp = attr.value
            if(this.isDirective(attrName)){
                const dir = attrName.substring(2) // Extract the vUE directive text HTML model
                this[dir] && this[dir](node,exp)
            } 
            if(this.isEvent(attrName)){
                const dir = attrName.substring(1)
                this.eventHandler(node, exp, dri)
            }
        })
    }
    
    eventHandler(node, exp, dir){
        const cb = this.$vm.$options.methods && this.$vm.$options.methods[exp]
        node.addEventListener(dir, fn.bind(this.$vm))
    }
    
    text(node, exp){
        this.update(node, exp, 'text')}html(node, exp){
        this.update(node, exp, 'html')}model(node, exp){
        this.update(node, exp, 'model')
        const { tagName, type } = node
        tagName = tagName.toLowerCase()
        if(tagName == 'input' && type == 'text') {// Assign an initial value if an initial value is bound
            node.value = this.$vm[exp]
            node.addEventListener('input'.() = >{
                this.$vm[exp] = e.target.value
                // Listen for input events to change the binding value})}else if(tagName == 'input' && type == 'checkbox') {
            node.value = this.$vm[exp]
            node.addEventListener('change'.() = >{
                this.$vm[exp] = e.target.checked
                // Listen for the change event of the checkbox to change the binding value})}else if(tagName == 'select'){
            node.value = this.$vm[exp]
             node.addEventListener('input'.() = >{
                this.$vm[exp] = e.target.value
                // Listen for input events to change the binding value}}})// All bindings need to bind the update function and the corresponding Wathcer instance
    update(node, exp, dir){
        const cb = this[dir + 'Updater']
        cb && cb(node, this.$vm(exp))
        new Watcher(this.$vm, exp, function(val){
            cb && cb(node, val)
        })
    }
    
    textUpdater(node, value){
        node.textContent = value
    }
    
    htmlUpdater(node, value){
         node.innerHTML = value
    }
    
    modelUpadter(node, value){
        node.value = value
    }
     
}

Copy the code

In VUE1, key and DEP are one-to-one correspondence. The object returned in data corresponds to a DEP, and each key in the object corresponds to this DEP

class Dep{
   constructor(){
    this.watchers = []
  }
    static target = null
    depend(){
        if (this.watchers.includes(Dep.target)){
            return
        }
        this.watchers.push(Dep.target)
    }
    notify(){
        this.watchers.forEach(watcher= >{
            watcher.update()
        })
    }
}

Copy the code

A dependency on a page corresponds to a watcher. In vue1, dep and watcher have a 1:N relationship

class watcher{
    constructor(vm, key, updateFn){
        this.vm = vm
        this.key = key
        this.updateFn = updateFn
        
        // Read the data once, triggering get() in definrReaective
        Dep.target = this
        this.vm[this.key]
        Dep.target = null
    }
    
    update(){
        // Pass the latest value of the parent to the update function
    this.updateFn.call(this.vm, this.vm[this.key])
    }
    
}
Copy the code

This basically creates a corresponding page, but we didn’t come close to overwriting the Array method, we just copied the method and didn’t notify the update. Let’s continue to refine this method.

    const orignalProto = Array.prototype;
    const arrayProto = Object.create(orignalProto);
    // Only these 7 methods will change the array, so let's override these 7 methods
    ['push'.'pop'.'shift'.'unshift'.'splice'.'reverse'.'sort'].forEach(method= >{
        orignalProto[method].apply(this.arguments)
        let inserted = []
        switch(method) {
            case 'push':
            case 'unshift':
                inserted = arguments
                break;
            case 'splice':
                inserted = arguments.slice(2)
                break;
        }
        if(inserted.length > 0){
            inserted.forEach(key= >{
                observe(key)
            })
        }
    })
    
Copy the code

A simple vUE is done, we only do the simplest analysis code, no compatibility processing. Don’t take it seriously.

conclusion

At the beginning of the design, VUE1 was designed to solve our daily tedious tasks of obtaining DOM, obtaining values, listening to DOM changes, updating DOM data, etc. He put all of these together into an automated framework that would reduce our cumbersome operations. Of course, vue1 also has disadvantages. The absence of virtual DOM and Diff in VUe1 will consume a lot of resources when the project is large and rich in content. Every corresponding data on a page generates a watcher, and a large page with a lot of data also generates a lot of Watcher. Total resources.