手写简易Vue

469 阅读3分钟

本文主要根据Vue数据响应原理和特点实现一个简单的 mvvm 框架。和真正的源码相比简化了许多,没有虚拟dom,diff算法等核心内容,主要实现了数据劫持,双向绑定功能。

项目创建

<!DOCTYPE html>
<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>

 <!--不再引用cdn 上的vue-->
  <!-- <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:'<p>快来给我<span style="color:red">点赞</span>吧</p>'
      }
    })

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

我们的目标就是将数据MyVue中的数据渲染视图层view上,并且当用户交互时,修改model中数据,同时改变依赖该数据的view层节点,更新显示新的数据。

image.png

需求分析

我们的思路大致如下图所示 image.png

  • 建立 observer类用于监听数据变化,并且负责监听数据变化
  • observer在遍历属性劫持数据的过程中,创建 dep实例,用于添加订阅者wathcer(依赖收集),以及今后通知变化
  • 解析模板,创建watch实例用于订阅数据变化并且绑定更新函数,同时初始化视图

数据劫持和Observer

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

class Observer{
   constructor(obj){
     //是否是数组判断
      if(Array.isArray(obj)){
       //todo 数组变化

      }else{
        this.walk(obj)

      }

   }

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

     })
   }

}

function defineReactive(obj,key,value){
   //递归遍历
   //劫持数据
   observe(value);

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

     set(val){
       //需要这样写
       if (val !== value) {
           //如果传入的是对象,需要继续观测
           observe(val)
           console.log(`set ${key}=>${val}`);
           value=val
       }
         
     }

   })
}


//测试
const data={
        counter: 1, 
        desc:'<p>YOYO很棒</p>',
           obj:{
               foo:'foo1',
               bar:'bar1'
           }
      }
observe(data)

setTimeout(()=>{
    data.counter=2; //set counter=>2
    data.desc;  //get desc=<p>YOYO很棒</p>
    data.obj.foo="foo2" //get obj=[object Object]因为先读的obj.obj set foo=>foo2
},1000)



初始化编译

//myVue类

class MyVue{
   constructor(options) {
       //保存选项
       this.$options = options;
       this.$data = options && options.data;
       //观测数据
       observe(this.$data)
       //代理data 到 vm 实例上  this.$data.counter => this.counter
       proxy(this)
       //遍历dom树,找到动态的表达式或者指令等,依赖收集
       new Compile(this.$el,this);
       
   } 
}

//代理data到vm实例上
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
          }

        }
    })
  })
}


// 遍历dom树,找到动态的表达式或者指令等
class Compile {
  constructor(el, vm) {
    this.$el = document.querySelector(el);
    this.$vm = vm;
    if (this.$el) {
      this.compile(this.$el);
    }
  }

  //递归遍历所有节点
  compile(el) {
    const childNodes = el.childNodes;
    childNodes.forEach((node) => {
      //如果是元素节点
      if (node.nodeType == 1) {
        //取出所有子节点进行遍历
        this.compileElement(node);
        //元素节点下有子节点
        if (node.hasChildNodes()) {
          //递归遍历
          this.compile(node);
        } // 形如{{xx}}
      } else if (this.isInter(node)) {
        //处理动态表达式
        this.compileText(node);
      }
    });

  }
  //所有的初始化及更新操作通过update转发
  //exp--counter dir--text
  update(node, exp, dir) {
    //初始化
    const fn = this[dir + "Updater"];
    fn && fn(node,this.$vm[exp])
  }
  //处理 v-text
  textUpdater(node, value) {
    node.textContent = value;
  }
  //处理 v-html
  htmlUpdater(node,value){
    node.innerHTML=value
  }
  //处理元素节点
  compileElement(node) {
    //处理指令内容
    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");
  }
  // 形如{{xx}}
  isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }
}

初始化编译成功

image.png

依赖收集

视图中会用到data中的某个key,这就称为依赖. 同一个key可能出现多次. 每次都需要收集出来用一个watcher维护,此过程称为依赖收集. 多个watcher需要统一一个Dep管理,更新时需要这个Dep统一通知

image.png

实现思路

  • defineReactive时为每个key创建一个Dep实例
  • 初始化视图时读取某个key,比如name1,创建watcher1
  • 利用name1读取时触发getter方法,将watcher1 添加到对应dep实例中
  • 当name1更新触发setter方法,通过对应dep实例通知其管理的所有watcher
class Compile{
  //...
    update(node, exp, dir) {
    //内层是 updater

    //初始化
    const fn = this[dir + "Updater"];
    console.log(`this.$vm[exp]`,this.$vm[exp])
    fn && fn(node,this.$vm[exp])
    
    //wacher绑定更新函数,用于执行今后数据的变更
    new Wathcer(this.$vm,exp,val=>{
      fn(node,val)
    })
  }
  
  
  //...

}




class Wathcer{
  constructor(vm,exp,fn){
     this.$vm=vm;
     this.key=exp;
     this.updaterFn=fn;
     //这里是依赖收集的关键
      Dep.target=this;
     //要触发 defineProperty的get
     vm[this.key]
     Dep.target=null
  }
  //未来执行更新时用
  update(){
    this.updaterFn.call(this.$vm,this.$vm[this.key])
  }

}

class Dep{
  constructor(){
    //用于放置 watcher
     this.deps=[];
  }
   //保存watcher
  addDep(watcher){
     this.deps.push(watcher)
  }
 //通知变更
  notify(){
    this.deps.forEach(watcher=>{
      watcher.update()
    })
  }

}

function defineReactive(obj,key,value){
   //递归遍历
   //劫持数据
   const dep=new Dep() //每一个key对应会有一个dep
   observe(value);
   Object.defineProperty(obj,key,{
     get(){
       console.log(`get ${key}=${value}`)
       //每一个绑定会生成watcher,在这里被收集
       if(Dep.target){
         dep.addDep(Dep.target)
       }
       return value
     },

     set(val){
       //需要这样写
       if (val !== value) {
           console.log(`set ${key}=>${val}`);
           observe(val)
           value=val
           //变更时触发变更
           dep.notify()
       }
         
     }
   })
}

doubleCounter2.gif

完整myVue代码


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



function defineReactive(obj,key,value){
   //递归遍历
   //劫持数据
   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){
       //需要这样写
       if (val !== value) {
           console.log(`set ${key}=>${val}`);
           observe(val)
           value=val
           dep.notify()
       }
         
     }
   })
}

class Observer{
   constructor(obj){
     //是否是数组判断
      if(Array.isArray(obj)){
       //todo 数组变化

      }else{
        this.walk(obj)

      }

   }

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

     })
   }

}

class MyVue{

   constructor(options) {
     //保存选项
     this.$options = options;
     this.$el = options && options.el;
     this.$data = options && options.data;
     //观测数据
     observe(this.$data);
     
     //代理 data 到 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
          }

        }

    })

  })

}


// 遍历dom树,找到动态的表达式或者指令等
class Compile {
  constructor(el, vm) {
    this.$el = document.querySelector(el);
    this.$vm = vm;
    if (this.$el) {
      this.compile(this.$el);
    }
  }

  //递归遍历所有节点
  compile(el) {
    const childNodes = el.childNodes;
    childNodes.forEach((node) => {
      if (node.nodeType == 1) {
        //取出所有子节点进行遍历
        this.compileElement(node);
        //元素节点下有子节点
        if (node.hasChildNodes()) {
          this.compile(node);
        } // 形如{{xx}}
      } else if (this.isInter(node)) {
        this.compileText(node);
      }
    });


  }
  //所有的初始化更新操作通过update转发
  //exp--counter dir--text
  update(node, exp, dir) {
    //内层是 updater

    //初始化
    const fn = this[dir + "Updater"];
    console.log(`this.$vm[exp]`,this.$vm[exp])
    fn && fn(node,this.$vm[exp])
    
    //新增
    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
  }

  //更新函数
  compileElement(node) {
    //处理指令内容
    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) {
    // 形如{{xx}}
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }
}


//新增
class Wathcer{
  constructor(vm,exp,fn){
     this.$vm=vm;
     this.key=exp;
     this.updaterFn=fn;
     Dep.target=this;
     //要出发
     vm[this.key]
     Dep.target=null
  }
  //未来执行更新时用,不需要使用新的val
  update(){
    this.updaterFn.call(this.$vm,this.$vm[this.key])
  }

}

class Dep{
  constructor(){
    //用于放置 watcher
     this.deps=[];
  }
   
  addDep(watcher){
     this.deps.push(watcher)
  }

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

}