如何在JavaScript中实现DOM数据绑定

请严格把这个问题当作教育问题来对待。我仍然有兴趣听到新的答案和想法来实现这一点

博士tl;

如何用JavaScript实现双向数据绑定?

到DOM的数据绑定

通过数据绑定到DOM,我的意思是,例如,拥有一个带有属性b的JavaScript对象a。然后有一个<input> DOM元素(例如),当DOM元素改变时,a也会改变,反之亦然(也就是说,我的意思是双向数据绑定)。

下面是AngularJS的一个图:

双向数据绑定

JavaScript是这样的:

var a = {b:3};

然后输入(或其他形式)元素,如:

<input type='text' value=''>

我希望输入的值是a.b的值(例如),当输入文本发生变化时,我希望a.b也发生变化。当JavaScript中的a.b改变时,输入也会改变。

这个问题

在纯JavaScript中完成这个任务的基本技术是什么?

具体来说,我想要一个好的答案参考:

  • 对象绑定是如何工作的?
  • 倾听形式上的变化是如何起作用的?
  • 是否有可能以一种简单的方式只在模板级别修改HTML ?我不想在HTML文档本身中跟踪绑定,而只在JavaScript中跟踪绑定(使用DOM事件,JavaScript保持对所使用的DOM元素的引用)。

我都试过什么?

我是一个大粉丝的胡子,所以我尝试使用它的模板。然而,我在尝试执行数据绑定本身时遇到了问题,因为Mustache将HTML处理为字符串,所以在我得到它的结果后,我没有引用到我的视图模型中的对象在哪里。我能想到的唯一解决办法是用属性修改HTML字符串(或创建DOM树)本身。我不介意使用不同的模板引擎。

基本上,我有一种强烈的感觉,我把手头的问题复杂化了,有一个简单的解决办法。

注意:请不要提供使用外部库的答案,特别是那些数千行代码的答案。我用过(而且喜欢!)AngularJS和KnockoutJS。我真的不想要“使用框架x”这种形式的答案。最理想的情况是,我希望未来不知道如何使用许多框架的读者能够自己掌握如何实现双向数据绑定。我不期待一个完整的的答案,但一个能让人理解的答案。

192653 次浏览
  • 对象绑定是如何工作的?
  • 倾听形式上的变化是如何起作用的?

更新两个对象的抽象

我认为还有其他技术,但最终我将拥有一个对象,该对象保存对相关DOM元素的引用,并提供一个接口,以协调对其自身数据及其相关元素的更新。

.addEventListener()为此提供了一个非常好的接口。你可以给它一个实现eventListener接口的对象,它将用该对象作为this值调用它的处理程序。

这使您可以自动访问元素及其相关数据。

定义对象

原型继承是实现这一点的好方法,当然不是必需的。首先,创建一个构造函数来接收元素和一些初始数据。

function MyCtor(element, data) {
this.data = data;
this.element = element;
element.value = data;
element.addEventListener("change", this, false);
}

因此,这里构造函数将元素和数据存储在新对象的属性上。它还将change事件绑定到给定的element。有趣的是,它将new对象而不是函数作为第二个参数传递。但单靠这个是不行的。

实现eventListener接口

要做到这一点,你的对象需要实现eventListener接口。要做到这一点,只需要给对象一个handleEvent()方法。

这就是继承的作用。

MyCtor.prototype.handleEvent = function(event) {
switch (event.type) {
case "change": this.change(this.element.value);
}
};


MyCtor.prototype.change = function(value) {
this.data = value;
this.element.value = value;
};

有许多不同的方法可以结构化,但对于你的协调更新的例子,我决定让change()方法只接受一个值,并让handleEvent传递该值而不是事件对象。这样,change()也可以在没有事件的情况下被调用。

所以现在,当change事件发生时,它会更新元素和.data属性。在JavaScript程序中调用.change()时也会发生同样的情况。

使用代码

现在只需创建新对象,并让它执行更新。JS代码中的更新将出现在输入中,并且输入中的更改事件将对JS代码可见。

var obj = new MyCtor(document.getElementById("foo"), "20");


// simulate some JS based changes.
var i = 0;
setInterval(function() {
obj.change(parseInt(obj.element.value) + ++i);
}, 3000);

演示:< em > < / em > http://jsfiddle.net/RkTMD/

我想给我的预习添点什么。我建议使用一种稍微不同的方法,它允许您不使用方法而简单地为对象分配一个新值。但必须指出的是,特别是老版本的浏览器不支持这一点,IE9仍然需要使用不同的界面。

最值得注意的是,我的方法没有使用事件。

getter和setter

我的建议使用了getter和setter相对年轻的特性,特别是仅使用setter。一般来说,突变器允许我们“自定义”某些属性如何赋值和检索的行为。

我将在这里使用的一个实现是Object.defineProperty方法。它适用于火狐,谷歌浏览器,我想还有IE9。还没有测试其他浏览器,但由于这只是理论…

不管怎样,它接受三个参数。第一个参数是您希望为其定义新属性的对象,第二个参数是类似于新属性名称的字符串,最后一个“描述符对象”提供关于新属性行为的信息。

两个特别有趣的描述符是getset。示例如下所示。注意,使用这两个描述符将禁止使用其他4个描述符。

function MyCtor( bindTo ) {
// I'll omit parameter validation here.


Object.defineProperty(this, 'value', {
enumerable: true,
get : function ( ) {
return bindTo.value;
},
set : function ( val ) {
bindTo.value = val;
}
});
}

现在使用这个有点不同:

var obj = new MyCtor(document.getElementById('foo')),
i = 0;
setInterval(function() {
obj.value += ++i;
}, 3000);

我想强调的是,这只适用于现代浏览器。

工作小提琴:http://jsfiddle.net/Derija93/RkTMD/1/

所以,我决定把我自己的解决方案扔进锅里。注意,这只在非常现代的浏览器上运行。

它使用什么

这个实现非常现代——它需要一个(非常)现代的浏览器和用户两项新技术:

  • MutationObservers来检测dom中的变化(事件监听器也被使用)
  • Object.observe来检测对象中的变化并通知dom。危险,由于这个答案已经被ECMAScript TC讨论并决定反对,考虑一个polyfill

它是如何工作的

  • 在元素上,放置一个domAttribute:objAttribute映射——例如bind='textContent:name'
  • 在dataBind函数中读取它。观察元素和对象的变化。
  • 当发生更改时,更新相关元素。

解决方案

下面是dataBind函数,注意它只有20行代码,可以更短:

function dataBind(domElement, obj) {
var bind = domElement.getAttribute("bind").split(":");
var domAttr = bind[0].trim(); // the attribute on the DOM element
var itemAttr = bind[1].trim(); // the attribute the object


// when the object changes - update the DOM
Object.observe(obj, function (change) {
domElement[domAttr] = obj[itemAttr];
});
// when the dom changes - update the object
new MutationObserver(updateObj).observe(domElement, {
attributes: true,
childList: true,
characterData: true
});
domElement.addEventListener("keyup", updateObj);
domElement.addEventListener("click",updateObj);
function updateObj(){
obj[itemAttr] = domElement[domAttr];
}
// start the cycle by taking the attribute from the object and updating it.
domElement[domAttr] = obj[itemAttr];
}

下面是一些用法:

HTML:

<div id='projection' bind='textContent:name'></div>
<input type='text' id='textView' bind='value:name' />

JavaScript:

var obj = {
name: "Benjamin"
};
var el = document.getElementById("textView");
dataBind(el, obj);
var field = document.getElementById("projection");
dataBind(field,obj);

这是一个< >强工作小提琴< / >强。注意,这个解决方案是相当通用的。对象。可以使用观察和突变观察器shimming。

我认为我的答案会更有技术含量,但不会像其他人用不同的技术展示同样的东西那样不同 所以,首先,这个问题的解决方案是使用一种被称为“观察者”的设计模式,它可以让你的数据从你的表示中解耦,使一个东西的变化被广播到它们的侦听器,但在这种情况下,它是双向的

对于DOM到JS的方式

要将DOM中的数据绑定到js对象,可以添加data属性(如果需要兼容性,也可以添加类)形式的标记,如下所示:

<input type="text" data-object="a" data-property="b" id="b" class="bind" value=""/>
<input type="text" data-object="a" data-property="c" id="c" class="bind" value=""/>
<input type="text" data-object="d" data-property="e" id="e" class="bind" value=""/>

这样,它可以通过js使用querySelectorAll(或老朋友getElementsByClassName兼容性)访问。

现在你可以绑定事件监听变化的方式:每个对象一个监听器,或者容器/文档一个大监听器。绑定到文档/容器将为它或它的子容器中所做的每一个更改触发事件,它将占用更小的内存,但会产生事件调用 代码看起来像这样:

//Bind to each element
var elements = document.querySelectorAll('input[data-property]');


function toJS(){
//Assuming `a` is in scope of the document
var obj = document[this.data.object];
obj[this.data.property] = this.value;
}


elements.forEach(function(el){
el.addEventListener('change', toJS, false);
}


//Bind to document
function toJS2(){
if (this.data && this.data.object) {
//Again, assuming `a` is in document's scope
var obj = document[this.data.object];
obj[this.data.property] = this.value;
}
}


document.addEventListener('change', toJS2, false);

对于JS做DOM的方式

你需要两件事:一个元对象,它将持有DOM元素的引用,绑定到每个js对象/属性,以及一种方法来监听对象的变化。基本上是相同的方式:您必须有一种方法来侦听对象中的更改,然后将其绑定到DOM节点,因为您的对象“不能有”元数据,您将需要另一个对象以属性名称映射到元数据对象的属性的方式保存元数据。 代码是这样的:

var a = {
b: 'foo',
c: 'bar'
},
d = {
e: 'baz'
},
metadata = {
b: 'b',
c: 'c',
e: 'e'
};
function toDOM(changes){
//changes is an array of objects changed and what happened
//for now i'd recommend a polyfill as this syntax is still a proposal
changes.forEach(function(change){
var element = document.getElementById(metadata[change.name]);
element.value = change.object[change.name];
});
}
//Side note: you can also use currying to fix the second argument of the function (the toDOM method)
Object.observe(a, toDOM);
Object.observe(d, toDOM);

希望我能帮上忙。

在这个链接简单的JavaScript双向数据绑定中有一个非常简单的2-way数据绑定的基本实现

前面的链接以及knockoutjs, backbone.js和agile .js的想法,导致了这个轻量级、快速的MVVM框架ModelView.js 基于jQuery,它可以很好地与jQuery一起使用,我是它的谦逊(或者不那么谦逊)作者。

重现下面的示例代码(来自博客文章链接):

DataBinder的示例代码

function DataBinder( object_id ) {
// Use a jQuery object as simple PubSub
var pubSub = jQuery({});


// We expect a `data` element specifying the binding
// in the form: data-bind-<object_id>="<property_name>"
var data_attr = "bind-" + object_id,
message = object_id + ":change";


// Listen to change events on elements with the data-binding attribute and proxy
// them to the PubSub, so that the change is "broadcasted" to all connected objects
jQuery( document ).on( "change", "[data-" + data_attr + "]", function( evt ) {
var $input = jQuery( this );


pubSub.trigger( message, [ $input.data( data_attr ), $input.val() ] );
});


// PubSub propagates changes to all bound elements, setting value of
// input tags or HTML content of other tags
pubSub.on( message, function( evt, prop_name, new_val ) {
jQuery( "[data-" + data_attr + "=" + prop_name + "]" ).each( function() {
var $bound = jQuery( this );


if ( $bound.is("input, textarea, select") ) {
$bound.val( new_val );
} else {
$bound.html( new_val );
}
});
});


return pubSub;
}
对于JavaScript对象,a的最小实现 本实验的用户模型可以如下:

function User( uid ) {
var binder = new DataBinder( uid ),


user = {
attributes: {},


// The attribute setter publish changes using the DataBinder PubSub
set: function( attr_name, val ) {
this.attributes[ attr_name ] = val;
binder.trigger( uid + ":change", [ attr_name, val, this ] );
},


get: function( attr_name ) {
return this.attributes[ attr_name ];
},


_binder: binder
};


// Subscribe to the PubSub
binder.on( uid + ":change", function( evt, attr_name, new_val, initiator ) {
if ( initiator !== user ) {
user.set( attr_name, new_val );
}
});


return user;
}
现在,当我们想要将一个模型的属性绑定到一个UI块时,我们 只需要设置相应的数据属性 HTML元素:< / p >
// javascript
var user = new User( 123 );
user.set( "name", "Wolfgang" );


<!-- html -->
<input type="number" data-bind-123="name" />

昨天,我开始编写自己的绑定数据的方法。

玩它很有趣。

我认为它很漂亮,很有用。至少在我使用firefox和chrome进行的测试中,Edge也能正常工作。不确定其他人,但如果他们支持代理,我认为它会起作用。

https://jsfiddle.net/2ozoovne/1/

<H1>Bind Context 1</H1>
<input id='a' data-bind='data.test' placeholder='Button Text' />
<input id='b' data-bind='data.test' placeholder='Button Text' />
<input type=button id='c' data-bind='data.test' />
<H1>Bind Context 2</H1>
<input id='d' data-bind='data.otherTest' placeholder='input bind' />
<input id='e' data-bind='data.otherTest' placeholder='input bind' />
<input id='f' data-bind='data.test' placeholder='button 2 text - same var name, other context' />
<input type=button id='g' data-bind='data.test' value='click here!' />
<H1>No bind data</H1>
<input id='h' placeholder='not bound' />
<input id='i' placeholder='not bound'/>
<input type=button id='j' />

代码如下:

(function(){
if ( ! ( 'SmartBind' in window ) ) { // never run more than once
// This hack sets a "proxy" property for HTMLInputElement.value set property
var nativeHTMLInputElementValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
var newDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
newDescriptor.set=function( value ){
if ( 'settingDomBind' in this )
return;
var hasDataBind=this.hasAttribute('data-bind');
if ( hasDataBind ) {
this.settingDomBind=true;
var dataBind=this.getAttribute('data-bind');
if ( ! this.hasAttribute('data-bind-context-id') ) {
console.error("Impossible to recover data-bind-context-id attribute", this, dataBind );
} else {
var bindContextId=this.getAttribute('data-bind-context-id');
if ( bindContextId in SmartBind.contexts ) {
var bindContext=SmartBind.contexts[bindContextId];
var dataTarget=SmartBind.getDataTarget(bindContext, dataBind);
SmartBind.setDataValue( dataTarget, value);
} else {
console.error( "Invalid data-bind-context-id attribute", this, dataBind, bindContextId );
}
}
delete this.settingDomBind;
}
nativeHTMLInputElementValue.set.bind(this)( value );
}
Object.defineProperty(HTMLInputElement.prototype, 'value', newDescriptor);


var uid= function(){
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}


// SmartBind Functions
window.SmartBind={};
SmartBind.BindContext=function(){
var _data={};
var ctx = {
"id" : uid()    /* Data Bind Context Id */
, "_data": _data        /* Real data object */
, "mapDom": {}          /* DOM Mapped objects */
, "mapDataTarget": {}       /* Data Mapped objects */
}
SmartBind.contexts[ctx.id]=ctx;
ctx.data=new Proxy( _data, SmartBind.getProxyHandler(ctx, "data"))  /* Proxy object to _data */
return ctx;
}


SmartBind.getDataTarget=function(bindContext, bindPath){
var bindedObject=
{ bindContext: bindContext
, bindPath: bindPath
};
var dataObj=bindContext;
var dataObjLevels=bindPath.split('.');
for( var i=0; i<dataObjLevels.length; i++ ) {
if ( i == dataObjLevels.length-1 ) { // last level, set value
bindedObject={ target: dataObj
, item: dataObjLevels[i]
}
} else {    // digg in
if ( ! ( dataObjLevels[i] in dataObj ) ) {
console.warn("Impossible to get data target object to map bind.", bindPath, bindContext);
break;
}
dataObj=dataObj[dataObjLevels[i]];
}
}
return bindedObject ;
}


SmartBind.contexts={};
SmartBind.add=function(bindContext, domObj){
if ( typeof domObj == "undefined" ){
console.error("No DOM Object argument given ", bindContext);
return;
}
if ( ! domObj.hasAttribute('data-bind') ) {
console.warn("Object has no data-bind attribute", domObj);
return;
}
domObj.setAttribute("data-bind-context-id", bindContext.id);
var bindPath=domObj.getAttribute('data-bind');
if ( bindPath in bindContext.mapDom ) {
bindContext.mapDom[bindPath][bindContext.mapDom[bindPath].length]=domObj;
} else {
bindContext.mapDom[bindPath]=[domObj];
}
var bindTarget=SmartBind.getDataTarget(bindContext, bindPath);
bindContext.mapDataTarget[bindPath]=bindTarget;
domObj.addEventListener('input', function(){ SmartBind.setDataValue(bindTarget,this.value); } );
domObj.addEventListener('change', function(){ SmartBind.setDataValue(bindTarget, this.value); } );
}


SmartBind.setDataValue=function(bindTarget,value){
if ( ! ( 'target' in bindTarget ) ) {
var lBindTarget=SmartBind.getDataTarget(bindTarget.bindContext, bindTarget.bindPath);
if ( 'target' in lBindTarget ) {
bindTarget.target=lBindTarget.target;
bindTarget.item=lBindTarget.item;
} else {
console.warn("Still can't recover the object to bind", bindTarget.bindPath );
}
}
if ( ( 'target' in bindTarget ) ) {
bindTarget.target[bindTarget.item]=value;
}
}
SmartBind.getDataValue=function(bindTarget){
if ( ! ( 'target' in bindTarget ) ) {
var lBindTarget=SmartBind.getDataTarget(bindTarget.bindContext, bindTarget.bindPath);
if ( 'target' in lBindTarget ) {
bindTarget.target=lBindTarget.target;
bindTarget.item=lBindTarget.item;
} else {
console.warn("Still can't recover the object to bind", bindTarget.bindPath );
}
}
if ( ( 'target' in bindTarget ) ) {
return bindTarget.target[bindTarget.item];
}
}
SmartBind.getProxyHandler=function(bindContext, bindPath){
return  {
get: function(target, name){
if ( name == '__isProxy' )
return true;
// just get the value
// console.debug("proxy get", bindPath, name, target[name]);
return target[name];
}
,
set: function(target, name, value){
target[name]=value;
bindContext.mapDataTarget[bindPath+"."+name]=value;
SmartBind.processBindToDom(bindContext, bindPath+"."+name);
// console.debug("proxy set", bindPath, name, target[name], value );
// and set all related objects with this target.name
if ( value instanceof Object) {
if ( !( name in target) || ! ( target[name].__isProxy ) ){
target[name]=new Proxy(value, SmartBind.getProxyHandler(bindContext, bindPath+'.'+name));
}
// run all tree to set proxies when necessary
var objKeys=Object.keys(value);
// console.debug("...objkeys",objKeys);
for ( var i=0; i<objKeys.length; i++ ) {
bindContext.mapDataTarget[bindPath+"."+name+"."+objKeys[i]]=target[name][objKeys[i]];
if ( typeof value[objKeys[i]] == 'undefined' || value[objKeys[i]] == null || ! ( value[objKeys[i]] instanceof Object ) || value[objKeys[i]].__isProxy )
continue;
target[name][objKeys[i]]=new Proxy( value[objKeys[i]], SmartBind.getProxyHandler(bindContext, bindPath+'.'+name+"."+objKeys[i]));
}
// TODO it can be faster than run all items
var bindKeys=Object.keys(bindContext.mapDom);
for ( var i=0; i<bindKeys.length; i++ ) {
// console.log("test...", bindKeys[i], " for ", bindPath+"."+name);
if ( bindKeys[i].startsWith(bindPath+"."+name) ) {
// console.log("its ok, lets update dom...", bindKeys[i]);
SmartBind.processBindToDom( bindContext, bindKeys[i] );
}
}
}
return true;
}
};
}
SmartBind.processBindToDom=function(bindContext, bindPath) {
var domList=bindContext.mapDom[bindPath];
if ( typeof domList != 'undefined' ) {
try {
for ( var i=0; i < domList.length ; i++){
var dataTarget=SmartBind.getDataTarget(bindContext, bindPath);
if ( 'target' in dataTarget )
domList[i].value=dataTarget.target[dataTarget.item];
else
console.warn("Could not get data target", bindContext, bindPath);
}
} catch (e){
console.warn("bind fail", bindPath, bindContext, e);
}
}
}
}
})();

然后,设置,只需:

var bindContext=SmartBind.BindContext();
SmartBind.add(bindContext, document.getElementById('a'));
SmartBind.add(bindContext, document.getElementById('b'));
SmartBind.add(bindContext, document.getElementById('c'));


var bindContext2=SmartBind.BindContext();
SmartBind.add(bindContext2, document.getElementById('d'));
SmartBind.add(bindContext2, document.getElementById('e'));
SmartBind.add(bindContext2, document.getElementById('f'));
SmartBind.add(bindContext2, document.getElementById('g'));


setTimeout( function() {
document.getElementById('b').value='Via Script works too!'
}, 2000);


document.getElementById('g').addEventListener('click',function(){
bindContext2.data.test='Set by js value'
})

现在,我只添加了HTMLInputElement值绑定。

如果你知道如何改进,请告诉我。

改变元素的值可以触发DOM事件。可以使用响应事件的侦听器在JavaScript中实现数据绑定。

例如:

function bindValues(id1, id2) {
const e1 = document.getElementById(id1);
const e2 = document.getElementById(id2);
e1.addEventListener('input', function(event) {
e2.value = event.target.value;
});
e2.addEventListener('input', function(event) {
e1.value = event.target.value;
});
}

在这里是一个代码和演示,展示了DOM元素如何相互绑定或与JavaScript对象绑定。

我已经通过一些基本的javascript示例,使用onkeypress和onchange事件处理程序,使绑定视图到我们的js和js视图

下面是活塞http://plnkr.co/edit/7hSOIFRTvqLAvdZT4Bcc?p=preview的例子

<!DOCTYPE html>
<html>
<body>


<p>Two way binding data.</p>


<p>Binding data from  view to JS</p>


<input type="text" onkeypress="myFunction()" id="myinput">
<p id="myid"></p>
<p>Binding data from  js to view</p>
<input type="text" id="myid2" onkeypress="myFunction1()" oninput="myFunction1()">
<p id="myid3" onkeypress="myFunction1()" id="myinput" oninput="myFunction1()"></p>


<script>


document.getElementById('myid2').value="myvalue from script";
document.getElementById('myid3').innerHTML="myvalue from script";
function myFunction() {
document.getElementById('myid').innerHTML=document.getElementById('myinput').value;
}
document.getElementById("myinput").onchange=function(){


myFunction();


}
document.getElementById("myinput").oninput=function(){


myFunction();


}


function myFunction1() {


document.getElementById('myid3').innerHTML=document.getElementById('myid2').value;
}
</script>


</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>


<input type="text" id="demo" name="">
<p id="view"></p>
<script type="text/javascript">
var id = document.getElementById('demo');
var view = document.getElementById('view');
id.addEventListener('input', function(evt){
view.innerHTML = this.value;
});


</script>
</body>
</html>

绑定任何html输入

<input id="element-to-bind" type="text">

定义两个函数:

function bindValue(objectToBind) {
var elemToBind = document.getElementById(objectToBind.id)
elemToBind.addEventListener("change", function() {
objectToBind.value = this.value;
})
}


function proxify(id) {
var handler = {
set: function(target, key, value, receiver) {
target[key] = value;
document.getElementById(target.id).value = value;
return Reflect.set(target, key, value);
},
}
return new Proxy({id: id}, handler);
}

使用函数:

var myObject = proxify('element-to-bind')
bindValue(myObject);

这是一个非常简单的双向数据绑定在香草javascript....

<input type="text" id="inp" onkeyup="document.getElementById('name').innerHTML=document.getElementById('inp').value;">


<div id="name">


</div>

将变量绑定到输入的一个简单方法(双向绑定)是直接访问getter和setter中的输入元素:

var variable = function(element){
return {
get : function () { return element.value;},
set : function (value) { element.value = value;}
}
};

在HTML中:

<input id="an-input" />
<input id="another-input" />

并使用:

var myVar = new variable(document.getElementById("an-input"));
myVar.set(10);


// and another example:
var myVar2 = new variable(document.getElementById("another-input"));
myVar.set(myVar2.get());
人力资源/ > < p > < 一个不需要getter/setter的更好的方法是:

var variable = function(element){


return function () {
if(arguments.length > 0)
element.value = arguments[0];


else return element.value;
}


}

使用方法:

var v1 = new variable(document.getElementById("an-input"));
v1(10); // sets value to 20.
console.log(v1()); // reads value.

下面是一个使用Object.defineProperty直接修改属性访问方式的想法。

代码:

function bind(base, el, varname) {
Object.defineProperty(base, varname, {
get: () => {
return el.value;
},
set: (value) => {
el.value = value;
}
})
}

用法:

var p = new some_class();
bind(p,document.getElementById("someID"),'variable');


p.variable="yes"

小提琴:在这里

迟来的派对,特别是因为我在几个月/几年前写了两篇相关的lib,我以后会提到它们,但看起来仍然与我有关。简短地说,我选择的技术是:

  • Proxy用于模型的观察
  • MutationObserver用于跟踪DOM的更改(用于绑定原因,而不是值更改)
  • 值的变化(从视图到模型流)是通过常规的addEventListener处理程序处理的

恕我直言,除了OP之外,重要的是数据绑定实现将:

  • 处理不同的应用生命周期情况(HTML先,然后JS, JS先,然后HTML,动态属性变化等)
  • 允许深度绑定模型,这样可以绑定user.address.block
  • 数组作为模型应该被正确支持(shiftsplice等)
  • 处理ShadowDOM
  • 尽量简化技术替换,因此任何模板子语言都是一种不适合未来更改的方法,因为它与框架耦合得太紧密了

考虑到所有这些因素,在我看来不可能仅仅抛出几十行JS代码。我试着把它作为模式而不是自由 -对我来说没用。

接下来,删除Object.observe,但考虑到对模型的观察是至关重要的部分-这整个部分必须被关注-分离到另一个库。现在说到我如何处理这个问题的原则——正如OP所问的那样:

模型(JS部分)

我对模型观察的看法是代理,恕我直言,这是使它工作的唯一理智的方式。 功能齐全的observer值得拥有自己的库,所以我为此开发了object-observer

模型应该通过一些专用的API注册,这就是pojo变成__abc0的地方,在这里看不到任何快捷方式。DOM元素被认为是绑定视图(见下文),首先用模型的值更新,然后在每次数据更改时更新。

视图(HTML部分)

恕我直言,表达绑定最简洁的方式是通过属性。很多人以前这样做过,很多人以后也会这样做,所以这里没有新闻,这是一种正确的方法。在我的例子中,我使用了以下语法:<span data-tie="modelKey:path.to.data => targerProperty"></span>,但这并不重要。对我来说很重要,在HTML中没有复杂的脚本语法-这是错误的,再次,恕我直言。

首先要收集指定为绑定视图的所有元素。在我看来,从性能角度来看,管理模型和视图之间的一些内部映射是不可避免的,似乎是牺牲内存+一些管理来节省运行时查找和更新的正确情况。

视图首先从模型中更新(如果可用的话),随后在模型更改时更新,如我们所说。 而且,整个DOM应该通过MutationObserver来观察,以便对动态添加/删除/更改的元素做出反应(绑定/解绑定)。 此外,所有这些都应该复制到ShadowDOM中(当然是打开的),以不留下未绑定的黑洞

具体细节还可以进一步列出,但在我看来,这些是实现数据绑定的主要原则,一方面是功能的完整性,另一方面是简单性。

因此,除了上面提到的object-observer之外,我还编写了data-tier库,它沿着上面提到的概念实现了数据绑定。

在过去的7年里,情况发生了很大的变化,我们现在在大多数浏览器中都有本地web组件。在我看来,问题的核心是在元素之间共享状态,一旦你有了它,当状态改变时更新ui就很简单了,反之亦然。

为了在元素之间共享数据,你可以创建一个StateObserver类,并以此扩展你的web组件。一个最小的实现是这样的:

// create a base class to handle state
class StateObserver extends HTMLElement {
constructor () {
super()
StateObserver.instances.push(this)
}
stateUpdate (update) {
StateObserver.lastState = StateObserver.state
StateObserver.state = update
StateObserver.instances.forEach((i) => {
if (!i.onStateUpdate) return
i.onStateUpdate(update, StateObserver.lastState)
})
}
}


StateObserver.instances = []
StateObserver.state = {}
StateObserver.lastState = {}


// create a web component which will react to state changes
class CustomReactive extends StateObserver {
onStateUpdate (state, lastState) {
if (state.someProp === lastState.someProp) return
this.innerHTML = `input is: ${state.someProp}`
}
}
customElements.define('custom-reactive', CustomReactive)


class CustomObserved extends StateObserver {
connectedCallback () {
this.querySelector('input').addEventListener('input', (e) => {
this.stateUpdate({ someProp: e.target.value })
})
}
}
customElements.define('custom-observed', CustomObserved)
<custom-observed>
<input>
</custom-observed>
<br />
<custom-reactive></custom-reactive>

提琴在这里

我喜欢这种方法,因为:

  • 没有dom遍历来查找data-属性
  • 没有对象。观察(弃用)
  • 没有代理(它提供了一个钩子,但没有通信机制)
  • 无依赖关系(取决于目标浏览器的填充除外)
  • 它是相当集中的&模块化的……用html描述状态,到处都有监听器会很快变得混乱。
  • 它是可扩展的。这个基本实现是20行代码,但是您可以轻松地构建一些便利性、不可变性和状态形状魔术,使其更易于使用。