随着vue的兴起,vue的一大亮点--双向数据绑定,也被很多人所熟知,之所以成为一大亮点是针对之前的单向数据绑定而言的,接下来就做一个简单的对比分析:
单向数据绑定:指的是我们先把模板写好,然后把模板和数据(可能来自于后台)整合到一起形成html代码,然后把这段html代码插入到文档流里面
单向数据绑定的缺点:html代码一旦生成之后就没法儿改变啦,如果数据发生变化的话,之前生成的html就得去掉,再重新把模板和数据一起整合后插入到文档流中
双向数据绑定:数据模型和视图层之间的双向绑定,用户在视图层的修改会自动同步到数据模型中去,同样的,数据模型中的值发生了变化,也会立即同步到视图层。
双向数据绑定最常用的应用场景就是表单,目前前端实现双向数据绑定主要流行的框架就是Angular和Vue,具体实现原理如下:
一,发布订阅模式(PubSub模式)
比较传统的模式,通过在数据对象上定义get和set方法,调用时手动调用set和get数据,改变数据之后发出UI层的渲染操作;以视图驱动数据变化的场景主要于input,textarea,select等元素,当UI层发生变化的时候,通过监听dom的change,keypress,keyup等事件来改变数据层的数据,整个过程均通过函数调用来完成
HTML
<div class="wrapper"> <input ps-value= "value" id="edit" type="text"/> <div ps-text="value" id="show"></div> </div>
JavaScript
<script> var elements = [document.getElementById('edit'),document.getElementById('show')] var data = { value: 'hello' } var command = { text: function(str){ this.innerHTML = str }, value:function (str) { this.setAttribute('value',str) } } var scan =function() { for (var i=0;i<elements.length;i++) { var el = elements[i] for (var j=0;j<el.attributes.length;j++) { var attr = el.attributes[j] if (attr.nodeName.indexOf('ps') !== -1) { command[attr.nodeName.slice(3)].call(el, data[attr.nodeValue]) } } } } function set (key,value){ data[key] = value scan() } // 视图层的变化触发数据层的变化 elements[0].addEventListener('keyup',function(e){ set('value',e.target.value) }) scan() // 模拟数据的变化 setTimeout(function(){ set('value','world') },2000) </script>
二,数据劫持
具体思路是使用Object.defineProperty对数据对象做属性的get和set监听,当有数据读取和赋值操作时则调用节点的指令,这样使用最通用的=赋值就可以了。具体实现如下:
HTML <div class="wrapper"> <input ps-value= "value" id="edit" type="text"/> <div ps-text="value" id="show"></div> </div>
JavaScript
<script> var elements = [document.getElementById('edit'),document.getElementById('show')] var data = { value: 'hello' } var command = { text: function(str){ this.innerHTML = str }, value:function (str) { this.setAttribute('value',str) } } var scan =function() { for (var i=0;i<elements.length;i++) { var el = elements[i] for (var j=0;j<el.attributes.length;j++) { var attr = el.attributes[j] if (attr.nodeName.indexOf('ps') !== -1) { command[attr.nodeName.slice(3)].call(el, data[attr.nodeValue]) } } } } var beforeValue var defineGetandSet = function(obj,name) { try{ Object.defineProperty(obj,name,{ get:function(){
return beforeValue }, set:function(newvalue){ beforeValue = newvalue scan() }, enumerable: true, configurable: true }) }catch(error){ console.log(error) } } // 初始化数据 scan() defineGetandSet(data,'value') // 视图层的改变 if (document.addEventListener) { elements[0].addEventListener('keyup',function(e){ data.value = e.target.value }) }else{ elements[0].attchEvent('keyup',function(e){ data.value = e.target.value })
} // 模拟数据的变化 setTimeout(function(){ data.value = 'world' },2000) </script>
三,脏检查机制
以angularjs为代表,angular通过检查脏数据来进行UI层的操作更新。关于angular的脏检测,有几点需要了解:当数据发生变化的时候才进行脏检测,并不是实时或者是定时的开启检测。angular对常用的dom事件,xhr事件等做了封装, 在里面触发进入angular的digest流程。在digest流程里面, 会从rootscope开始遍历, 检查所有的watcher。脏检测的主要思路:通过设置的数据来需找与该数据相关的所有元素,然后再比较数据变化,如果变化则进行指令操作。
HTML
<div class="wrapper"> <input p-event = "value" ng-bind="value" type="text" id="edit"/> <div p-event = "text" ng-bind="value" id="show"></div> </div>
JavaScript <script> var elements = [document.getElementById('edit'),document.getElementById('show')] var data = { value:'hello' } var command = { text:function(str){ this.innerHTML = str }, value:function(str){ this.setAttribute('value',str) } } var scan = function(){ elements.forEach((el)=>{ el.command ={} for (var i=0;i<el.attributes.length;i++) { var attr = el.attributes[i] if (attr.nodeName.indexOf('p-event') !== -1) { var datakey = el.getAttribute('ng-bind') || undefined command[attr.nodeValue].call(el,data[datakey]) //数据初始化 el.command[attr.nodeValue] = data[datakey] } } }) } // 脏循环检查,从根部开始轮询,检测值是否有变化 var digest = function(elements) { elements.forEach((el)=>{ for (var i=0;i<el.attributes.length;i++) { var attr = el.attributes[i] if (attr.nodeName.indexOf('p-event') !== -1) { var datakey = el.getAttribute('ng-bind') || undefined if (el.command[attr.nodeValue] !== data[datakey]) { //当数据有变化的时候,检查并且进行重新赋值 command[attr.nodeValue].call(el,data[datakey]) el.command[attr.nodeValue] = data[datakey] } } } }) } scan() // 数据初始化 function $digest (value) { var list = document.querySelectorAll('[ng-bind='+value+']') digest(list) } // 输入框数据绑定监听 if (document.addEventListener) { elements[0].addEventListener('keyup',function(e){ data.value = e.target.value $digest(e.target.getAttribute('ng-bind')) },false) } else { elements[0].attachEvent('onkeyup',function(e){ data.value = e.target.value $digest(e.target.getAttribute('ng-bind')) },false) } // 模式数据的变化 setTimeout(function(){ data.value = 'world' $digest('value') // 这里需要手动启动脏检查 ,数据改变的时候,视图层也跟着更新 },2000) </script>