溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

js怎么實現(xiàn)數(shù)據(jù)雙向綁定

發(fā)布時間:2021-09-07 14:05:46 來源:億速云 閱讀:198 作者:chen 欄目:開發(fā)技術(shù)

本篇內(nèi)容主要講解“js怎么實現(xiàn)數(shù)據(jù)雙向綁定”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學(xué)習(xí)“js怎么實現(xiàn)數(shù)據(jù)雙向綁定”吧!

雙向綁定:

雙向綁定基于MVVM模型:model-view-viewModel

model: 模型層,負(fù)責(zé)業(yè)務(wù)邏輯以及與數(shù)據(jù)庫的交互
view:視圖層,負(fù)責(zé)將數(shù)據(jù)模型與UI結(jié)合,展示到頁面中
viewModel:視圖模型層,作為model和view的通信橋梁

雙向綁定的含義:當(dāng)model數(shù)據(jù)發(fā)生變化的時候,會通知到view層,當(dāng)用戶修改了view層的數(shù)據(jù)的時候,會反映到模型層。

而雙向數(shù)據(jù)綁定的好處在于:只關(guān)注于數(shù)據(jù)操作,DOM操作減少

Vue.js實現(xiàn)的原理就是采用的訪問器監(jiān)聽,所以這里也采用訪問器監(jiān)聽的方式實現(xiàn)簡單的數(shù)據(jù)雙向綁定。

訪問器監(jiān)聽的實現(xiàn),主要采用了javascript中原生方法:Object.defineProperty,該方法可以為某對象添加訪問器屬性,當(dāng)訪問或者給該對象屬性賦值的時候,會觸發(fā)訪問器屬性,因此利用此思路,可以在訪問器屬性中添加處理程序。

這里先實現(xiàn)一個簡單的input標(biāo)簽的數(shù)據(jù)雙向綁定過程,先大致了解一下什么是數(shù)據(jù)的雙向綁定。

<input type="text">

<script>
// 獲取到input輸入框?qū)ο?
let input = document.querySelector('input');
// 創(chuàng)建一個沒有原型鏈的對象,用于監(jiān)聽該對象的某屬性的變化
let model = Object.create(null);
// 當(dāng)鼠標(biāo)移開輸入框的時候,view層數(shù)據(jù)通知model層數(shù)據(jù)的變化
input.addEventListener('blur',function() {
    model['user'] = this.value;
})

// 當(dāng)model層數(shù)據(jù)發(fā)生變化的時候,通知view層數(shù)據(jù)的變化。
Object.defineProperty(model, 'user', {
    set(v) {
        user = v;
        input.value = v;
    },
    get() {
        return user;
    }
})
</script>

以上的代碼中首先對Input標(biāo)簽對象進(jìn)行獲取,然后對input元素對象添加監(jiān)聽事件(blur),當(dāng)事件被觸發(fā)的時候,也就是view層發(fā)生變化的時候,就需要去通知model層去更新數(shù)據(jù),這里的model層利用的是一個沒有原型的空對象(使用空對象的原因:避免獲取某屬性的時候,由于原型鏈的存在,造成數(shù)據(jù)的誤讀)。

使用Object.defineProperty的方法,為該對象的指定屬性添加訪問器屬性,當(dāng)該對象的屬性被修改,就會觸發(fā)setter訪問器,我們這里就可以為view層的數(shù)據(jù)賦值,更新view層的數(shù)據(jù),這里的view層指的是Input標(biāo)簽的屬性value。

看一下效果:

在文本框中輸入一個數(shù)據(jù),在控制臺打印model.user可以看到數(shù)據(jù)已經(jīng)影響到了model層

js怎么實現(xiàn)數(shù)據(jù)雙向綁定

接著在控制臺手動修改model層的數(shù)據(jù):model.user = ‘9090';
此時可以看到數(shù)據(jù)文本框也被相應(yīng)的進(jìn)行了修改,影響到了view層

js怎么實現(xiàn)數(shù)據(jù)雙向綁定

好啦,實現(xiàn)了最簡單的只針對于文本框的數(shù)據(jù)雙向綁定,我們可以從以上的案例中可以發(fā)現(xiàn)以下的實現(xiàn)邏輯:

①. 要實現(xiàn)view層到model的數(shù)據(jù)通信,就需要知道view層的數(shù)據(jù)變化了,以及view層的值,但是一般要獲取到標(biāo)簽本身的值,除非有內(nèi)置屬性,比如:input標(biāo)簽的value屬性,可以獲得文本框的輸入值

②. 利用Object.defineProperty實現(xiàn)model層向view層的通信,當(dāng)數(shù)據(jù)被修改,就會立馬觸發(fā)訪問器屬性setter,從而可以通知使用了該屬性的所有view層去更新他們的現(xiàn)在的數(shù)據(jù)(觀察者)

③. 被綁定的數(shù)據(jù)需要是作為一個對象的屬性,因為Object.defineProperty是對某一個對象的屬性開啟的訪問器特性。

爭對以上的總結(jié),我們可以設(shè)計出類似于vue.js的數(shù)據(jù)雙向綁定模式:
利用自定義指令實現(xiàn)view到model層的數(shù)據(jù)通信
利用Object.defineProperty實現(xiàn)model層到view層的數(shù)據(jù)通信。

這里的實現(xiàn)涉及到三個主要的函數(shù):

  • _observer: 對數(shù)據(jù)進(jìn)行處理,重寫每一個屬性的getter/setter

  • _compile:對自定義指令(這里只涉及了e-bind/e-click/e-model)進(jìn)行解析,并在解析過程中為節(jié)點綁定原生處理事件,以及實現(xiàn)view層到model層的綁定

  • Watcher: 作為model與view的中間橋梁,當(dāng)model發(fā)生變化進(jìn)一步更新view層

實現(xiàn)代碼:

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>雙向數(shù)據(jù)綁定</title>
    <style>
        #app {
            text-align: center;
        }
    </style>
    <script src="/js/eBind.js"></script>
    <script>
        window.onload = function () {
           let ebind =  new EBind({
                el: '#app',
                data: {
                    number: 0,
                    person: {
                        age: 0
                    }
                },
                methods: {
                    increment: function () {
                        this.number++;
                    },
                    addAge: function () {
                        this.person.age++;
                    }
                }
            })
        }
    </script>
</head>
<body>
<div id="app">
    <form>
        <input type="text" e-model="number">
        <button type="button" e-click="increment">增加</button>
    </form>
    <input e-model="number" type="text">
    <form>
        <input type="text" e-model="person.age">
        <button type="button" e-click="addAge">增加</button>
    </form>
    <h4 e-bind="person.age"></h4>
</div>
</body>
</html>

eBind.js

function EBind(options) {
    this._init(options);
}

// 根據(jù)所給的自定義參數(shù),進(jìn)行數(shù)據(jù)雙向綁定的初始化工作
EBind.prototype._init = function (options) {
    // options是初始化時的數(shù)據(jù),包括el,data,method
    this.$options = options;

    // el是需要管理的Element對象,el:#app this.$el:id為app的Element對象
    this.$el = document.querySelector(options.el);

    // 數(shù)據(jù)
    this.$data = options.data;

    // 方法
    this.$methods = options.methods;

    // _binding保存著model與view的映射關(guān)系,也就是Wachter的實例,當(dāng)model更新的時候,更新對應(yīng)的view
    this._binding = {};

    // 重寫 this.$data的get和set方法
    this._obverse(this.$data);

    // 解析指令
    this._compile(this.$el);
}


// 該函數(shù)的作用:對所有的this.$data里面的屬性進(jìn)行監(jiān)聽,訪問器監(jiān)聽,實現(xiàn)model到view層的數(shù)據(jù)通信。當(dāng)model層改變的時候通知view層
EBind.prototype._obverse = function (currentObj, completeKey) {
    // 保存上下文
    var _this = this;

    // currentObj就是需要重寫get/set的對象,Object.keys獲取該對象的屬性,得到的是一個數(shù)組
    // 對該數(shù)組進(jìn)行遍歷
    Object.keys(currentObj).forEach(function (key) {

        // 當(dāng)且僅當(dāng)對象自身的屬性才監(jiān)聽
        if (currentObj.hasOwnProperty(key)) {

            // 如果是某一對象的屬性,則需要以person.age的形式保存
            var completeTempKey = completeKey ? completeKey + '.' + key : key;

            // 建立需要監(jiān)測屬性的關(guān)聯(lián)
            _this._binding[completeTempKey] = {
                _directives: [] // 存儲所有使用該數(shù)據(jù)的地方
            };

            // 獲取到當(dāng)前屬性的值
            var value = currentObj[key];

            // 如果值是對象,則遍歷處理,對每個對象屬性都完全監(jiān)測
            if (typeof value == 'object') {
                _this._obverse(value, completeTempKey);
            }

            var binding = _this._binding[completeTempKey];

            // 修改對象的每一個屬性的get和set,在get和set中添加處理事件
            Object.defineProperty(currentObj, key, {
                enumerable: true,
                configurable: true, // 避免默認(rèn)為false
                get() {
                    return value;
                },
                set(v) {
                    // value保存當(dāng)前屬性的值
                    if (value != v) {
                        // 如果數(shù)據(jù)被修改,則需要通知每一個使用該數(shù)據(jù)的地方進(jìn)行更新數(shù)據(jù),也即:model通知view層,Watcher類作為中間層去完成該操作(通知操作)
                        value = v;
                        binding._directives.forEach(function (item) {
                            item.update();
                        })
                    }
                }
            })
        }
    })
}


// 該函數(shù)的作用是:對自定義指令進(jìn)行編譯,為其添加原生監(jiān)聽事件,實現(xiàn)view到model層的數(shù)據(jù)通信,也即當(dāng)view層數(shù)據(jù)變化之后通知model層數(shù)據(jù)更新
// 實現(xiàn)原理:通過托管的element對象:this.$el,獲取到所有的子節(jié)點,遍歷所有的子節(jié)點,查看其是否有自定義屬性,如果有指定含義的自定義屬性
// 比如說:e-bind/e-model/e-click則根據(jù)節(jié)點上添加的自定義屬性的不同為其添加監(jiān)聽事件
// e-click添加原生的onclick事件,這里主要注意點就是:需要將this.$method中指定方法的上下文this改為this.$data
// e-model為綁定的數(shù)據(jù)更新,這里只支持input,textarea標(biāo)簽,原因:采用標(biāo)簽自帶的value屬性實現(xiàn)的view到model層的數(shù)據(jù)通信
// e-bind
EBind.prototype._compile = function (root) {
    // 保存執(zhí)行上下文
    var _this = this;

    // 獲取到托管節(jié)點元素的所有子節(jié)點,只包括元素節(jié)點
    var nodes = root.children;

    for (let i = 0; i < nodes.length; i++) {
        // 獲取到子節(jié)點/按順序
        var node = nodes[i];

        // 如果當(dāng)前節(jié)點有子節(jié)點,則繼續(xù)逐層處理子節(jié)點
        if (node.children.length) {
            this._compile(node);
        }

        // 如果當(dāng)前節(jié)點綁定了e-click屬性,則需要為當(dāng)前節(jié)點綁定onclick事件
        if (node.hasAttribute('e-click')) {
            // hasAttribute可以獲取到自定義屬性
            node.addEventListener('click',(function () {
                // 獲取到當(dāng)前節(jié)點的屬性值,也就是方法
                var attrVal = node.getAttribute('e-click');
                // 由于綁定的方法里面的數(shù)據(jù)要使用data里面的數(shù)據(jù),所以需要將執(zhí)行的函數(shù)的上下文,也就是this改為this.$data
                // 而使用bind,不使用call/apply的原因是onclick方法需要觸發(fā)之后才會執(zhí)行,而不是立馬執(zhí)行
                return _this.$methods[attrVal].bind(_this.$data);
            })())
        }


        // 只對input和textarea標(biāo)簽元素可以施行雙向綁定,原因:利用這兩個標(biāo)簽的內(nèi)置的value屬性實現(xiàn)雙向綁定
        if (node.hasAttribute('e-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
            // 給element對象添加監(jiān)聽input事件 ,第二個參數(shù)是一個立即執(zhí)行函數(shù),獲取到節(jié)點的索引值,執(zhí)行函數(shù)內(nèi)部代碼,返回事件處理
            node.addEventListener('input', (function (index) {
                // 獲取到當(dāng)前節(jié)點的屬性值,也就是方法
                var attrVal = node.getAttribute('e-model');

                // 給當(dāng)前element對象添加model到view層的映射
                _this._binding[attrVal]._directives.push(new Watcher({
                    name: 'input',
                    el: node,
                    eb: _this,
                    exp: attrVal,
                    attr: 'value'
                }))

                // 如果input標(biāo)簽value值改變,此時需要更新model層的數(shù)據(jù),也就是view層到model層的改變
                return function () {
                    // 獲取到綁定的屬性,以.為分隔符,如果只是一個值,就直接獲取當(dāng)前值,如果是個對象(obj.key)的形式,則綁定的其實obj對象
                    // 中的key的值,此時就需要獲取到key,并對key進(jìn)行賦值為已改變的input標(biāo)簽的value值
                    var keys = attrVal.split('.');

                    // 獲取上一步得到的屬性的集合中最后一個屬性(最后一個屬性才是真正被綁定的值)
                    var lastKey = keys[keys.length - 1];

                    // 獲得真正被綁定的值的父對象
                    // 因為如果是對象,比如:obj.key.val,則需要找到key的引用,因為這里要改變的是val
                    // 通過引用key 從而改變val的值,但是如果直接獲取到的val的引用,val是數(shù)值型存儲,賦值給另一個變量的時候,其實是新開辟的一個空間
                    // 并不能直接改變model層也就是this.$data里面的數(shù)據(jù),而引用數(shù)據(jù)存儲的話,賦值給另一個變量,另一個變量的修改,會影響原來的引用的數(shù)據(jù)
                    // 所以這里需要找到真正被綁定值的父對象,也就是obj.key里面的obj值
                    var model = keys.reduce(function (value, key) {
                        // 如果不是對象,則直接返回屬性value
                        if (typeof value[key] !== 'object') {
                            return value;
                        }

                        return value[key];
                        // 這里使用model層作為起始值,原因:keys里面記錄的是this.$data里面的屬性,所以需要從父對象this.$data出發(fā)去找目標(biāo)屬性
                    }, _this.$data);

                    // model也就是之前說得父對象,obj.key中的obj,而lastkey也就是真正被綁定的屬性,找到了之后就需要對其更新為節(jié)點的值啦。
                    // 這里的model層被修改會觸發(fā)_observe里面的訪問器屬性setter,所以如果其他地方也使用了這個屬性的話,也會相應(yīng)的發(fā)生改變哦
                    model[lastKey] = nodes[index].value;
                }
            })(i))
        }


        // 對節(jié)點上綁定e-bind,為其添加model到view的映射即可,原因:e-bind實現(xiàn)的是model到view的數(shù)據(jù)通信,而在this._observer中
        // 已經(jīng)通過definePrototype實現(xiàn)了,所以這里只需要添加通信,便于在_oberver中實現(xiàn)。
        if(node.hasAttribute('e-bind')) {
            var attrVal = node.getAttribute('e-bind');
            _this._binding[attrVal]._directives.push(new Watcher({
                name: 'text',
                el: node,
                eb: _this,
                exp: attrVal,
                attr: 'innerHTML'
            }))
        }
    }
}

/**
 * options 屬性:
 * name: 節(jié)點名稱:文本節(jié)點:text, 輸入框:input
 * el: 指令對應(yīng)的DOM元素
 * eb: 指令對應(yīng)的EBind實例
 * exp: 指令對應(yīng)的值:e-bind="test";test就是指令對應(yīng)的值
 * attr: 綁定的屬性值, 比如:e-bind綁定的屬性,其實會反應(yīng)到innerHTML中,v-model綁定的標(biāo)簽會反應(yīng)到value中
 */
function Watcher(options) {
    this.$options = options;
    this.update();
}

Watcher.prototype.update = function () {
    // 保存上下文
    var _this = this;
    // 獲取到被綁定的對象
    var keys = this.$options.exp.split('.');

    // 獲取到DOM對象上要改變的屬性,對其進(jìn)行更改
    this.$options.el[this.$options.attr] = keys.reduce(function (value, key) {
        return value[key];
    }, _this.$options.eb.$data)
}

實現(xiàn)效果:

js怎么實現(xiàn)數(shù)據(jù)雙向綁定

到此,相信大家對“js怎么實現(xiàn)數(shù)據(jù)雙向綁定”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

js
AI