Vue 响应式的模拟以及 v-model 双向绑定的模拟
在第一天学习Vue时,打开了Vue的官网,看了一段视频演示,被 Vue 的响应式惊到了,非常好奇这种实时更新是如何实现的.遂查询了一些资料,其中有一些明显超出了现在我能理解的范围.但大体的思路弄明白了,赶紧总结一下.
SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。
目标 1:实现简单的响应式
思路:
所谓的响应式,就是在 vm的 data 对象中的成员属性发生变化时,会将此变化实时更新至使用此数据的页面部分上,也就是m层发生变化,v层自动更新.
首先想到的解决办法,就是在我们改变一个属性时,会触发某个事件,然后为这个事件写一个更新DOM的回调函数,就可以实现基本的响应式了.
困难:
可是之前所见过的所有的事件,都是基于 DOM 元素的,没有基于对象的属性的.
那首先看看一切的起始点,vm.$data有什么特别:
并不是想象中的单纯的 data 中的键值对,而是将data 中的键值对变为一个大对象的一部分,然后添加了一些 get 和 set 方法.还有一个属性__ob__是什么,暂时搞不懂,先不管他.根据字面意思来理解,这些方法就是获取和设置某个属性后触发的回调函数.看来思路是对的,下来就是找为属性注册事件的方法了.
这个方法就是出路.此方法可为一个对象添加一个新属性(或修改一个已存在的属性),并且为这个属性设置属性描述符.
Object.defineProperty(obj, prop, descriptor) //obj:添加或修改属性的对象 //prop:要添加或修改的属性名 //descriptor:属性描述符,对象形式
其中的 descriptor 是核心,是区别于普通方法(如 obj.key=value 的形式)定义属性的优势所在,在其中,就可以注册我们一直在寻找的事件监听.
var obj={} Object.defineProperty(obj, 'test', { get: function () { console.log('调用了 get'); }, set: function (newVal) { console.log('调用了 set,参数为'+newVal); } }) obj.test //调用了 get obj.test='params' //调用了 set,参数为params obj.test //调用了 get
在以上的代码中,使用 Objective.defineProperty 方法为 obj 添加了一个 test 属性,并添加了 get 和 set 两个回调函数作为属性描述符.而这两个回调函数触发的条件即分别是这个属性被访问时和被设置时,即为这个属性添加了一个访问事件和一个设置事件.
注意:在上面的代码中,无论事先给 test设定任何值,或者不设定值,访问属性 obj.test 都只会得到 get 方法的返回值,也就是说,这个属性存在的意义已经限定在触发 get和 set 的回调函数上了,与普通的属性(作为储存数据的变量)完全不同了.所以,obj.test 可看做一个函数(get)调用; obj.test=newVal可看做一个函数(set)调用,并传入了一个实参 newVal,这里的赋值运算符只是为了触发 set 并标识实参,并没有为 test 设定新值.
现在,对一个已存在的对象属性添加 get和 set.并且要求在访问这个属性时,能输出之前设定的值;在设置这个属性时,更新get方法所能返回的值.
这里遇到一个小问题:根据上面的分析,在使用Objective.defineProperty 方法为obj更新属性,设定 get 和 set时,原本这个属性代表的值将会和这个属性完全剥离,再也无法通过obj.key 或者 obj[key]访问到.那么,就在更新属性之前,把这个值记录下来,并且让这个值只能被本属性的 get和 set 方法访问----对,就是闭包.
function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get: function () { return val }, set: function (newVal) { if (newVal === val) return; val = newVal } }) }
函数写好了,联想到 vm.$data 中有多个键值对,肯定这里面的多个甚至全部键值对都是响应式的,那我们模拟一个对象,遍历这个对象时调用这个函数好了
var stu = {name: 'Alex', age: 15, height: 170}; Object.keys(stu).forEach(function (key) { defineReactive(stu, key, stu[key]) })
现在打印下这个 stu:
嗯,已经有 vm.$data点样子了.
实现:
到现在,所有的属性都已经绑定好了事件,属性值也都可以正常获取和修改了,接下来就要在页面上呈现了,也就是在 set 的回调函数中添加更新页面的操作.
为了观察方便,直接做一个简单的双向绑定:
<body> <span id="name"></span> 的年龄是 <span id="age"></span>岁,身高是 <span id=height></span><br> <input type="text" placeholder="修改姓名" refer="name"><br> <input type="text" placeholder="修改年龄" refer="age"><br> <input type="text" placeholder="修改身高" refer="height"><br> </body> <script> function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get: function () { return val }, set: function (newVal) { if (newVal === val) return; val = newVal update() //每次设置数据,都会更新页面 } }) } var stu = { name: 'Alex', age: 15, height: 170 }; Object.keys(stu).forEach(function (key) { defineReactive(stu, key, stu[key]) }) document.querySelectorAll('input').forEach(function(item){ item.oninput=function(){ stu[item.getAttribute('refer')]=item.value //每当输入数据,都会触发set } }) var span_age=document.getElementById('age') var span_name=document.getElementById('name') var span_heigth=document.getElementById('height') function update(){ span_name.innerText=stu.name span_age.innerText=stu.age span_heigth.innerText=stu.height } update() //初始化页面数据 </script>
至此,第一个目标,一个简单的响应式已经实现.
但是,在使用的过程中,发现一个很严重的问题,那就是数据绑定.我们并没有去识别 DOM中哪个位置使用了哪个对象属性.在实际使用中,手动为一个个 DOM添加事件,或者为一个个标签写入 innerText显然是不现实的.所以接下来,解决数据的自动绑定问题.
目标 2:实现数据的自动绑定
思路:
通过学习 Vue的基本语法,可以知道 vue 判断数据绑定的标识:在标签内,通过 v-bind,v-for,v-model 等命令标识,而在内容区域,通过{{}}来标识.那么要找到这些标识,vue 必然会对vm.$el中的DOM树进行一次全面分析,找出这些标识符,并将这些标识符所表示的属性与 data 中的对应属性值关联起来.
分解步骤:
1.遍历节点
将一个标签中的所有子节点拿出来遍历,可能是一个耗能非常高的任务.为了高效完成这一步操作,可以使用DocumentFragment(文档片段) 来进行.正如其中所描述的,在虚拟 DOM中进行修改,不会触发页面的重渲染,仅在 append到页面中时,触发一次性渲染,大大提高了大批量 DOM渲染的效率.国外的一些大神也专门做了这项测试.
那第一步操作,就变成了劫持所有 DOM节点至文档片段中:
