Vue-简单实现双向数据绑定
1.前言本文主要参考了坦洲课堂_雷米老师的课堂录播视频。本文适合正在学习Vue源码的初学者。看完之后,你会对Vue的双向数据绑定原理有一个大概的了解,了解Observer、Compile、Wathcer这三个角色(如下图所示)及其作用。.本文将带你一步步实现简单版的双向数据绑定。每一步都会详细分析这一步要解决的问题,为什么要这样写代码。因此,希望大家在看完本文后,能够自己实现一个简单版本的数据双向绑定。2.代码实现2.1目标分析本文要实现的效果如下图所示:本文主要用到的HTML和JS代码如下:
letvm=newVue({el:"#app",data:{msg:"helloworld",msg2:"helloxiaofei"}})我们将按照下面分三步实现:首先第一步:将data中的数据同步到页面中,实现M==>V的初始化;第二步:当在输入框中输入一个值时,将新值同步到data中,实现V==>M的绑定;第三步:当data数据更新时,触发页面发生变化,实现绑定M==>V。2.2实现过程2.2.1入口代码首先,我们需要创建一个Vue类,它接收一个options对象,同时我们需要在options对象中保存有效信息;那么,我们主要有三个模块:Observer、Compile、Wathcer,其中Observer是用来进行数据劫持的,Compile是用来解析元素的,Wathcer是一个观察者。可以写出如下代码:(Observer、Compile、Wathcer这三个概念不用详细研究,后面会详细解释)。classVue{//接收传入的对象constructor(options){//保存有效信息this.$el=document.querySelector(options.el);this.$data=options.data;//Container:{Attribute1:[wathcer1,wathcer2...],attribute2:[...]},用于存放每个属性观察者this.$watcher={};//解析元素:实现Compilethis.compile(this.$el);//要解析元素,你必须传入元素//劫持数据:implementObserverthis.observe(this.$data);//劫持数据,必须传入数据}compile(){}observe(){}}2.2.2页面初始化这一步我们需要对页面进行初始化,即解析出v-text和v-model指令,将data中的数据渲染到页面。这一步的关键是实现compile方法,那么如何解析el元素呢?思路如下:首先获取el下的所有子节点,然后遍历这些子节点。如果子节点有子节点,那么我们就需要用到递归的思想;遍历子节点找到所有带指令的元素,并将相应的数据渲染到页面。代码如下:(主要看编译部分)classVue{//接收传入的对象constructor(options){//获取有用信息this.$el=document.querySelector(options.el);this.$data=options.data;//容器:{属性1:[wathcer1,wathcer2...],属性2:[...]}this.$watcher={};//2.解析元素:实现Compilethis.compile(this.$el);//要解析元素,必须将元素传入//3.劫持数据:implementObserverthis.observe(this.$data);//劫持数据,就得把数据传入}compile(el){//分析元素下的每个子节点,所以得到el.children//备注:children返回元素集合,childNodes返回节点集合让节点=el.children;//解析每个子节点的指令for(vari=0,length=nodes.length;i
{this.$data[attrVal]=ev.target.value;//你可以尝试在这里执行:console.log(this.$data),//just可以看到每次在输入框中输入文字,data中的msg值也会发生变化})}}}observe(data){}}2.2.4数据影响视图至此,我们实现了:当我们在输入框中输入字符时,data中的数据会自动更新;本节的主要任务是:当data中的数据更新时,数据绑定的元素会自动更新页面上的view具体思路如下:1)我们会实现一个Wathcer类,它有一个update方法来更新页面。观察者的代码如下:classWatcher{constructor(node,updatedAttr,vm,expression){//保存传入的值,这些数据就是渲染页面时要用到的数据this.node=node;这。更新属性=更新属性;这个.vm=虚拟机;this.expression=表达式;这个.更新();}update(){this.node[this.updatedAttr]=this.vm.$data[this.expression];}}2)想象一下,我们应该向哪些数据添加观察者?何时向数据添加观察者?在解析一个元素的时候,当v-text和v-model指令都被解析出来的时候,就意味着这个元素需要和数据进行双向绑定,所以这个时候我们给容器添加观察者。我们需要用到这样一个数据结构:{property1:[wathcer1,wathcer2...],property2:[...]},如果不是很清楚可以看下图:可以看到:有一个$wathcer对象,$wathcer的每个属性对应每一个需要绑定的数据,value是一个数组,用来存放观察数据的观察者。(备注:Dep是Vue源码中专门创建的类,对应这里说的数组,本文是精简版,不再过多介绍。)3)劫持数据:利用对象的访问器属性getter和setter被用作data更新时,触发一个动作。这个动作的主要目的是让所有观察到数据的观察者执行update方法。综上所述,本节我们要做的工作:实现一个Wathcer类;在解析指令时(即在compile方法中)添加观察者;实施数据劫持(实施观察方法)。完整代码如下:classVue{//接收传入的对象constructor(options){//获取有用信息this.$el=document.querySelector(options.el);this.$data=options.data;//容器:{属性1:[wathcer1,wathcer2...],属性2:[...]}this.$watcher={};//解析元素:实现编译this.compile(this.$el);//要解析元素,你必须传递元素//劫持数据:implementObserverthis.observe(this.$data);//劫持数据,必须把数据传入}compile(el){//解析元素el.children的每个子节点,所以得到el.children//扩展:children返回元素集合,childNodes返回节点设置让节点=el.children;//每个子节点的解析指令for(vari=0,length=nodes.length;i{this.$data[attrVal]=ev.目标值;})if(!this.$watcher[attrVal]){this.$watcher[attrVal]=[];}//与上面使用的innerHTML不同,这里的input使用了vaule属性this.$watcher[attrVal].push(newWatcher(node,"value",this,attrVal))}}}observe(data){Object.keys(data).forEach((key)=>{letval=data[key];//这个val会一直保存在内存中,每次访问data[key],都是在访问这个val对象。defineProperty(data,key,{get(){returnval;//不能直接返回data[key],否则会陷入死循环},set(newVal){if(val!==newVal){val=newVal;//同理这里也不能直接设置data[key],会陷入死循环this.$watcher[key].forEach((w)=>{w.update();})}}})})}}classWatcher{constructor(node,updatedAttr,vm,expression){//保存传入的值this.node=node;this.updatedAttr=updatedAttr;这个.vm=vm;this.expression=表达式;这个.更新();}update(){this.node[this.updatedAttr]=this.vm.$data[this.expression];}}letvm=newVue({el:"#app",data:{msg:"helloworld",msg2:"helloxiaofei"}})至此,代码完成3.未来计划运用知识设计模式分析上面源码中的问题,并与Vue源码对比,是对Vue源码的分析