手写MVVM

359 阅读2分钟

我们本次来手写一套VUE的MVVM的实现原理,是基于VUE2.0的Object.defineProperty来实现的

首先我们来先写几个标签,来展示一下视图部分

<div id="app">
    <h1>姓名是{{  name   }}</h1>
    <h2>年龄是{{age}}</h2>
    {{name}}
    <h3>姓名是{{  name   }};年龄是{{age}}</h3>
    <input type="text" v-model='name'>
    <input type="text" v-model='age'>
</div>

然后来编写原创建VUE实例的代码

let vm = new Vue({
    el:'#app',
    data:{
        name:"哈哈666",
        age:100,
        c:{
          b:123
        }
    }
});

接着就是实现MVVM的过程了

function observe(data){
  if(typeof data !== 'object')return;
  // 用来劫持数据
  let keys = Object.keys(data);// keys 是所有属性名组成的数组
  keys.forEach(key=>{
    defineReactive(data,key,data[key])
  })

}
function defineReactive(obj,key,value){
  //专门调用defineProperty 实现数据数据劫持
  observe(value);
  let dep = new Dep;
  Object.defineProperty(obj,key,{
    get(){
      console.log('get')
      if(Dep.target123){
        //Dep.target  就是watcher实例
        dep.addSub(Dep.target123)
      }
      return value
    },
    set(newVal){
      console.log('set')
    
      if(value !== newVal){
        value = newVal;
        observe(value);
        dep.notify();
      }
      
    }
  })
}

function nodeToFragment(node,vm){
  // 把元素节点 转移到了 文档碎片上
  let child;
  let fragment = document.createDocumentFragment();
  
  while(child = node.firstChild){
    fragment.appendChild(child)
    compile(child,vm)
  }
  // while循环  只是 把node中的每一个子节点 都转移到了 fragment上
  // 转移完成之后  页面中的 node 节点 里边 就没有元素了
  node.appendChild(fragment)
  // 又把 fragment上的所有节点 一次性还给了 node
}
function compile(node,vm){
  // 判断node的节点类型 看他是不是元素节点
  
  if(node.nodeType == 1){
    //证明是元素节点  那么 我们要去处理行内属性
    let attrs = node.attributes;// 所有的行内属性,然后看那个是v-开头的
    [...attrs].forEach(item=>{
      if(/^v\-/.test(item.nodeName)){
        //证明这个属性是v-开头的
        let valName = item.nodeValue;// 获取 "name" 这个单词
        new Watcher(node,vm,valName)
        let val = vm.$data[valName];// 获取"name"对应的值:珠峰
        node.value = val;//把 珠峰 这两个字 放到input框中;
        node.addEventListener('input',(e)=>{
          //要把更改之后的input框的内容 设置给name
          vm.$data[valName] = e.target.value
        })
      }
    });
    [...node.childNodes].forEach(item=>{
      //针对有子节点的元素 接着进行编译
      compile(item,vm)
    })
  }else{
    // 文本节点
    // debugger
    let str = node.textContent; // "{{ name    }}{{  age  }}"
    node.str = str;
    // console.log(str)
    if(/\{\{(.+?)\}\}/.test(str)){
      str = str.replace(/\{\{(.+?)\}\}/g,(a,b)=>{
        // console.log(a,b)
        b = b.replace(/^ +| +$/g,'');// 去除首尾空格
        new Watcher(node,vm,b)
        return vm.$data[b]
      })
      // console.log(str)
      node.textContent = str
    }
  }
}

// 订阅器
class Dep{
  constructor(){
    this.subs = [];
  }
  addSub(sub){
    this.subs.push(sub)
  }
  notify(){
    this.subs.forEach(item=>{
      // 让对应的事件 做更新操作
      item.update();
    })
  }
}
class Watcher{
  constructor(node,vm,key){
    Dep.target123  = this;
    this.node = node;
    this.vm = vm;
    this.key = key;
    this.get123();
    Dep.target123 = null;
  }
  update(){
    this.get123();
    if(this.node.nodeType==1){
      // 就是input
      this.node.value = this.value
    }else{
      let str = this.node.str;// 姓名是{{name}}
      str = str.replace(/\{\{(.+?)\}\}/g,(a,b)=>{
        b = b.trim();
        // if(b == this.key){
        //   return this.value
        // }else{
          return this.vm.$data[b]
        // }
      })
      this.node.textContent = str
    }
  }
  get123(){
    this.value = this.vm.$data[this.key]
  }
}
/* 
  每创造一个watcher实例 我们都要把这个实例放到对应属性的事件池中
  怎么实现?
  每当 new Watcher的时候  我们都把实例 放到 订阅器的一个静态属性上;
  然后 主动出发 该属性的 对应的get函数? 是通过 主动调用实例的get123方法
  get123 这个方法使用了 对应的属性 这是就会触发对应的get
  等对应的get执行完成之后呢? 再把这个静态属性清空? 因为使用这个属性的方式很多

  我们只单单要 再模板编译的时候(new Watcher)的时才需要向事件池中添加内容
 */


function Vue(options){
  // $el  存储的是当前元素
  this.$el = document.querySelector(options.el)

  // $data存储的是data中的属性
  this.$data = options.data;
  observe(this.$data)// 数据劫持
  nodeToFragment(this.$el,this)// 模板编译
  // 我们使用观察者模式 把这两条线 联系起来
}