This paper mainly based on Vue data response principle and characteristics to achieve a simple MVVM framework. And the real source code compared to a lot of simplified, no virtual DOM, diff algorithm and other core content, the main realization of data hijacking, two-way binding function.

Project creation

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <p>{{counter}}</p>
    <p v-html="desc"></p>
  </div>

 <! -- No longer reference vue on CDN -->
  <! -- <script src=".. /node_modules/vue/dist/vue.js"></script> -->
  <script src="myvue.js"></script>
  <script>
    const app = new MyVue({
      el: '#app'.data: {
        counter: 1.desc:

}})setInterval(() = > { app.counter++ }, 1000);
</script> </body> </html> Copy the code

Our goal is to render the data in MyVue onto the view layer, and when the user interacts, modify the data in the model, change the view layer nodes that depend on the data, and update the new data to display.

Demand analysis

Our thinking is roughly shown in the figure below

  • Create an Observer class to listen for and be responsible for data changes
  • The Observer creates an instance of the DEP as it traverses the attribute hijacking data, which is used to add subscriber Wathcer (dependency collection) and notify future changes
  • Parse the template, create an instance of Watch to subscribe to data changes and bind the update function, and initialize the view

Data hijacking and the Observer

function observe(obj){
   if(typeofobj ! = ='object' ||  obj===null) {return obj
   }
   
   new Observer(obj)
   
}

class Observer{
   constructor(obj){
     // Check whether it is an array
      if(Array.isArray(obj)){
       // Todo array changes

      }else{
        this.walk(obj)

      }

   }

   walk(obj){
     Object.keys(obj).forEach(key= >{
         defineReactive(obj,key,obj[key])

     })
   }

}

function defineReactive(obj,key,value){
   // recursive traversal
   // Hijacking data
   observe(value);

   Object.defineProperty(obj,key,{
     get(){
       console.log(`get ${key}=${value}`)
       return value
     },

     set(val){
       // It needs to be written like this
       if(val ! == value) {// If an object is passed in, continue observing
           observe(val)
           console.log(`set ${key}= >${val}`);
           value=val
       }
         
     }

   })
}


/ / test
const data={
        counter: 1.desc:'< p > great YOYO < / p >'.obj: {foo:'foo1'.bar:'bar1'
           }
      }
observe(data)

setTimeout(() = >{
    data.counter=2; //set counter=>2
    data.desc;  //get desc=

YOYO is great

data.obj.foo="foo2" //get obj=[object object] obj set foo=>foo2 },1000) Copy the code

Initial compilation

/ / myVue class

class MyVue{
   constructor(options) {
       // Save options
       this.$options = options;
       this.$data = options && options.data;
       // Observation data
       observe(this.$data)
       $data.counter => this.counter
       proxy(this)
       // Walk through the DOM tree to find dynamic expressions or instructions, etc., depending on collection
       new Compile(this.$el,this); }}// Proxy data to the VM instance
function proxy(vm){
  Object.keys(vm.$data).forEach(key= >{
    Object.defineProperty(vm,key,{
        // this.counter => this.$data.counter
        get(){
         console.log(`proxy, get ${key} ${vm.$data[key]}`)
         return vm.$data[key]
        },

        set(val){
          if(val! ==vm.$data[key]){ vm.$data[key]=val } } }) }) }// Walk through the DOM tree to find dynamic expressions or instructions, etc
class Compile {
  constructor(el, vm) {
    this.$el = document.querySelector(el);
    this.$vm = vm;
    if (this.$el) {
      this.compile(this.$el); }}// Recursively iterate over all nodes
  compile(el) {
    const childNodes = el.childNodes;
    childNodes.forEach((node) = > {
      // If it is an element node
      if (node.nodeType == 1) {
        // Fetch all the child nodes for traversal
        this.compileElement(node);
        // There are children under the element node
        if (node.hasChildNodes()) {
          // recursive traversal
          this.compile(node);
        } / / like {{xx}}
      } else if (this.isInter(node)) {
        // Handle dynamic expressions
        this.compileText(node); }}); }// All initialization and update operations are forwarded by update
  //exp--counter dir--text
  update(node, exp, dir) {
    / / initialization
    const fn = this[dir + "Updater"];
    fn && fn(node,this.$vm[exp])
  }
  / / v - text processing
  textUpdater(node, value) {
    node.textContent = value;
  }
  / / processing v - HTML
  htmlUpdater(node,value){
    node.innerHTML=value
  }
  // Process element nodes
  compileElement(node) {
    // Process the contents of the instruction
    const nodeAttrs = node.attributes;
    Array.from(nodeAttrs).forEach((attr) = > {
      const attrName = attr.name; // v-text
      const exp = attr.value; // xxx
      console.log(`attrName`, attrName);//v-text
      console.log(`exp`, exp);//desc
      let dir
      if(attrName){
        dir=attrName.slice(2)}this.update(node,exp,dir)

    })

  }

  compileText(node) {
    //RegExp.$1 -- counter
    this.update(node, RegExp. $1,"text");
  }
  / / like {{xx}}
  isInter(node) {
    return node.nodeType === 3 && / \ {\ {(. *) \} \} /.test(node.textContent); }}Copy the code

Initialization compilation succeeded. Procedure

Depend on the collection

A view uses a key from data, which is called a dependency. The same key may appear more than once. Each time it needs to be collected and maintained with a Watcher, this process is called dependency collection. Multiple Watchers need to be managed by a unified Dep, which requires unified notification when updates are made

Implementation approach

  • Create a Dep instance for each key when defineReactive
  • Initialize the view to read a key, such as name1, and create watcher1
  • Add watcher1 to the corresponding DEP instance by firing the getter method when name1 reads
  • When name1 updates the setter method, all watcher managed by name1 is notified through the corresponding DEP instance
class Compile{
  / /...
    update(node, exp, dir) {
    // The inner layer is the updater

    / / initialization
    const fn = this[dir + "Updater"];
    console.log(`this.$vm[exp]`.this.$vm[exp])
    fn && fn(node,this.$vm[exp])
    
    // Wacher binds the update function to perform future data changes
    new Wathcer(this.$vm,exp,val= >{
      fn(node,val)
    })
  }
  
  
  / /...

}




class Wathcer{
  constructor(vm,exp,fn){
     this.$vm=vm;
     this.key=exp;
     this.updaterFn=fn;
     // Here is the key to dependency collection
      Dep.target=this;
     // Get to trigger defineProperty
     vm[this.key]
     Dep.target=null
  }
  // For future updates
  update(){
    this.updaterFn.call(this.$vm,this.$vm[this.key])
  }

}

class Dep{
  constructor(){
    // Place the watcher
     this.deps=[];
  }
   / / save the watcher
  addDep(watcher){
     this.deps.push(watcher)
  }
 // Notify the change
  notify(){
    this.deps.forEach(watcher= >{
      watcher.update()
    })
  }

}
Copy the code

function defineReactive(obj,key,value){
   // recursive traversal
   // Hijacking data
   const dep=new Dep() // There is a deP for each key
   observe(value);
   Object.defineProperty(obj,key,{
     get(){
       console.log(`get ${key}=${value}`)
       // Each binding generates a watcher, which is collected here
       if(Dep.target){
         dep.addDep(Dep.target)
       }
       return value
     },

     set(val){
       // It needs to be written like this
       if(val ! == value) {console.log(`set ${key}= >${val}`);
           observe(val)
           value=val
           // The change is triggered when the change is made
           dep.notify()
       }
         
     }
   })
}
Copy the code

Complete myVue code


function observe(obj){
   if(typeofobj ! = ='object' ||  obj===null) {return obj
   }
   new Observer(obj)
}



function defineReactive(obj,key,value){
   // recursive traversal
   // Hijacking data
   const dep=new Dep()
   observe(value);
   Object.defineProperty(obj,key,{
     get(){
       console.log(`get ${key}=${value}`)
       if(Dep.target){
         dep.addDep(Dep.target)
       }
       return value
     },

     set(val){
       // It needs to be written like this
       if(val ! == value) {console.log(`set ${key}= >${val}`);
           observe(val)
           value=val
           dep.notify()
       }
         
     }
   })
}

class Observer{
   constructor(obj){
     // Check whether it is an array
      if(Array.isArray(obj)){
       // Todo array changes

      }else{
        this.walk(obj)

      }

   }

   walk(obj){
     Object.keys(obj).forEach(key= >{
         defineReactive(obj,key,obj[key])

     })
   }

}

class MyVue{

   constructor(options) {
     // Save options
     this.$options = options;
     this.$el = options && options.el;
     this.$data = options && options.data;
     // Observation data
     observe(this.$data);
     
     // Proxy data to the VM
     proxy(this)

     this.counter
     new Compile(this.$el, this); }}function proxy(vm){
  Object.keys(vm.$data).forEach(key= >{
    Object.defineProperty(vm,key,{
        // this.counter => this.$data.counter
        get(){
         return vm.$data[key]
        },

        set(val){
          if(val! ==vm.$data[key]){ vm.$data[key]=val } } }) }) }// Walk through the DOM tree to find dynamic expressions or instructions, etc
class Compile {
  constructor(el, vm) {
    this.$el = document.querySelector(el);
    this.$vm = vm;
    if (this.$el) {
      this.compile(this.$el); }}// Recursively iterate over all nodes
  compile(el) {
    const childNodes = el.childNodes;
    childNodes.forEach((node) = > {
      if (node.nodeType == 1) {
        // Fetch all the child nodes for traversal
        this.compileElement(node);
        // There are children under the element node
        if (node.hasChildNodes()) {
          this.compile(node);
        } / / like {{xx}}
      } else if (this.isInter(node)) {
        this.compileText(node); }}); }// All initial update operations are forwarded by update
  //exp--counter dir--text
  update(node, exp, dir) {
    // The inner layer is the updater

    / / initialization
    const fn = this[dir + "Updater"];
    console.log(`this.$vm[exp]`.this.$vm[exp])
    fn && fn(node,this.$vm[exp])
    
    / / new
    new Wathcer(this.$vm,exp,val= >{
      fn(node,val)
    })
  }

  textUpdater(node, value) {
    node.textContent = value;
  }

  htmlUpdater(node,value){
    console.log(`value`,value)
    node.innerHTML=value
  }

  // Update function
  compileElement(node) {
    // Process the contents of the instruction
    const nodeAttrs = node.attributes;
    
    Array.from(nodeAttrs).forEach((attr) = > {
      const attrName = attr.name; // v-text
      const exp = attr.value; // xxx
      console.log(`attrName`, attrName);//v-text
      console.log(`exp`, exp);//desc
      let dir
      if(attrName && attrName.indexOf("v-") = =0){
        dir=attrName.slice(2)}this.update(node,exp,dir)

    })

  }

  compileText(node) {
    //RegExp.$1 -- counter
    this.update(node, RegExp. $1,"text");
  }

  isInter(node) {
    / / like {{xx}}
    return node.nodeType === 3 && / \ {\ {(. *) \} \} /.test(node.textContent); }}/ / new
class Wathcer{
  constructor(vm,exp,fn){
     this.$vm=vm;
     this.key=exp;
     this.updaterFn=fn;
     Dep.target=this;
     / / to start
     vm[this.key]
     Dep.target=null
  }
  // For future updates, the new val is not needed
  update(){
    this.updaterFn.call(this.$vm,this.$vm[this.key])
  }

}

class Dep{
  constructor(){
    // Place the watcher
     this.deps=[];
  }
   
  addDep(watcher){
     this.deps.push(watcher)
  }

  notify(){
    this.deps.forEach(watcher= >{
      watcher.update()
    })
  }

}




Copy the code