JavaScript實(shí)現(xiàn)雙向綁定的三種方式

前端數(shù)據(jù)的雙向綁定方法

前端的視圖層和數(shù)據(jù)層有時(shí)需要實(shí)現(xiàn)雙向綁定(two-way-binding)色迂,例如mvvm框架除呵,數(shù)據(jù)驅(qū)動(dòng)視圖巧号,視圖狀態(tài)機(jī)等浓瞪,研究了幾個(gè)目前主流的數(shù)據(jù)雙向綁定框架,總結(jié)了下女轿。目前實(shí)現(xiàn)數(shù)據(jù)綁定主要有以下三種箭启。

  1. 手動(dòng)綁定

比較老的實(shí)現(xiàn)方式,有點(diǎn)像觀察者編程模式蛉迹,主要思路是通過(guò)在數(shù)據(jù)對(duì)象上定義get和set方法(當(dāng)然還有其它方法)傅寡,調(diào)用時(shí)手動(dòng)調(diào)用get或set數(shù)據(jù),改變數(shù)據(jù)后觸發(fā)UI層的渲染操作;以視圖驅(qū)動(dòng)數(shù)據(jù)變化的場(chǎng)景主要應(yīng)用于input荐操、select芜抒、textarea等元素,當(dāng)UI層變化時(shí)托启,通過(guò)監(jiān)聽dom的change宅倒,keypress,keyup等事件來(lái)觸發(fā)事件改變數(shù)據(jù)層的數(shù)據(jù)屯耸。整個(gè)過(guò)程均通過(guò)函數(shù)調(diào)用完成拐迁。

<!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(){        
            /**
             * 掃描帶指令的節(jié)點(diǎn)屬性
             */
            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){
                        /**
                         * 調(diào)用屬性指令,這里可以使用數(shù)據(jù)改變檢測(cè)
                         */
                        command[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
                        elem.command.push(attr.nodeName.slice(2));
                    }
                }
            }
        }

        /**
         * 設(shè)置數(shù)據(jù)后掃描
         */
        function mvSet(key, value){
            data[key] = value;
            scan();
        }
        /**
         * 數(shù)據(jù)綁定監(jiān)聽
         */
        elems[1].addEventListener('keyup', function(e){
            mvSet('value', e.target.value);
        }, false);

        scan();

        /**
         * 改變數(shù)據(jù)更新視圖
         */
        setTimeout(function(){
            mvSet('value', 'fuck');
        },1000)

    </script>
</body>
</html>
  1. 臟檢查機(jī)制

以典型的mvvm框架angularjs為代表疗绣,angular通過(guò)檢查臟數(shù)據(jù)來(lái)進(jìn)行UI層的操作更新线召。關(guān)于angular的臟檢測(cè),有幾點(diǎn)需要了解:

  • 臟檢測(cè)機(jī)制并不是使用定時(shí)檢測(cè)
  • 臟檢測(cè)的時(shí)機(jī)是在數(shù)據(jù)發(fā)生變化時(shí)進(jìn)行
  • angular對(duì)常用的dom事件多矮,xhr事件等做了封裝缓淹,在里面觸發(fā)進(jìn)入angular的digest流程
  • 在digest流程里面,會(huì)從rootscope開始遍歷工窍,檢查所有的watcher

臟檢測(cè)如何去做割卖,主要是通過(guò)設(shè)置的數(shù)據(jù)去找與該數(shù)據(jù)相關(guān)的所有元素前酿,然后再比較數(shù)據(jù)變化患雏,如果變化則進(jìn)行指令操作

<!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) {
        /**
         * 掃描帶指令的節(jié)點(diǎn)屬性
         */
        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) {
                    /**
                     * 調(diào)用屬性指令
                     */
                    var dataKey = elem.getAttribute('ng-bind') || undefined;
                    /**
                     * 進(jìn)行數(shù)據(jù)初始化
                     */
                    command[attr.nodeValue].call(elem, data[dataKey]);
                    elem.command[attr.nodeValue] = data[dataKey];
                }
            }
        }
    }

    /**
     * 臟循環(huán)檢測(cè)
     * @param  {[type]} elems [description]
     * @return {[type]}       [description]
     */
    var digest = function(elems) {
        /**
         * 掃描帶指令的節(jié)點(diǎn)屬性
         */
        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) {
                    /**
                     * 調(diào)用屬性指令
                     */
                    var dataKey = elem.getAttribute('ng-bind') || undefined;

                    /**
                     * 進(jìn)行臟數(shù)據(jù)檢測(cè),如果數(shù)據(jù)改變罢维,則重新執(zhí)行指令淹仑,否則跳過(guò)
                     */
                    if(elem.command[attr.nodeValue] !== data[dataKey]){

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

    /**
     * 初始化數(shù)據(jù)
     */
    scan(elems);

    /**
     * 可以理解為做數(shù)據(jù)劫持監(jiān)聽
     */
    function $digest(value){
        var list = document.querySelectorAll('[ng-bind='+ value + ']');
        digest(list);
    }

    /**
     * 輸入框數(shù)據(jù)綁定監(jiān)聽
     */
    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';
        /**
         * 這里問(wèn)啥還要執(zhí)行$digest這里關(guān)鍵的是需要手動(dòng)調(diào)用$digest方法來(lái)啟動(dòng)臟檢測(cè)
         */
        $digest('value');
    }, 2000)

    </script>
</body>
</html>
  1. 前端數(shù)據(jù)劫持(Hijacking)

第三種方法則是avalon等框架使用的數(shù)據(jù)劫持方式》畏酰基本思路是使用Object.defineProperty對(duì)數(shù)據(jù)對(duì)象做屬性get和set的監(jiān)聽匀借,當(dāng)有數(shù)據(jù)讀取和賦值操作時(shí)則調(diào)用節(jié)點(diǎn)的指令,這樣使用最通用的=等號(hào)賦值就可以了平窘。具體實(shí)現(xiàn)如下:

<!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() {
        /**
         * 掃描帶指令的節(jié)點(diǎn)屬性
         */
        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) {
                    /**
                     * 調(diào)用屬性指令
                     */
                    command[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
                    elem.command.push(attr.nodeName.slice(2));

                }
            }
        }
    }

    var bValue;
    /**
     * 定義屬性設(shè)置劫持
     */
    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.");
        }
    }
    /**
     * 初始化數(shù)據(jù)
     */
    scan();

    /**
     * 可以理解為做數(shù)據(jù)劫持監(jiān)聽
     */
    defineGetAndSet(data, 'value');

    /**
     * 數(shù)據(jù)綁定監(jiān)聽
     */
    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來(lái)做兼容但是瀏覽器兼容性的原因,直接用defineProperty就可以了瑰艘。至于IE8瀏覽器仍需要使用其它方法來(lái)做hack是鬼。如下代碼可以對(duì)IE8進(jìn)行hack,defineProperty支持IE8紫新。例如使用es5-shim.js就可以了均蜜。(IE8以下瀏覽器忽略)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市芒率,隨后出現(xiàn)的幾起案子囤耳,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件充择,死亡現(xiàn)場(chǎng)離奇詭異德玫,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)椎麦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門化焕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人铃剔,你說(shuō)我怎么就攤上這事撒桨。” “怎么了键兜?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵凤类,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我普气,道長(zhǎng)谜疤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任现诀,我火速辦了婚禮夷磕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仔沿。我一直安慰自己坐桩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布封锉。 她就那樣靜靜地躺著绵跷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪成福。 梳的紋絲不亂的頭發(fā)上碾局,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音奴艾,去河邊找鬼净当。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蕴潦,可吹牛的內(nèi)容都是我干的像啼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼品擎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼埋合!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起萄传,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤甚颂,失蹤者是張志新(化名)和其女友劉穎蜜猾,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體振诬,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蹭睡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了赶么。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肩豁。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖辫呻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情放闺,我是刑警寧澤祟昭,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布篡悟,位于F島的核電站,受9級(jí)特大地震影響匾寝,放射性物質(zhì)發(fā)生泄漏搬葬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一艳悔、第九天 我趴在偏房一處隱蔽的房頂上張望急凰。 院中可真熱鬧,春花似錦很钓、人聲如沸香府。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至锭碳,卻和暖如春袁稽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背擒抛。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工推汽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人歧沪。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓歹撒,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親诊胞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子暖夭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容