preface

Hello everyone, I am console without log, a sophomore junior college student. This article will share with you the principle of vUE’s data-driven view, as well as the knowledge of template compilation. I will write this article very simply, trust me, you will understand.

In this article, I mainly explain the logic of the principle from requirements, flow charts and codes. Now I will start to write the article without more words.

Step into the project’s essentials

closure

Scopes are divided into global scopes and local scopes

Variables in the local scope are collected by the garbage collection mechanism after the execution of the scoped method

function run() {
  let a = 11
  return a
}

let num = run() // When this code is done, variable A in the local scope of the run method is disposed of by the garbage collection mechanism
let num2 = run() // When this code is executed, a local scope is recreated and a variable a is generated, which is disposed of again by the garbage collection mechanism when the function completes

Copy the code
function run () {
  let a = 11
  let b = 12

  function asd() {
    a = a+1
    return a
  }

  return asd
}

let me = run() / / at this point, in accordance with the original plan, run the local scope of variables can be carried in the run method, garbage collection will be recycled, but found that a variable is used by another local scope, then can produce an effect, is run the local scope of other variables will be to get rid of garbage collection mechanism, The A variable will be stored in memory.
let num = me() // The value of variable A is read in memory and returned by incrementing. The answer is no, a is still in memory and the value of num here is 12
console.log(num);  // -- 12

let num2 = me() // It is obvious that the me method returned twice with the same memory address as the a variable
console.log(num2); // -- 13

Copy the code

The same is true for function arguments, so a function declares a variable, so it declares a variable in its local scope without using the var let const flag. Look at the code

function run(a) {

  function asd() {
    a += 1
    return a
  }

  return asd
}

let me = run(1)
let num = me()
console.log(num); // -- 2
let num2 = me()
console.log(num2); // -- 3

// It is possible to express the parameters of a function.
Copy the code

Object.defineProperty

Can look at the link Object directly. DefineProperty () – JavaScript | MDN (mozilla.org), if you don’t understand again to see my explanation.

This method directly defines a new property on an object, or modifies an existing property of an object, and returns the object.

let obj = {}

Object.defineProperty(obj, 'a', {
  value: '111',})console.log(obj); // -- { a : '111' }

Object.keys(obj).forEach(key= > console.log(key)); // -- 'a'

// This method defines a new attribute directly on an object and can set the value
Copy the code
let obj = {}

Object.defineProperty(obj, 'a', {
  value: '111'.enumerable: false // We can set whether this key is enumerable or not, if it is false we can use either for... in... Keys and so on will not read it
})

console.log(obj); // -- { a : '111' }

Object.keys(obj).forEach(key= > console.log(key)); // -- no print

for (let key in obj) {
  console.log(key); // No print
}

Copy the code
let obj = {}

Object.defineProperty(obj, 'a', {
  value: '111'.writable: false // Sets whether the value can be changed, or not if it is false
})

console.log(obj); // -- { a : '111' }

obj.a = '222'

console.log(obj) // -- { a : '111' }
Copy the code

Get and set down here

Get: represents the function that fires when the property is used, and the return value of the function is the value obtained when the property is used

Set: the function that fires when the property is set to a new value. The function takes one argument, which is the new value you set

// The following image is the print of the code block after it runs. It is found that the maximum stack memory is exceeded
let obj = {
  a: 11
}

Object.defineProperty(obj, 'a', {
  get() {
    console.log('OBj. A is used');
    return 'I'm a'
  },
  set(val) {
		// console.log(val) // -- 12
    obj.a = val; // This is because if you assign obj. A to obj}})let a = obj.a // -- 'I am a'

obj.a = 12
Copy the code

How to solve this problem, see the following code

Solution 1

// We can use a variable to accept the new value and return the variable we set the new value to when we get

let obj = {
  a: 11
}
let temp;

Object.defineProperty(obj, 'a', {
  get() {
    console.log('OBj. A is used');
    return temp
  },
  set(val) {
    temp = val;
  }
})

obj.a = 12

let as = obj.a
console.log('As has the value:'.as);
// Here is the result
Copy the code

Solution 2

This is where closure is used, remember we said that the argument to a function is actually declaring a new variable, and the argument variable here is equivalent to the temp in our code block up there and it’s just a mediator

let obj = {
  a: 11
}
function defineReactive(obj, key, value) {
  Object.defineProperty(obj, key, {
    get() {
      return value;
    },
    set(newValue) {
      value = newValue;
    }
  }) 
}

defineReactive(obj)
let as = obj.a 
console.log(as);
obj.a = 22
console.log(obj);
Copy the code

Design pattern -- Subscribe publish pattern

I think this article is quite well written, you can go to see the observer mode vs publish and subscribe mode – Zhihu.com after reading each article, come back to see my explanation

Publish and subscribe is one to many. Let’s take a real life example

“There is a sales office, the sales office has a boss (DEP) sales office has three salespeople (watcher) now Zhang SAN, Li Si, Wang Wu to buy a house, they were respectively received by a sales office, but now there is no house search some sales let her corresponding customers go back and so on notice, suddenly one day, the house has, Dep informs every Watcher that a house is available, Watcher informs all of his customers that a house is available, and all of their customers come and sell their houses when they hear about it. This is an example of a publish and subscribe model.”

Building the base Directory

  1. First we create a new folder 📁my-vue(My VUE)
  2. So let’s donpm init -yInitialize our project
  3. useyarn add viteInstall the Vite environment
  4. Create files in the root directoryindex.jsandindex.htmlAnd write the text we want to see in the HTML
  5. Run from the command lineviteNow that our project is running, click on the link to see the page we wrote in the HTML

The overall function

Since we’re only looking at data-driven views, we’ll just implement data

The theoretical knowledge

Vue uses a combination of data hijacking and the publisheer-subscriber pattern to realize the responsivity of data. It hijabs the setter and getter of data through Object.defineProperty to publish messages to subscribers when data changes. The subscriber receives the message and processes it accordingly. .

Theory of implementation

The get method is first registered in the Observer process, which is used for “dependency collection.” It will have a Dep object in its closure that will hold an instance of the Watcher object. The process of “dependency collection” is to store the Watcher instance into the corresponding Dep object. The get method stores the current Watcher object (dep.target) in its subs (addSub). When data changes, set calls notify of the Dep object to notify all of its internal Watcher objects of view updates.

In my own words, I used the Object.defineProperty API to add get (use it) and set (change it) methods to all attributes in data. In Compiler class, variables in the data we use on the page are bound with a Watcher listener, and this listener is the general manager of Dep. When the page data changes, the set method is triggered to notify the corresponding manager. Let him tell each Watcher listener to trigger their own page update method.

Data driven prologue

  1. Write the basic configuration of the vue class in index.js

    • Save the DOM element to mount. By default we bind the Dome element with the ID app
    • Execute the data function passed in to retrieve the data object it returns
    class Vue {
      constructor(option) {
        this.$el = option.el ?
          document.querySelector(option.el)
        :
          document.querySelector('#app')
        
        this.$data = option.data()
        
      }
    }
    
    Copy the code
  2. We want to be able to access data directly from data using this

    • In the Vue class_proxyDataMethod that takes a parameter (data)
    • throughObject.keysThe getKey () method extracts all the keys of an object into an array
    • ForEach iterates through an array of data key values
    • throughObject.definePropertyMethod implementation can implement this access data requirements
    • Execute _proxyData in the constructor
    class Vue {
      constructor(option) {
        this.$el = option.el ?
          document.querySelector(option.el)
        :
          document.querySelector('#app')
        
        this.$data = option.data()
        
        this._proxyData(this.$data)
      }
    
    
      _proxyData(data) {
        Object.keys(data).forEach(key= > {
          Object.defineProperty(this, key, {
            get() {
              return data[key]
            },
            set(newValue) {
              if(newValue === data[key]) return
    
              data[key] = newValue
            }
          })
        })
      }
      
    }
    Copy the code

    So before we get down to this, let’s go ahead and hit the code and see if it works

    let vue = new Vue({
      data() {
        return {
          name: 'I'm console without log'}}})console.log(vue.$data.name); // I have no log for console
    console.log(vue.name); // I am console without log
    Copy the code

    No problem at all. That’s it. Heh heh.

  3. Implement the Observer class Observer

    • Create observer.js in the root directory, and create the Observer class
    • We usedObject.definePropertyJust to realize that this can access data, this time we re using data in dataObject.defineProperty, this time is to listen to data changes in data
    export class Observer {
      constructor(data) {
        this.walk(data)
      }
    
      walk(data) {
        if(! data ||typeofdata ! = ='object') return
        Object.keys(data).forEach(key= > {
          this.defineReactive(data, key, data[key])
        })
      }
    
      defineReactive(obj, key, value) {
        const self = this
        self.walk(value)
    
        Object.defineProperty(obj, key, {
          get() {
            return value
          },
          set(newValue) {
            if(newValue === value) return
            value = newValue
            self.walk(value)
          }
        })
      }
      
    }
    Copy the code

    Just a moment, LET me draw a flow chart and explain it to you……

    Ok! finished

    There are no changes to the view yet, don’t worry because there are still three classes to write, we will now use the Observer class for Vue

    import {Observer} from './Observer'
    
    class Vue {
      constructor(option) {
        this.$el = option.el ?
          document.querySelector(option.el)
        :
          document.querySelector('#app')
        
        this.$data = option.data()
    
        this._proxyData(this.$data)
    
        new Observer(this.$data)
      }
    
    
      _proxyData(data) {
        Object.keys(data).forEach(key= > {
          Object.defineProperty(this, key, {
            get() {
              return data[key]
            },
            set(newValue) {
              if(newValue === data[key]) return
    
              data[key] = newValue
            }
          })
        })
      }
    }
    Copy the code

    Now that we’ve been writing the logic for a while but can’t see it in action, let’s implement a compiler that can see the page directly.

  4. Compiler class

    • To achieve the DOM text node compilation
    • Create it in the root directoryCompiler.jsfile
    export class Compiler {
      constructor(vm) {
        this.vm = vm
        this.el = vm.$el
    
        this.compile(this.el)
      }
    
      compile(el) {
        // Get all the child nodes of the element
        let childNodes = [...el.childNodes]
        childNodes.forEach(node= > {
    			// If it is a text node
          if(this.isTextNode(node)) {
            this.compileText(node)
            
            // If it is an element node
          } else if(this.isElementNode) {
    
          }
    			
          // Determine if there are any children below the element, and recurse if there are
          if(node.childNodes && node.childNodes.length) {
            this.compile(node)
          }
    
        })
      }
      compileText(node) {
        let val = node.textContent
        let reg = / \ {\ {(. +?) \} \} /
    
        if(reg.test(val)) {
          // Get the value inside the double braces
          let key = RegExp.$1.trim()
          node.textContent = val.replace(reg, this.vm[key])
        }
      }
    
      compileElement(node) {
        // Don't write anything here
      }
    
      isTextNode(node) {
        return node.nodeType === 3
      }
    
      isElementNode(node) {
        return node.nodeType === 1}}Copy the code

    I still have to write a flow chart. Hold on!

    Ok, there we go again

    Hey hey this time we can see the effect immediately !!!!!!!!!

    We do this in index.html!

    <div id="app">
      {{name}}
    </div>
    
    <script type="module" src="./index.js"></script>
    Copy the code

    Use the Compiler class in the Vue class and write it that way

    import {Observer} from './Observer'
    import {Compiler} from './Compiler'
    
    class Vue {
      constructor(option) {
        this.$el = option.el ?
          document.querySelector(option.el)
        :
          document.querySelector('#app')
        
        this.$data = option.data()
    
        this._proxyData(this.$data)
    
        new Observer(this.$data)
    
        new Compiler(this) // Use it
      }
    
    
      _proxyData(data) {
        Object.keys(data).forEach(key= > {
          Object.defineProperty(this, key, {
            get() {
              return data[key]
            },
            set(newValue) {
              if(newValue === data[key]) return
    
              data[key] = newValue
            }
          })
        })
      }
    }
    // Write it here
    new Vue({
      data() {
        return {
          name: 'I'm console without log'}}})Copy the code

    Next look at our page !!!!

    It’s such a good thing. It’s awesome

    The following two classes are a bit tricky to explain, so my solution is to write them all in one go and draw a whole flow chart! Get it in seconds.

  5. Dep class

    • Create it in the root directoryDep.js
    • withaddSubMethods can be used in the presentDepObjectWatcherSubscribe operation;
    • withnotifyMethod notice presentDepThe object’ssubsAll of theWatcherObject triggers the update operation
    export class Dep {
      constructor() {
        this.subs = []
      }
    
      addSub(sub) {
        if(sub && sub.update) {
          this.subs.push(sub)
        }
      }
    
      notify() {
        this.subs.forEach(sub= > {
          sub.update()
        })
      }
    }
    Copy the code
  6. Watcher class

    • Create it in the root directoryWatcher.js
    • It adds a listener to each variable used on the page and receives notification whenever data changes in data to update the page. If this variable is not used on the page, then it would be a waste of time to have it bind the view update method. —- Please pay attention to this sentence
    
    import {Dep} from './Dep'
    
    export class Watcher {
      constructor(vm, key, cd) {
        this.vm = vm
        this.key = key
        this.cd = cd
        Dep.nb = this
    
        this.oldValue = vm[key]
    
        Dep.nb = null
        
      }
    
      update() {
        let newValue = this.vm[this.key]
    
        if(newValue === this.oldValue) return
    
        this.cd(newValue)
      }
    }
    Copy the code

    Now let’s use these two classes

    Watcher is used in Compiler

    import { Watcher } from "./Watcher"
    
    export class Compiler {
      constructor(vm) {
        this.vm = vm
        this.el = vm.$el
    
        this.compile(this.el)
      }
    
      compile(el) {
        let childNodes = [...el.childNodes]
        childNodes.forEach(node= > {
    
          if(this.isTextNode(node)) {
            this.compileText(node)
          } else if(this.isElementNode) {
    
          }
    
          if(node.childNodes && node.childNodes.length) {
            this.compile(node)
          }
    
        })
      }
      compileText(node) {
        let val = node.textContent
        let reg = / \ {\ {(. +?) \} \} /
    
        if(reg.test(val)) {
          let key = RegExp.$1.trim()
          node.textContent = val.replace(reg, this.vm[key])
    			
          new Watcher(this.vm, key, (newValue) = > { // In this case, the variable used by the page will be set to update the page method
            node.textContent = newValue
          })
        }
      }
    
      compileElement(node) {
        // Don't write anything here
      }
    
      isTextNode(node) {
        return node.nodeType === 3
      }
    
      isElementNode(node) {
        return node.nodeType === 1}}Copy the code

    Dep is used in the Observer

    import { Dep } from "./Dep"
    
    export class Observer {
      constructor(data) {
        this.walk(data)
      }
    
      walk(data) {
        if(! data ||typeofdata ! = ='object') return
        Object.keys(data).forEach(key= > {
          this.defineReactive(data, key, data[key])
        })
      }
    
      defineReactive(obj, key, value) {
        const self = this
        self.walk(value)
        let dep = new Dep() / / here
        Object.defineProperty(obj, key, {
          get() {
            Dep.nb && dep.addSub(Dep.nb) / / here
            return value
          },
          set(newValue) {
            if(newValue === value) return
            value = newValue
            self.walk(value)
            dep.notify() / / here}}}})Copy the code

    Look at the theory again, and then look at the whole flow chart

    The get method is first registered in the Observer process, which is used for “dependency collection.” It will have a Dep object in its closure that will hold an instance of the Watcher object. The process of “dependency collection” is to store the Watcher instance into the corresponding Dep object. The get method stores the current Watcher object (dep.target) in its subs (addSub). When data changes, set calls notify of the Dep object to notify all of its internal Watcher objects of view updates.

    As a reminder, if the data in the data is changed, the set method bound to the Observer will be run, which can be combined with my flow chart

    play

    This is the end of the data impact view principle, so we need to bind events to change the data in the data, which I can’t bear. Let’s play with it

    Remember when we decided there was nothing to do if it was an element node, what did we do next

    Add $methods to the Vue class

    import {Observer} from './Observer'
    import {Compiler} from './Compiler'
    
    class Vue {
    
      constructor(option) {
        this.$el = option.el ?
          document.querySelector(option.el)
        :
          document.querySelector('#app')
        
        this.$data = option.data()
    
        this.$methods = option.methods ? option.methods : {}
        this._proxyData(this.$data)
        this._initMethods(this.$methods)
        this._proxyMethods(this.$methods)
    
        new Observer(this.$data)
        new Compiler(this)}_proxyData(data) {
        Object.keys(data).forEach(key= > {
          Object.defineProperty(this, key, {
            get() {
              return data[key]
            },
            set(newValue) {
              if(newValue === data[key]) return
              data[key] = newValue
            }
          })
        })
      }
      _proxyMethods(methods) {
        Object.keys(methods).forEach(key= > {
          Object.defineProperty(this, key, {
            get() {
              return methods[key]
            },
            set(newValue) {
              if(newValue === methods[key]) return;
              methods[key] = newValue
            }
          })
        })
      }
      _initMethods(methods) {
        Object.keys(methods).forEach(key= > {
          methods[key] = methods[key].bind(this)}}}Copy the code

    Add the compileElement method in Compiler

    compileElement(node) {
    		let self = this,
    			props = node.getAttributeNames(),
    			reg = /^c-(\w+)/,
    			reg2 = / / / ((. +) \),
    			reg3 = / \ '\' (. +),
    			reg4 = /^\d+$/,
    			reg5 = /(\w+)\(/,
    			value,
    			methodName
    		props.forEach((key) = > {
    			if (reg.test(key)) {
    				methodName = RegExp.$1.trim()
    			}
    
    			let qian = node.getAttribute(key)
    
    			if (reg2.test(qian)) {
    				value = RegExp.$1.trim()
    				if (reg4.test(value)) {
    					value = parseInt(value)
    				}
    				if (reg3.test(value)) {
    					value = RegExp.$1.trim
    				}
    			}
    
    			if (reg5.test(qian)) {
    				qian = RegExp.$1.trim
    			}
    			console.log(qian)
    			console.log(methodName)
    			console.log(value)
    			node.addEventListener(methodName, function (e) {
    				self.vm[qian](value ? (self.vm[value] ? self.vm[value] : value) : e)
    			})
    		})
    	}
    
    
    Copy the code

    Now we can bind methods to the instantiated Vue object in the index.js file

    let vue = new Vue({
      data() {
        return {
          name: 'I'm console without log'}},methods: {
        run() {
          this.name = 'This is really great.'}}})Copy the code

    Add to the HTML file

    <div id="app">
      <div>
      	{{name}}
      </div>
    <button c-click="run">change</button>
    </div>
    
    <script type="module" src="./index.js"></script>
    Copy the code

conclusion

This is my first article in nuggets, in fact, THE first article I tangled for a long time, do not know what to post, want to post this look that the blogger has written and write very well, ah a little inferiority. But I want to think everyone’s way of learning is not the same, may not see my can understand hey hey

I would like to give special thanks to Pongo for his guidance. He really helped me a lot and he is very kind. This is punge nuggets address, I hope you also pay more attention to Punge, really very powerful

Gitee’s warehouse address

If you like this style of my article please like and follow, there are comments let me know!!

Bye ~ see you next time