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


來源:http://jixianqianduan.com/frontend-javascript/2015/11/29/js-data-two-ways-binding.html

前端數據的雙向綁定方法

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

1、手動綁定

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

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>雙向數據綁定之手動綁定</title>
</head>
<body>
      <input q-value="value"  type="text" id="input">
      <div q-text="value" id="el"></div>
      <script type="text/javascript">
           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]; 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]); } } } } //設置數據后掃描 set方法 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','World'); },2000); </script> </body> </html>

 

2、臟檢查機制

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

  • 臟檢測機制並不是使用定時檢測。
  • 臟檢測的時機是在數據發生變化時進行。
  • angular對常用的dom事件,xhr事件等做了封裝, 在里面觸發進入angular的digest流程。
  • 在digest流程里面, 會從rootscope開始遍歷, 檢查所有的watcher。 (關於angular的具體設計可以看其他文檔,這里只討論數據綁定),那我們看下臟檢測該如何去做:主要是通過設置的數據來需找與該數據相關的所有元素,然后再比較數據變化,如果變化則進行指令操作
<!DOCTYPE html>

<html>

<head>

    <meta charset="UTF-8">

      <title>雙向數據綁定之臟檢查機制</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 type="text/javascript">

      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>

 

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>

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM