随着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>