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


244

请将此问题视为严格的教育意义。我仍然有兴趣听到新的答案和想法来实现这一目标

tl; dr

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

数据绑定到DOM

例如,通过将数据绑定到DOM,我的意思是拥有一个a带property 的JavaScript对象b。然后有一个<input>DOM元素(例如),当DOM元素发生变化时,它a也会发生变化,反之亦然(即,我是指双向数据绑定)。

这是AngularJS的示意图:

双向数据绑定

所以基本上我的JavaScript类似于:

var a = {b:3};

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

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

我希望输入的值是a.b(例如)的值,并且当输入文本更改时,我也想a.b更改。当a.bJavaScript更改时,输入也会更改。

问题

有什么基本的技术可以在普通JavaScript中完成此操作?

具体来说,我想提供一个很好的答案:

  • 绑定如何作用于对象?
  • 如何聆听表格中的更改可能会起作用?
  • 是否可以通过简单的方式仅在模板级别修改HTML?我不想在HTML文档本身中跟踪绑定,而仅在JavaScript中跟踪(带有DOM事件,而JavaScript始终引用所使用的DOM元素)。

我尝试了什么?

我是Mustache的忠实粉丝,所以我尝试使用它进行模板制作。但是,由于Mustache将HTML作为字符串处理,因此在尝试自行执行数据绑定时遇到了问题,因此,在获得结果后,我将不再引用视图模型中对象的位置。我可以想到的唯一解决方法是使用属性修改HTML字符串(或创建的DOM树)本身。我不介意使用其他模板引擎。

基本上,我有一种很强烈的感觉,就是正在手头的问题变得复杂,并且有一个简单的解决方案。

注意:请不要提供使用外部库的答案,尤其是成千上万行代码的库。我用过(并且喜欢!)AngularJS和KnockoutJS。我真的不想要“使用框架x”形式的答案。理想情况下,我希望将来的读者不知道如何使用许多框架来自己掌握如何实现双向数据绑定。我不希望得到一个完整的答案,但是会得到一个完整的想法。


2
我基于Benjamin Gruenbaum的设计创建了CrazyGlue。它还支持SELECT,复选框和单选标记。jQuery是一个依赖项。
JohnSz 2014年

12
这个问题太棒了。如果它因脱位或其他愚蠢的胡说而被关闭,我将被认真地选中。
OCDev 2014年

@JohnSz感谢您提到您的CrazyGlue项目。我一直在寻找一种简单的2路数据绑定器。看来您没有使用Object.observe,因此您的浏览器支持应该很棒。而且您没有使用胡须模板,因此非常完美。
加文2015年

@Benjamin你最终在做什么?
约翰尼

我认为@johnny的正确方法是在JS中创建DOM(如React),而不是相反。我认为最终这就是我们要做的。
本杰明·格林鲍姆

Answers:


106
  • 绑定如何作用于对象?
  • 如何聆听表格中的更改可能会起作用?

更新两个对象的抽象

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

.addEventListener()此提供了一个非常漂亮的界面。您可以为它提供一个实现该eventListener接口的对象,然后它将以该对象作为this值调用其处理程序。

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

定义对象

原型继承是实现此目的的好方法,尽管当然不是必需的。首先,您将创建一个构造函数,以接收您的元素和一些初始数据。

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

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

实施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属性。当您调用.change()JavaScript程序时,也会发生同样的情况。

使用代码

现在,您只需创建新对象,然后让它执行更新即可。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);

演示: http : //jsfiddle.net/RkTMD/


5
+1非常简洁的方法,简单易用,足以让人们学习,比我所拥有的要干净得多。一个常见的用例是在代码中使用模板来表示对象的视图。我想知道这在这里如何工作?在诸如Mustache之类的引擎中Mustache.render(template,object),我想做些事情,假设我想使对象与模板保持同步(不特定于Mustache),那么我将如何进行呢?
本杰明·格林鲍姆

3
@BenjaminGruenbaum:我没有使用过客户端模板,但是我可以想象,Mustache具有一些用于标识插入点的语法,并且该语法包括一个标签。因此,我认为模板的“静态”部分将呈现为存储在Array中的HTML块,而动态部分将位于这些块之间。然后,插入点上的标签将用作对象属性。然后,如果有人input要更新这些点之一,则将有一个从输入到该点的映射。我看看是否可以举一个简单的例子。

1
@BenjaminGruenbaum:嗯...我还没想过如何协调两个不同的元素。这比起初我想的要复杂得多。不过,我很好奇,因此我可能需要稍后进行处理。:)

2
您将看到有一个主要的Template构造函数来进行解析,保存不同的MyCtor对象,并提供一个接口以通过其标识符更新每个对象。如果您有任何问题,请告诉我。:) 编辑: ... 改为使用此链接 ...我忘记了每10秒输入值呈指数增长以演示JS更新。这限制了它。

2
... 完整评论的版本以及较小的改进。

36

因此,我决定将自己的解决方案放入锅中。这是工作中的小提琴。请注意,这只能在非常现代的浏览器上运行。

它用什么

此实现非常现代-需要(非常)现代的浏览器和用户两种新技术:

  • MutationObserverš以检测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);

这是工作中的小提琴。请注意,此解决方案非常通用。Object.observe和变异观察者匀场是可用的。


1
如果有人发现它有用,我只是碰巧写了这个(es5)-将自己踢倒jsfiddle.net/P9rMm
Benjamin Gruenbaum

1
请记住,当obj.name有设置者时,不能从外部观察它,而必须广播它在设置者内部已发生变化-html5rocks.com/en/tutorials/es7/observe/#toc-notifications-有点把扳手砸了如果您想要使用setter进行更复杂,相互依赖的行为,请使用Oo()。此外,当obj.name不可配置时,也不允许重新定义它的设置器(使用各种技巧来添加通知)-因此,在这种特定情况下,完全废弃了带有Oo()的泛型。
2014年

8
从所有浏览器中删除了Object.observe
JvdBerg

1
可以使用Proxy代替Object.observe或github.com/anywhereway/proxy-observegist.github.com/ebidel/1b553d571f924da2da06或更旧的polyfills,也可以在github @JvdBerg上使用
jimmont

29

我想添加到我的海报中。我建议使用一种稍有不同的方法,该方法将允许您简单地为对象分配新值,而无需使用方法。但必须注意,特别是较旧的浏览器不支持此功能,并且IE9仍需要使用其他接口。

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

吸气剂和二传手

我的建议利用了吸气剂和装夹器(尤其是装夹器)相对较年轻的功能。一般而言,增变器使我们可以“自定义”某些属性如何分配值和进行检索的行为。

我将在这里使用的一种实现是Object.defineProperty方法。它可以在FireFox,GoogleChrome和IE9中运行。尚未测试其他浏览器,但由于这只是理论上的...

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

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

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/


2
如果我们只有Harmony Proxy对象:)设置器确实是个不错的主意,但是那不要求我们修改实际的对象吗?另外,顺便说一句- Object.create可以在这里使用(再次,假设现代浏览器允许使用第二个参数)。同样,setter / getter可以用来“投影”与对象和DOM元素不同的值:)。我想知道您是否也对模板有任何见解,这似乎是一个真正的挑战,尤其是要很好地构造:)
Benjamin Gruenbaum

就像我的海报一样,对不起,我也不太会使用客户端模板引擎。:(但是您修改实际对象是什么意思呢?我想了解您对如何理解setter / getter可以用来...的想法。这里的getter / setter毫无用处。但将所有输入重定向到DOM元素以及从该对象检索到DOM元素,基本上Proxy就像您所说的那样;;)我理解的挑战是保持两个不同的属性同步。我的方法消除了两者之一。
Kiruse 2013年

A Proxy将消除使用getter / setter的需要,您可以在不知道元素具有什么属性的情况下对其进行绑定。我的意思是,获取器可以更改的范围比bindTo.value包含的逻辑(甚至模板)要多。问题是如何在考虑模板的情况下保持这种双向绑定?可以说我正在将对象映射到表单,我想使元素和表单保持同步,并且想知道如何处理这种事情。你可以看看如何在淘汰赛的作品learn.knockoutjs.com/#/?tutorial=intro例如
本杰明Gruenbaum

@BenjaminGruenbaum Gotcha。我来看一下。
Kiruse 2013年

@BenjaminGruenbaum我明白了您要了解的内容。考虑到所有这些模板,要困难得多。我将在脚本上工作一段时间(并不断对其进行基础调整)。但是现在,我要休息一下。我实际上没有足够的时间。
Kiruse

7

我认为我的答案会更具技术性,但别无二致,因为其他人使用不同的技术来呈现同一件事。
因此,首先,解决此问题的方法是使用称为“观察者”的设计模式,它使您可以将数据与演示文稿分离,从而将一件事的更改广播给他们的听众,但是在这种情况下它是双向的。

对于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);

我希望我能有所帮助。


.observer是否没有可比性问题?
Mohsen Shakiba,2015年

目前,它仅需要Object.observe镀铬就可以使用垫片或填充胶。caniuse.com/#feat=object-observe
madcampos

9
Object.observe已死。只是以为我会在这里注意。
本杰明·格伦鲍姆

@BenjaminGruenbaum既然已经死了,现在正确使用什么?
约翰尼

1
@johnny,如果我没有记错的话,那将是代理陷阱,因为它们允许对我可以处理的对象进行更精细的控制,但是我必须对此进行调查。
madcampos '16

7

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

玩起来很有趣。

我认为它很漂亮而且非常有用。至少在我使用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值绑定。

让我知道您是否知道如何改进它。


6

在此链接“ JavaScript中的轻松双向数据绑定”中,有非常简单的2向数据绑定的准系统实现。

先前的链接以及敲除js,border.js和agility.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对象有关的问题,为进行本实验,用户模型的最小实现可能如下:

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元素上设置适当的data属性即可:

// javascript
var user = new User( 123 );
user.set( "name", "Wolfgang" );

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

尽管此链接可以回答问题,但最好在此处包括答案的基本部分,并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会无效。
山姆·汉利

@sphanley,指出,如果我有更多时间,我可能会更新,因为这是答案发布的相当长的代码
Nikos M.

@sphanley,重现了引用链接的答案上的示例代码(尽管无论如何,我的thinbk大部分时间都会创建重复的内容)
Nikos M.

1
它肯定会创建重复的内容,但这就是重点-博客链接经常会随着时间而中断,并且通过在此处复制相关内容,可以确保该内容将对将来的读者可用和有用。答案看起来不错!
山姆·汉利

3

更改元素的值可能会触发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对象绑定。


3

绑定任何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);

3

这是一个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"

小提琴:这里


2

我已经看过一些使用onkeypress和onchange事件处理程序的基本javascript示例,用于将视图绑定到我们的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>

2
<!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>

2

将变量绑定到输入的简单方法(双向绑定)是直接在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());


在不使用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.

1

这是香草javascript中非常简单的两种方式的数据绑定。

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

<div id="name">

</div>


2
当然这只能与onkeyup事件一起使用吗?即,如果您进行了ajax请求,然后通过JavaScript更改了innerHTML,则此操作将无效
Zach Smith,

1

晚会晚了,特别是自从几个月前/几年前我写了2个与libs相关的文章后,我会在稍后提及它们,但看起来仍然与我相关。为了使它真正变短,我选择的技术是:

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

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

  • 处理不同的应用程序生命周期案例(首先是HTML,然后是JS,然后是JS,然后是HTML,然后更改动态属性,等等)
  • 允许模型的深度绑定,以便可以绑定 user.address.block
  • 阵列作为模型应当正确支持(shiftsplice和相似)
  • 处理ShadowDOM
  • 尝试尽可能容易地进行技术替换,因此,任何模板化子语言都是对未来不友好的方法,因为它与框架过于紧密地结合在一起

考虑到所有这些因素,我认为不可能仅抛出几十条JS行。我尝试将其作为模式而不是lib进行 -对我不起作用。

接下来,将Object.observe其删除,但是鉴于对模型的观察是至关重要的部分-整个部分必须分开关注另一个库。现在讲到我如何处理此问题的原理-完全按照OP的要求:

模型(JS部分)

我对模型观察的看法是Proxy,恕我直言,这是使其正常工作的唯一明智的方法。功能齐全的功能observer值得拥有它自己的库,因此我object-observer出于这个目的开发了库。

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

视图(HTML部分)

恕我直言,表达绑定的最简单方法是通过属性。许多人以前这样做过,许多人以后都会这样做,所以这里没有消息,这只是这样做的正确方法。就我而言,我使用了以下语法:<span data-tie="modelKey:path.to.data => targerProperty"></span>,但这并不重要。什么对我很重要,在HTML中没有复杂的脚本语法-这是错误的,再次,恕我直言。

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

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

细节列表的确可以走得更远,但是我认为这些原则是实现数据绑定的主要原则,一方面要实现功能完整,另一方面要保持理智简单。

因此,除了object-observer上面提到的以外,我确实还编写了data-tier库,该库根据上述概念实现了数据绑定。


0

在过去的7年中,情况发生了很大的变化,我们现在在大多数浏览器中都具有本机Web组件。IMO问题的核心是在元素之间共享状态,一旦状态发生变化,它就很容易更新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-属性
  • 没有Object.observe(不建议使用)
  • 没有代理(它提供了一个钩子,但始终没有通信机制)
  • 没有依赖关系(除了polyfill取决于目标浏览器)
  • 它是合理的集中式和模块化...用html描述状态,到处都有监听器会很快变得混乱。
  • 它是可扩展的。这个基本实现是20行代码,但是您可以轻松构建一些便利性,不变性和状态形状魔术,以使其更易于使用。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.