vue响应式的简单实现——手写实现MVVM

我们知道,vue框架的一个特点之一就是它的响应式,在视图层/控制台对对象进行操作时,会影响对应的视图。

它的核心是数据劫持Object.defineProperty来实现的,通过监听数据的变化(getter和setter函数)来实时编译新的模板,在vue底层中,尤大大是在这个方法中实现的Vue.util.defineReactive,里面正是基于数据劫持来实现响应式原理的。

下面来一步步地模仿这个方法,简单实现一下vue的响应式!
ps:下面是又长又臭的代码,尽量写多注释,请耐心观看

  1. 首先,我们和平时用vue框架一样的步骤:先创建一个vue实例vm,顺便写几行简单的html代码,方便后续的观察。
 <div id="app">
    <input type="text" v-model="name">
     <p>{{name}}</p>
     <p>{{age}} </p>
 </div>
 <script>
 //注意:Vue构造函数的参数是一个对象,里面是vue实例的配置项(option)
  let vm = new Vue({
     el:'#app',
      data:{
          name:'Koi',
          age:11
      }
  })
 </script>
  1. 往常这时候呢,我们是需要引入vuejs的文件的,才能在页面上看到我们要的效果,因为浏览器是无法编译类似{{ }}v-model这样的语法的。下面开始来手写啦!
//首先创建一个Vue构造函数
function Vue(option){
	this.$el = document.querySelector(option.el);
	this.$data = option.data;
	//先进行数据劫持==》对应第三点
	observe(this.$data,this);
	//再进行模板编译===》对应第四点
	nodeToFragment(this.$el,this.$data,this)
}
  1. 数据劫持
function observe(data){
	if(({}).toString.call(data) !== '[object Object]') return;//确保data是一个对象!
	//我们要对data对象中的各个属性进行数据劫持,所以先获取属性,然后遍历实现数据劫持
	Object.keys(data).forEach(key=>{
		//key对应data中的各个属性,例如name、age
		//进行数据劫持(要操作的对象,要操作的对象的属性,要操作的对象属性的属性值)
		defineReactive$$1(data,key,data[key]);
	})
}
function defineReactive$$1(data,key,val){
	Object.defineProperty(data,key,{
		emunerable:true;//使属性key可枚举
		get(){//当访问data.key的时候,触发get函数
			return val;
		}
		set(newV){//当data.key=xxx的时候,触发set函数
			val = newV;
		}
	}
}
  1. 模板编译
function nodeToFragment(el,data){
	let fragment = document.createDocumentFragment();//把文档上的节点转移到文档碎片上
	let child;
	while(child = el.firstChild){//当el有子节点时,把它的第一个子节点赋值给child(这里包括空白文本节点)
		complie(child);//编译模板
		flagment.appendChild(child);
	}
	//最后把文档碎片插入到页面上
	el.appendChild(flagment);
}
function compile(node,vm){
    //先判断节点类型nodeType 1(元素节点) 3(文本节点) 8(注释节点) 
    // 先处理元素节点
    if(node.nodeType === 1){
     //    console.dir(node)
        // 处理行内属性
         let arrs = node.attributes;
         [...arrs].forEach(item=>{
             // console.dir(item)
             if(/^v-/.test(item.nodeName)){//获取v-开头的行内属性(v-model)
                 let vName = item.nodeValue;//获取name这个属性
                 let val = vm.$data[vName];//获取name属性的属性值 Koi
                 node.value = val;//把Koi 放到input框中
             }
         });
         //除了行内属性,还要考虑子节点
         [...node.childNodes].forEach(item=>{
             compile(item,vm);//递归编译子节点
         })
    }else{
        //处理文本节点
         // console.dir(node);
         let str = node.textContent;//获取文本字符串 {{name}}/{{age}}
         str = str.replace(/\{\{(\w+)\}\}/,(a,b)=>{
         	//a是匹配的值({{name}}/{{age}}),b是匹配到的第一个分组(即括号里面的,name/age)
             return vm.$data[b];//把str中的{{name}}替换成Koi,{{age}}替换成11
         })
         node.textContent = str;//把具体值Koi,11放到文本节点的textContent属性中
    }
}

到这里我们已经完成一大半啦,完成了对Vue实例中,配置项data的数据劫持,以及将配置项el对应的css选择器对应的页面进行模板编译。

但是这时候又有问题了——此时它们俩是没有连接关系的,我在视图层input框任意输入文字(即试图改变v-mode=‘name’中name的属性值),却发现,下面的{{name}}并没有任何反应。

别急,因为我们还没实现呢!因为模板是页面一更新就编译好了的,数据劫持虽然知道数据发生改变了,可是却无法通知到模板去重新编译,所以我们需要给它们架起一座桥梁!这里采用订阅者/观察者模式。

简单讲一下订阅者/观察者模式。例如公众号,它可以看成一个订阅器,一旦公众号更新推文,就会通知它的订阅者们:你们可以去看啦!这里的订阅器就是我们劫持的每个数据,一旦发现劫持的数据发生改变,就会通知它的订阅者(观察者/模板编译):我劫持的数据更新啦,你们可以去更新模板啦!

  1. 订阅者/观察者模式
//订阅者/观察者模式
class Dep{
    constructor(){
        this.subs = [];
    }
    // 添加订阅者(事件池)
    addSub(sub){
         this.subs.push(sub);
    }
    // 通知订阅者做对应的事件
    notify(){
         this.subs.forEach(sub=>{
             //通知订阅者发布事件了(如果数据发生变化,通知观察者及时更新数据)
             sub.update();// 让对应的事件执行  sub 就是哪些 watcher
         })
    }
}
// 观察者
class Watcher{//node key vm
     constructor(node,key,vm){
         Dep.target = this;//
         this.node = node;
         this.key = key;
         this.vm = vm;
         this.getValue();//到这里可以把当前的Watcher实例放在Dep事件池中
         Dep.target = null;
     }
     getValue(){
         this.val = this.vm.$data[this.key];//会触发get函数
     }
     update(){//页面更新数据
         this.getValue();//获取最新的dom值
         if(this.node.nodeType === 1){
             this.node.value = this.val;
         }else{
             this.node.textContent = this.val;
         }
     }
}

然后呢,我们需要在前面代码的基础上进行简单的调整,为了方便观看,我把之前的代码截图,并对一些需要改变的地方标红
在这里插入图片描述
在这里插入图片描述
这样子我们就简单的实现了vue的响应式啦!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值