js實(shí)現(xiàn)數(shù)據(jù)雙向綁定(訪問(wèn)器監(jiān)聽(tīng))
本文實(shí)例為大家分享了js實(shí)現(xiàn)數(shù)據(jù)雙向綁定的具體代碼,供大家參考,具體內(nèi)容如下
雙向綁定:
雙向綁定基于MVVM模型:model-view-viewModel
model: 模型層,負(fù)責(zé)業(yè)務(wù)邏輯以及與數(shù)據(jù)庫(kù)的交互
view:視圖層,負(fù)責(zé)將數(shù)據(jù)模型與UI結(jié)合,展示到頁(yè)面中
viewModel:視圖模型層,作為model和view的通信橋梁
雙向綁定的含義:當(dāng)model數(shù)據(jù)發(fā)生變化的時(shí)候,會(huì)通知到view層,當(dāng)用戶修改了view層的數(shù)據(jù)的時(shí)候,會(huì)反映到模型層。
而雙向數(shù)據(jù)綁定的好處在于:只關(guān)注于數(shù)據(jù)操作,DOM操作減少
Vue.js實(shí)現(xiàn)的原理就是采用的訪問(wèn)器監(jiān)聽(tīng),所以這里也采用訪問(wèn)器監(jiān)聽(tīng)的方式實(shí)現(xiàn)簡(jiǎn)單的數(shù)據(jù)雙向綁定。
訪問(wèn)器監(jiān)聽(tīng)的實(shí)現(xiàn),主要采用了javascript中原生方法:Object.defineProperty,該方法可以為某對(duì)象添加訪問(wèn)器屬性,當(dāng)訪問(wèn)或者給該對(duì)象屬性賦值的時(shí)候,會(huì)觸發(fā)訪問(wèn)器屬性,因此利用此思路,可以在訪問(wèn)器屬性中添加處理程序。
這里先實(shí)現(xiàn)一個(gè)簡(jiǎn)單的input標(biāo)簽的數(shù)據(jù)雙向綁定過(guò)程,先大致了解一下什么是數(shù)據(jù)的雙向綁定。
<input type="text">
<script>
// 獲取到input輸入框?qū)ο?
let input = document.querySelector('input');
// 創(chuàng)建一個(gè)沒(méi)有原型鏈的對(duì)象,用于監(jiān)聽(tīng)該對(duì)象的某屬性的變化
let model = Object.create(null);
// 當(dāng)鼠標(biāo)移開(kāi)輸入框的時(shí)候,view層數(shù)據(jù)通知model層數(shù)據(jù)的變化
input.addEventListener('blur',function() {
model['user'] = this.value;
})
// 當(dāng)model層數(shù)據(jù)發(fā)生變化的時(shí)候,通知view層數(shù)據(jù)的變化。
Object.defineProperty(model, 'user', {
set(v) {
user = v;
input.value = v;
},
get() {
return user;
}
})
</script>
以上的代碼中首先對(duì)Input標(biāo)簽對(duì)象進(jìn)行獲取,然后對(duì)input元素對(duì)象添加監(jiān)聽(tīng)事件(blur),當(dāng)事件被觸發(fā)的時(shí)候,也就是view層發(fā)生變化的時(shí)候,就需要去通知model層去更新數(shù)據(jù),這里的model層利用的是一個(gè)沒(méi)有原型的空對(duì)象(使用空對(duì)象的原因:避免獲取某屬性的時(shí)候,由于原型鏈的存在,造成數(shù)據(jù)的誤讀)。
使用Object.defineProperty的方法,為該對(duì)象的指定屬性添加訪問(wèn)器屬性,當(dāng)該對(duì)象的屬性被修改,就會(huì)觸發(fā)setter訪問(wèn)器,我們這里就可以為view層的數(shù)據(jù)賦值,更新view層的數(shù)據(jù),這里的view層指的是Input標(biāo)簽的屬性value。
看一下效果:
在文本框中輸入一個(gè)數(shù)據(jù),在控制臺(tái)打印model.user可以看到數(shù)據(jù)已經(jīng)影響到了model層

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

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

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
javascript實(shí)現(xiàn)百度地圖鼠標(biāo)滑動(dòng)事件顯示、隱藏
這篇文章主要介紹了javascript實(shí)現(xiàn)百度地圖鼠標(biāo)滑動(dòng)事件顯示、隱藏的思路和方法,十分的實(shí)用,這里推薦給小伙伴們,有需要的朋友可以參考下。2015-04-04
JS中Json數(shù)據(jù)的處理和解析JSON數(shù)據(jù)的方法詳解
JSON (JavaScript Object Notation)一種簡(jiǎn)單的數(shù)據(jù)格式,比xml更輕巧,這篇文章主要介紹了JS中Json數(shù)據(jù)的處理和解析JSON數(shù)據(jù)的方法詳解的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-06-06
深入理解ES6之?dāng)?shù)據(jù)解構(gòu)的用法
本文介紹了深入理解ES6之?dāng)?shù)據(jù)解構(gòu)的用法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
微信小程序?qū)崿F(xiàn)音樂(lè)播放頁(yè)面布局
這篇文章主要為大家詳細(xì)介紹了JS實(shí)現(xiàn)京東商品分類側(cè)邊欄,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-12-12
返回頂部按鈕響應(yīng)滾動(dòng)且動(dòng)態(tài)顯示與隱藏
返回頂部功能在很多的網(wǎng)站上都有,判斷滾動(dòng)參數(shù)動(dòng)態(tài)顯示與隱藏,下面的示例很實(shí)用,喜歡的朋友可以收藏下2014-10-10

