本篇內容主要講解“js怎么實現數據雙向綁定”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“js怎么實現數據雙向綁定”吧!
雙向綁定基于MVVM模型:model-view-viewModel
model: 模型層,負責業務邏輯以及與數據庫的交互
view:視圖層,負責將數據模型與UI結合,展示到頁面中
viewModel:視圖模型層,作為model和view的通信橋梁
雙向綁定的含義:當model數據發生變化的時候,會通知到view層,當用戶修改了view層的數據的時候,會反映到模型層。
而雙向數據綁定的好處在于:只關注于數據操作,DOM操作減少
Vue.js實現的原理就是采用的訪問器監聽,所以這里也采用訪問器監聽的方式實現簡單的數據雙向綁定。
訪問器監聽的實現,主要采用了javascript中原生方法:Object.defineProperty,該方法可以為某對象添加訪問器屬性,當訪問或者給該對象屬性賦值的時候,會觸發訪問器屬性,因此利用此思路,可以在訪問器屬性中添加處理程序。
這里先實現一個簡單的input標簽的數據雙向綁定過程,先大致了解一下什么是數據的雙向綁定。
<input type="text">
<script>
// 獲取到input輸入框對象
let input = document.querySelector('input');
// 創建一個沒有原型鏈的對象,用于監聽該對象的某屬性的變化
let model = Object.create(null);
// 當鼠標移開輸入框的時候,view層數據通知model層數據的變化
input.addEventListener('blur',function() {
model['user'] = this.value;
})
// 當model層數據發生變化的時候,通知view層數據的變化。
Object.defineProperty(model, 'user', {
set(v) {
user = v;
input.value = v;
},
get() {
return user;
}
})
</script>以上的代碼中首先對Input標簽對象進行獲取,然后對input元素對象添加監聽事件(blur),當事件被觸發的時候,也就是view層發生變化的時候,就需要去通知model層去更新數據,這里的model層利用的是一個沒有原型的空對象(使用空對象的原因:避免獲取某屬性的時候,由于原型鏈的存在,造成數據的誤讀)。
使用Object.defineProperty的方法,為該對象的指定屬性添加訪問器屬性,當該對象的屬性被修改,就會觸發setter訪問器,我們這里就可以為view層的數據賦值,更新view層的數據,這里的view層指的是Input標簽的屬性value。
看一下效果:
在文本框中輸入一個數據,在控制臺打印model.user可以看到數據已經影響到了model層

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

好啦,實現了最簡單的只針對于文本框的數據雙向綁定,我們可以從以上的案例中可以發現以下的實現邏輯:
①. 要實現view層到model的數據通信,就需要知道view層的數據變化了,以及view層的值,但是一般要獲取到標簽本身的值,除非有內置屬性,比如:input標簽的value屬性,可以獲得文本框的輸入值
②. 利用Object.defineProperty實現model層向view層的通信,當數據被修改,就會立馬觸發訪問器屬性setter,從而可以通知使用了該屬性的所有view層去更新他們的現在的數據(觀察者)
③. 被綁定的數據需要是作為一個對象的屬性,因為Object.defineProperty是對某一個對象的屬性開啟的訪問器特性。
爭對以上的總結,我們可以設計出類似于vue.js的數據雙向綁定模式:
利用自定義指令實現view到model層的數據通信
利用Object.defineProperty實現model層到view層的數據通信。
這里的實現涉及到三個主要的函數:
_observer: 對數據進行處理,重寫每一個屬性的getter/setter
_compile:對自定義指令(這里只涉及了e-bind/e-click/e-model)進行解析,并在解析過程中為節點綁定原生處理事件,以及實現view層到model層的綁定
Watcher: 作為model與view的中間橋梁,當model發生變化進一步更新view層
實現代碼:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>雙向數據綁定</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);
}
// 根據所給的自定義參數,進行數據雙向綁定的初始化工作
EBind.prototype._init = function (options) {
// options是初始化時的數據,包括el,data,method
this.$options = options;
// el是需要管理的Element對象,el:#app this.$el:id為app的Element對象
this.$el = document.querySelector(options.el);
// 數據
this.$data = options.data;
// 方法
this.$methods = options.methods;
// _binding保存著model與view的映射關系,也就是Wachter的實例,當model更新的時候,更新對應的view
this._binding = {};
// 重寫 this.$data的get和set方法
this._obverse(this.$data);
// 解析指令
this._compile(this.$el);
}
// 該函數的作用:對所有的this.$data里面的屬性進行監聽,訪問器監聽,實現model到view層的數據通信。當model層改變的時候通知view層
EBind.prototype._obverse = function (currentObj, completeKey) {
// 保存上下文
var _this = this;
// currentObj就是需要重寫get/set的對象,Object.keys獲取該對象的屬性,得到的是一個數組
// 對該數組進行遍歷
Object.keys(currentObj).forEach(function (key) {
// 當且僅當對象自身的屬性才監聽
if (currentObj.hasOwnProperty(key)) {
// 如果是某一對象的屬性,則需要以person.age的形式保存
var completeTempKey = completeKey ? completeKey + '.' + key : key;
// 建立需要監測屬性的關聯
_this._binding[completeTempKey] = {
_directives: [] // 存儲所有使用該數據的地方
};
// 獲取到當前屬性的值
var value = currentObj[key];
// 如果值是對象,則遍歷處理,對每個對象屬性都完全監測
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, // 避免默認為false
get() {
return value;
},
set(v) {
// value保存當前屬性的值
if (value != v) {
// 如果數據被修改,則需要通知每一個使用該數據的地方進行更新數據,也即:model通知view層,Watcher類作為中間層去完成該操作(通知操作)
value = v;
binding._directives.forEach(function (item) {
item.update();
})
}
}
})
}
})
}
// 該函數的作用是:對自定義指令進行編譯,為其添加原生監聽事件,實現view到model層的數據通信,也即當view層數據變化之后通知model層數據更新
// 實現原理:通過托管的element對象:this.$el,獲取到所有的子節點,遍歷所有的子節點,查看其是否有自定義屬性,如果有指定含義的自定義屬性
// 比如說:e-bind/e-model/e-click則根據節點上添加的自定義屬性的不同為其添加監聽事件
// e-click添加原生的onclick事件,這里主要注意點就是:需要將this.$method中指定方法的上下文this改為this.$data
// e-model為綁定的數據更新,這里只支持input,textarea標簽,原因:采用標簽自帶的value屬性實現的view到model層的數據通信
// e-bind
EBind.prototype._compile = function (root) {
// 保存執行上下文
var _this = this;
// 獲取到托管節點元素的所有子節點,只包括元素節點
var nodes = root.children;
for (let i = 0; i < nodes.length; i++) {
// 獲取到子節點/按順序
var node = nodes[i];
// 如果當前節點有子節點,則繼續逐層處理子節點
if (node.children.length) {
this._compile(node);
}
// 如果當前節點綁定了e-click屬性,則需要為當前節點綁定onclick事件
if (node.hasAttribute('e-click')) {
// hasAttribute可以獲取到自定義屬性
node.addEventListener('click',(function () {
// 獲取到當前節點的屬性值,也就是方法
var attrVal = node.getAttribute('e-click');
// 由于綁定的方法里面的數據要使用data里面的數據,所以需要將執行的函數的上下文,也就是this改為this.$data
// 而使用bind,不使用call/apply的原因是onclick方法需要觸發之后才會執行,而不是立馬執行
return _this.$methods[attrVal].bind(_this.$data);
})())
}
// 只對input和textarea標簽元素可以施行雙向綁定,原因:利用這兩個標簽的內置的value屬性實現雙向綁定
if (node.hasAttribute('e-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
// 給element對象添加監聽input事件 ,第二個參數是一個立即執行函數,獲取到節點的索引值,執行函數內部代碼,返回事件處理
node.addEventListener('input', (function (index) {
// 獲取到當前節點的屬性值,也就是方法
var attrVal = node.getAttribute('e-model');
// 給當前element對象添加model到view層的映射
_this._binding[attrVal]._directives.push(new Watcher({
name: 'input',
el: node,
eb: _this,
exp: attrVal,
attr: 'value'
}))
// 如果input標簽value值改變,此時需要更新model層的數據,也就是view層到model層的改變
return function () {
// 獲取到綁定的屬性,以.為分隔符,如果只是一個值,就直接獲取當前值,如果是個對象(obj.key)的形式,則綁定的其實obj對象
// 中的key的值,此時就需要獲取到key,并對key進行賦值為已改變的input標簽的value值
var keys = attrVal.split('.');
// 獲取上一步得到的屬性的集合中最后一個屬性(最后一個屬性才是真正被綁定的值)
var lastKey = keys[keys.length - 1];
// 獲得真正被綁定的值的父對象
// 因為如果是對象,比如:obj.key.val,則需要找到key的引用,因為這里要改變的是val
// 通過引用key 從而改變val的值,但是如果直接獲取到的val的引用,val是數值型存儲,賦值給另一個變量的時候,其實是新開辟的一個空間
// 并不能直接改變model層也就是this.$data里面的數據,而引用數據存儲的話,賦值給另一個變量,另一個變量的修改,會影響原來的引用的數據
// 所以這里需要找到真正被綁定值的父對象,也就是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出發去找目標屬性
}, _this.$data);
// model也就是之前說得父對象,obj.key中的obj,而lastkey也就是真正被綁定的屬性,找到了之后就需要對其更新為節點的值啦。
// 這里的model層被修改會觸發_observe里面的訪問器屬性setter,所以如果其他地方也使用了這個屬性的話,也會相應的發生改變哦
model[lastKey] = nodes[index].value;
}
})(i))
}
// 對節點上綁定e-bind,為其添加model到view的映射即可,原因:e-bind實現的是model到view的數據通信,而在this._observer中
// 已經通過definePrototype實現了,所以這里只需要添加通信,便于在_oberver中實現。
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: 節點名稱:文本節點:text, 輸入框:input
* el: 指令對應的DOM元素
* eb: 指令對應的EBind實例
* exp: 指令對應的值:e-bind="test";test就是指令對應的值
* attr: 綁定的屬性值, 比如:e-bind綁定的屬性,其實會反應到innerHTML中,v-model綁定的標簽會反應到value中
*/
function Watcher(options) {
this.$options = options;
this.update();
}
Watcher.prototype.update = function () {
// 保存上下文
var _this = this;
// 獲取到被綁定的對象
var keys = this.$options.exp.split('.');
// 獲取到DOM對象上要改變的屬性,對其進行更改
this.$options.el[this.$options.attr] = keys.reduce(function (value, key) {
return value[key];
}, _this.$options.eb.$data)
}實現效果:

到此,相信大家對“js怎么實現數據雙向綁定”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。