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

前端數據的雙向綁定方法

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

  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>
  1. 臟檢查機制

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

  • 臟檢測機制并不是使用定時檢測
  • 臟檢測的時機是在數據發生變化時進行
  • angular對常用的dom事件,xhr事件等做了封裝,在里面觸發進入angular的digest流程
  • 在digest流程里面,會從rootscope開始遍歷,檢查所有的watcher

臟檢測如何去做,主要是通過設置的數據去找與該數據相關的所有元素,然后再比較數據變化,如果變化則進行指令操作

<!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>
  1. 前端數據劫持(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>

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 如果覺得不錯的話,請點一下贊吧 ??本文章會持續更新中。。。 2016年,vuejs可謂是大放異彩,以迅雷不及掩耳之...
    fullbook閱讀 1,456評論 3 6
  • js雙向綁定幾種方法的介紹 使用Object.defineProperty實現簡單的js雙向綁定剖析Vue原理&實...
    darr250閱讀 9,636評論 1 5
  • MVVM框架主要包含3個部分:model、view和 viewmodel。 Model:指的是數據部分,對應到前端...
    overflow_hidden閱讀 1,966評論 0 0
  • 關于雙向數據綁定 當我們在前端開發中采用MV*的模式時,M – model,指的是模型,也就是數據,V – vie...
    Alex_Deng閱讀 968評論 0 3
  • 【今日最佳】延安get? 【晨間計劃之情感心理課】 【晨間計劃-美顏季復盤】 【晨間計劃之Reading 】 【本...
    小尾巴巨人閱讀 107評論 0 1