javascript實現數據雙向綁定的三種方式

fuss228 9年前發布 | 12K 次閱讀 HTML 前端技術 angularjs

前端數據的雙向綁定方法

前端的視圖層和數據層有時需要實現雙向綁定(two-way-binding),例如mvvm框架,數據驅動視圖,視圖狀態機等,研究了幾個目前主流的數據雙向綁定框架,總結了下。目前實現數據雙向綁定主要有以下三種。

github演示例子

1、手動綁定

比較老的實現方式,有點像觀察者編程模式,主要思路是通過在數據對象上定義get和set方法(當然還有其它方法),調用時手動調用get或set數據,改變數據后出發UI層的渲染操作;以視圖驅動數據變化的場景主要應用與input、select、textarea等元素,當UI層變化時,通過監聽dom的change,keypress,keyup等事件來出發事件改變數據層的數據。整個過程均通過函數調用完成。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>data-binding-method-set</title>
</head>
<body>
    <input q-value="value" type="text" id="input">
    <div q-text="value" id="el"></div>
    <script>
        var elems = [document.getElementById('el'), document.getElementById('input')];

    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, len = elems.length; i < len; i++){
             var elem = elems[i];
             elem.command = [];
             for(var j = 0, len1 = elem.attributes.length; j < len1; j++){
                 var attr = elem.attributes[j];
                 if(attr.nodeName.indexOf('q-') >= 0){
                     /**
  • 調用屬性指令,這里可以使用數據改變檢測 */

                     command[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
                     elem.command.push(attr.nodeName.slice(2));
                 }
             }
         }
     }
    
     /**
  • 設置數據后掃描 */
     function mvSet(key, value){
         data[key] = value;
         scan();
     }
     /**
  • 數據綁定監聽 */

     elems[1].addEventListener('keyup', function(e){
         mvSet('value', e.target.value);
     }, false);
    
     scan();
    
     /**
  • 改變數據更新視圖 */

     setTimeout(function(){
         mvSet('value', 'fuck');
     },1000)
    
    

    </script> </body> </html> </code></pre>

    2、臟檢查機制

    以典型的mvvm框架angularjs為代表,angular通過檢查臟數據來進行UI層的操作更新。關于angular的臟檢測,有幾點需要了解些:

    - 臟檢測機制并不是使用定時檢測。

    - 臟檢測的時機是在數據發生變化時進行。

    - angular對常用的dom事件,xhr事件等做了封裝, 在里面觸發進入angular的digest流程。

    - 在digest流程里面, 會從rootscope開始遍歷, 檢查所有的watcher。

    (關于angular的具體設計可以看其他文檔,這里只討論數據綁定),那我們看下臟檢測該如何去做:主要是通過設置的數據來需找與該數據相關的所有元素,然后再比較數據變化,如果變化則進行指令操作

    <!DOCTYPE html>
    <html lang="en">

<head> <meta charset="UTF-8"> <title>data-binding-drity-check</title> </head>

<body> <input q-event="value" ng-bind="value" type="text" id="input"> <div q-event="text" ng-bind="value" id="el"></div> <script>

var elems = [document.getElementById('el'), document.getElementById('input')];

var data = {
    value: 'hello!'
};

var command = {
    text: function(str) {
        this.innerHTML = str;
    },
    value: function(str) {
        this.setAttribute('value', str);
    }
};

var scan = function(elems) {
    /**
  • 掃描帶指令的節點屬性 */
     for (var i = 0, len = elems.length; i < len; i++) {
         var elem = elems[i];
         elem.command = {};
         for (var j = 0, len1 = elem.attributes.length; j < len1; j++) {
             var attr = elem.attributes[j];
             if (attr.nodeName.indexOf('q-event') >= 0) {
                 /**
  • 調用屬性指令 */
                 var dataKey = elem.getAttribute('ng-bind') || undefined;
                 /**
  • 進行數據初始化 */

                 command[attr.nodeValue].call(elem, data[dataKey]);
                 elem.command[attr.nodeValue] = data[dataKey];
             }
         }
     }
    

    }

    /**

  • 臟循環檢測
  • @param {[type]} elems [description]
  • @return {[type]} [description] */ var digest = function(elems) {
     /**
  • 掃描帶指令的節點屬性 */
     for (var i = 0, len = elems.length; i < len; i++) {
         var elem = elems[i];
         for (var j = 0, len1 = elem.attributes.length; j < len1; j++) {
             var attr = elem.attributes[j];
             if (attr.nodeName.indexOf('q-event') >= 0) {
                 /**
  • 調用屬性指令 */

                 var dataKey = elem.getAttribute('ng-bind') || undefined;
    
                 /**
  • 進行臟數據檢測,如果數據改變,則重新執行指令,否則跳過 */

                 if(elem.command[attr.nodeValue] !== data[dataKey]){
    
                     command[attr.nodeValue].call(elem, data[dataKey]);
                     elem.command[attr.nodeValue] = data[dataKey];
                 }
             }
         }
     }
    

    }

    /**

  • 初始化數據 */ scan(elems);

    /**

  • 可以理解為做數據劫持監聽 */ function $digest(value){

     var list = document.querySelectorAll('[ng-bind='+ value + ']');
     digest(list);
    

    }

    /**

  • 輸入框數據綁定監聽 */ if(document.addEventListener){

     elems[1].addEventListener('keyup', function(e) {
         data.value = e.target.value;
         $digest(e.target.getAttribute('ng-bind'));
     }, false);
    

    }else{

     elems[1].attachEvent('onkeyup', function(e) {
         data.value = e.target.value;
         $digest(e.target.getAttribute('ng-bind'));
     }, false);
    

    }

    setTimeout(function() {

     data.value = 'fuck';
     /**
  • 這里問啥還要執行$digest這里關鍵的是需要手動調用$digest方法來啟動臟檢測 */

     $digest('value');
    

    }, 2000)

    </script> </body> </html> </code></pre>

    3、前端數據劫持(Hijacking)

    第三種方法則是avalon等框架使用的數據劫持方式。基本思路是使用Object.defineProperty對數據對象做屬性get和set的監聽,當有數據讀取和賦值操作時則調用節點的指令,這樣使用最通用的=等號賦值就可以了。具體實現如下:

    <!DOCTYPE html>
    <html lang="en">

<head> <meta charset="UTF-8"> <title>data-binding-hijacking</title> </head>

<body> <input q-value="value" type="text" id="input"> <div q-text="value" id="el"></div> <script>

var elems = [document.getElementById('el'), document.getElementById('input')];

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, len = elems.length; i < len; i++) {
         var elem = elems[i];
         elem.command = [];
         for (var j = 0, len1 = elem.attributes.length; j < len1; j++) {
             var attr = elem.attributes[j];
             if (attr.nodeName.indexOf('q-') >= 0) {
                 /**
  • 調用屬性指令 */

                 command[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
                 elem.command.push(attr.nodeName.slice(2));
    
             }
         }
     }
    

    }

    var bValue; /**

  • 定義屬性設置劫持 */ var defineGetAndSet = function(obj, propName) {

     try {
         Object.defineProperty(obj, propName, {
    
             get: function() {
                 return bValue;
             },
             set: function(newValue) {
                 bValue = newValue;
                 scan();
             },
    
             enumerable: true,
             configurable: true
         });
     } catch (error) {
         console.log("browser not supported.");
     }
    

    } /**

  • 初始化數據 */ scan();

    /**

  • 可以理解為做數據劫持監聽 */ defineGetAndSet(data, 'value');

    /**

  • 數據綁定監聽 */ if(document.addEventListener){

     elems[1].addEventListener('keyup', function(e) {
         data.value = e.target.value;
     }, false);
    

    }else{

     elems[1].attachEvent('onkeyup', function(e) {
         data.value = e.target.value;
     }, false);
    

    }

    setTimeout(function() {

     data.value = 'fuck';
    

    }, 2000) </script> </body>

</html> </code></pre>

但值得注意的是defineProperty支持IE8以上的瀏覽器,這里可以使用_ defineGetter _ 和 _ defineSetter _ 來做兼容但是瀏覽器兼容性的原因,直接用defineProperty就可以了。至于IE8瀏覽器仍需要使用其它方法來做hack。如下代碼可以對IE8進行hack,defineProperty支持IE8。例如使用es5-shim.js就可以了。(IE8以下瀏覽器忽略)

4、小結

首先這里的例子只是簡單的實現,讀者可以深入感受三種方式的異同點,復雜的框架也是通過這樣的基本思路滾雪球滾大的。

演示例子

參考:

https://gist.github.com/brettz9/4093766#file_html5_dataset.js

https://github.com/xufei/blog/issues/10

 

來自: http://ouvens.github.io/frontend-javascript/2015/11/29/js-data-two-ways-binding.html

 

 本文由用戶 fuss228 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!