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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16483560/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-29 08:17:02  来源:igfitidea点击:

How to Implement DOM Data Binding in JavaScript

javascripthtmldomdata-binding

提问by Benjamin Gruenbaum

Please treat this question as strictly educational. I'm still interested in hearing new answers and ideas to implement this

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

tl;dr

tl;博士

How would I implement bi-directional data-binding with JavaScript?

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

Data Binding to the DOM

数据绑定到 DOM

By data binding to the DOM I mean for example, having a JavaScript object awith a property b. Then having an <input>DOM element (for example), when the DOM element changes, achanges and vice versa (that is, I mean bidirectional data binding).

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

Here is a diagram from AngularJS on what this looks like:

这是来自 AngularJS 的图表,展示了它的样子:

two way data binding

双向数据绑定

So basically I have JavaScript similar to:

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

var a = {b:3};

Then an input (or other form) element like:

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

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

I'd like the input's value to be a.b's value (for example), and when the input text changes, I'd like a.bto change too. When a.bchanges in JavaScript, the input changes.

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

The Question

问题

What are some basic techniques to accomplish this in plain JavaScript?

在纯 JavaScript 中完成此操作的一些基本技术是什么?

In specific, I'd like a good answer to refer to:

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

  • How would binding work for objects?
  • How listening to change in the form might work?
  • Is it possible in a simple way to only have the HTML modified on the template level? I'd like to not keep track of the binding in the HTML document itself but only in JavaScript (with DOM events, and JavaScript keeping reference to the DOM elements used).
  • 绑定如何为对象工作?
  • 如何倾听形式的变化可能会起作用?
  • 是否有可能以简单的方式只在模板级别修改 HTML?我不想跟踪 HTML 文档本身中的绑定,而只跟踪 JavaScript(使用 DOM 事件,并且 JavaScript 保持对所用 DOM 元素的引用)。

What have I tried?

我尝试了什么?

I'm a big fan of Mustache so I tried using it for templating. However, I ran into issues when trying to perform the data binding itself since Mustache processes HTML as a string so after I get its result I have no reference to where the objects in my viewmodel are. The only workaround I could think for this was modifying the HTML string (or created DOM tree) itself with attributes. I don't mind using a different templating engine.

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

Basically, I got a strong feeling that I was complicating the issue at hand and there is a simple solution.

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

Note:Please do not provide answers that use external libraries, especially ones that are thousands of lines of code. I've used (and like!) AngularJS and KnockoutJS. I really don't want answers in the form 'use framework x'. Optimally, I'd like a future reader who doesn't know how to use many frameworks to grasp how to implement bi-directional data-binding herself. I do not expect a completeanswer, but one that gets the idea across.

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

回答by Benjamin Gruenbaum

  • How would binding work for objects?
  • How listening to change in the form might work?
  • 绑定如何为对象工作?
  • 如何倾听形式的变化可能会起作用?

An abstraction that updates both objects

更新两个对象的抽象

I suppose there are other techniques, but ultimately I'd have an object that holds reference to a related DOM element, and provides an interface that coordinates updates to its own data and its related element.

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

The .addEventListener()provides a very nice interface for this. You can give it an object that implements the eventListenerinterface, and it'll invoke its handlers with that object as the thisvalue.

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

This gives you automatic access to both the element and its related data.

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

Defining your object

定义你的对象

Prototypal inheritance is a nice way to implement this, though not required of course. First you'd create a constructor that receives your element and some initial data.

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

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

So here the constructor stores the element and data on properties of the new object. It also binds a changeevent to the given element. The interesting thing is that it passes the new object instead of a function as the second argument. But this alone won't work.

所以在这里构造函数存储新对象的属性的元素和数据。它还将change事件绑定到给定的element. 有趣的是,它传递了新对象而不是函数作为第二个参数。但仅凭这一点是行不通的。

Implementing the eventListenerinterface

实现eventListener接口

To make this work, your object needs to implement the eventListenerinterface. All that's needed to accomplish this is to give the object a handleEvent()method.

为了使这个工作,你的对象需要实现eventListener接口。完成此操作所需要做的就是为对象提供一个handleEvent()方法。

That's where the inheritance comes in.

这就是继承的用武之地。

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;
};

There are many different ways in which this could be structured, but for your example of coordinating updates, I decided to make the change()method only accept a value, and have the handleEventpass that value instead of the event object. This way the change()can be invoked without an event as well.

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

So now, when the changeevent happens, it'll update both the element and the .dataproperty. And the same will happen when you call .change()in your JavaScript program.

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

Using the code

使用代码

Now you'd just create the new object, and let it perform updates. Updates in JS code will appear on the input, and change events on the input will be visible to the JS code.

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

DEMO:http://jsfiddle.net/RkTMD/

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

回答by Benjamin Gruenbaum

So, I decided to throw my own solution in the pot. Here is a working fiddle. Note this only runs on very modern browsers.

所以,我决定把我自己的解决方案扔进锅里。这是一个工作小提琴。请注意,这仅在非常现代的浏览器上运行。

What it uses

它使用什么

This implementation is very modern - it requires a (very) modern browser and users two new technologies:

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

  • MutationObserversto detect changes in the dom (event listeners are used as well)
  • Object.observeto detect changes in the object and notifying the dom. Danger, since this answer has been written O.o has been discussed and decided against by the ECMAScript TC, consider a polyfill.
  • MutationObservers检测 dom 中的变化(也使用事件侦听器)
  • Object.observe检测对象的变化并通知dom。危险,由于已编写此答案 Oo 已被 ECMAScript TC 讨论并决定反对,请考虑使用 polyfill

How it works

这个怎么运作

  • On the element, put a domAttribute:objAttributemapping - for example bind='textContent:name'
  • Read that in the dataBind function. Observe changes to both the element and the object.
  • When a change occurs - update the relevant element.
  • 在元素上,放置一个domAttribute:objAttribute映射 - 例如bind='textContent:name'
  • 在 dataBind 函数中读取它。观察元素和对象的变化。
  • 当发生变化时 - 更新相关元素。

The solution

解决方案

Here is the dataBindfunction, note it's just 20 lines of code and could be shorter:

这是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]; 
}

Here is some usage:

下面是一些用法:

HTML:

HTML:

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

JavaScript:

JavaScript:

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

Here is a working fiddle. Note that this solution is pretty generic. Object.observe and mutation observer shimming is available.

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

回答by Kiruse

I'd like to add to my preposter. I suggest a slightly different approach that will allow you to simply assign a new value to your object without using a method. It must be noted though that this is not supported by especially older browsers and IE9 still requires use of a different interface.

我想添加到我的推荐者中。我建议一种稍微不同的方法,它允许您在不使用方法的情况下简单地为您的对象分配一个新值。但必须注意的是,特别旧的浏览器不支持这一点,IE9 仍然需要使用不同的界面。

Most notably is that my approach does not make use of events.

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

Getters and Setters

吸气剂和吸气剂

My proposal makes use of the relatively young feature of getters and setters, particularly setters only. Generally speaking, mutators allow us to "customize" the behavior of how certain properties are assigned a value and retrieved.

我的提议利用了getter 和 setter相对年轻的特性,尤其是仅使用 setter。一般来说,mutators 允许我们“自定义”某些属性如何分配值和检索的行为。

One implementation I'll be using here is the Object.definePropertymethod. It works in FireFox, GoogleChrome and - I think - IE9. Haven't tested other browsers, but since this is theory only...

我将在这里使用的一种实现是Object.defineProperty方法。它适用于 FireFox、GoogleChrome 和 - 我认为 - IE9。尚未测试其他浏览器,但由于这只是理论......

Anyways, it accepts three parameters. The first parameter being the object that you wish to define a new property for, the second a string resembling the the name of the new property and the last a "descriptor object" providing information on the behavior of the new property.

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

Two particularly interesting descriptors are getand set. An example would look something like the following. Note that using these two prohibits the use of the other 4 descriptors.

两个特别有趣的描述符是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;
        }
    });
}

Now making use of this becomes slightly different:

现在使用它变得略有不同:

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

I want to emphasize that this only works for modern browsers.

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

Working fiddle: http://jsfiddle.net/Derija93/RkTMD/1/

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

回答by madcampos

I think my answer will be more technical, but not different as the others present the same thing using different techniques.
So, first things first, the solution to this problem is the use of a design pattern known as "observer", it let's you decouple your data from your presentation, making the change in one thing be broadcasted to their listeners, but in this case it's made two-way.

我认为我的答案会更具技术性,但不会因为其他人使用不同的技术呈现相同的东西而有所不同。
所以,首先,解决这个问题的方法是使用一种称为“观察者”的设计模式,它让你将数据与演示文稿分离,将一件事的变化广播给他们的听众,但在这种情况下它是双向的。

For the DOM to JS way

对于 DOM 转 JS 的方式

To bind the data from the DOM to the js object you may add markup in the form of dataattributes (or classes if you need compatibility), like this:

要将来自 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=""/>

This way it can be accessed via js using querySelectorAll(or the old friend getElementsByClassNamefor compatibility).

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

Now you can bind the event listening to the changes in to ways: one listener per object or one big listener to the container/document. Binding to the document/container will trigger the event for every change made in it or it's child, it willhave a smaller memory footprint but will spawn event calls.
The code will look something like this:

现在,您可以将侦听更改的事件绑定到以下方式:每个对象一个侦听器或一个大侦听器到容器/文档。绑定到文档/容器将针对其中或其子项所做的每次更改触发事件,它将具有较小的内存占用,但会产生事件调用。
代码如下所示:

//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);

For the JS do DOM way

对于JS做DOM的方式

You will need two things: one meta-object that will hold the references of witch DOM element is binded to each js object/attribute and a way to listen to changes in objects. It is basically the same way: you have to have a way to listen to changes in the object and then bind it to the DOM node, as your object "can't have" metadata you will need another object that holds metadata in a way that the property name maps to the metadata object's properties. The code will be something like this:

您将需要两件事:一个将保存女巫 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);

I hope that i was of help.

我希望我有帮助。

回答by ton

Yesterday, I started to write my own way to bind data.

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

It's very funny to play with it.

玩它很有趣。

I think it's beautiful and very useful. At least on my tests using firefox and chrome, Edge must works too. Not sure about others, but if they support Proxy, I think it will work.

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

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

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' />

Here is the code:

这是代码:

(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);
                }
            }
        }
    }
})();

Then, to set, just:

然后,要设置,只需:

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'
})

For now, I've just added the HTMLInputElement value bind.

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

Let me know if you know how to improve it.

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

回答by Nikos M.

There is a very simple barebones implementation of 2-way data-binding in this link "Easy Two-Way Data Binding in JavaScript"

在这个链接“Easy Two-Way Data Binding in JavaScript”中有一个非常简单的2路数据绑定的准系统实现

The previous link along with ideas from knockoutjs, backbone.js and agility.js, led to this light-weight and fast MVVM framework, ModelView.jsbased on jQuerywhich plays nicely with jQuery and of which i am the humble (or maybe not so humble) author.

前面的链接以及来自 Knockoutjs、backbone.js 和 agility.js 的想法,导致了这个轻量级和快速的 MVVM 框架 ModelView.js基于jQuery它与 jQuery 配合得很好,我是其中一位谦逊(或可能不那么谦逊)的作者。

Reproducing sample code below (from blog post link):

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

Sample code for DataBinder

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;
}

For what concerns the JavaScript object, a minimal implementation of a User model for the sake of this experiment could be the following:

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

Now, whenever we want to bind a model's property to a piece of UI we just have to set an appropriate data attribute on the corresponding HTML element:

现在,每当我们想要将模型的属性绑定到一个 UI 片段时,我们只需要在相应的 HTML 元素上设置适当的数据属性:

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

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

回答by amusingmaker

Changing an element's value can trigger a DOM event. Listeners that respond to events can be used to implement data binding in JavaScript.

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

For example:

例如:

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;
  });
}

Hereis code and a demo that shows how DOM elements can be bound with each other or with a JavaScript object.

下面的代码和演示展示了 DOM 元素如何相互绑定或与 JavaScript 对象绑定。

回答by Ollie Williams

Bind any html input

绑定任何 html 输入

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

define two functions:

定义两个函数:

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);
}

use the functions:

使用函数:

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

回答by Thornkey

Here's an idea using Object.definePropertywhich directly modifies the way a property is accessed.

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

Code:

代码:

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

Usage:

用法:

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

p.variable="yes"

fiddle: Here

小提琴:这里

回答by A-Sharabiani

A simple way of binding a variable to an input (two-way binding) is to just directly access the input element in the getter and setter:

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

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

In HTML:

在 HTML 中:

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

And to use:

并使用:

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;                                                  
                }

        }

To use:

使用:

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