【问题标题】:How to Implement DOM Data Binding in JavaScript如何在 JavaScript 中实现 DOM 数据绑定
【发布时间】:2013-05-05 05:09:24
【问题描述】:

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

tl;博士

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

数据绑定到 DOM

通过数据绑定到 DOM 我的意思是,例如,有一个 JavaScript 对象 a 和一个属性 b。然后有一个<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 的忠实粉丝,所以我尝试将它用于模板。但是,我在尝试执行数据绑定时遇到了问题,因为 Mustache 将 HTML 处理为字符串,所以在我得到它的结果后,我没有参考我的视图模型中的对象在哪里。我能想到的唯一解决方法是使用属性修改 HTML 字符串(或创建的 DOM 树)本身。我不介意使用不同的模板引擎。

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

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

【问题讨论】:

  • 我基于 Benjamin Gruenbaum 的设计CrazyGlue。它还支持 SELECT、复选框和单选标签。 jQuery 是一个依赖项。
  • 这个问题太棒了。如果它因为跑题或其他一些愚蠢的废话而被关闭,我会被认真地勾掉。
  • @JohnSz 感谢您提及您的 CrazyGlue 项目。很长一段时间以来,我一直在寻找一种简单的 2 路数据绑定器。看起来你没有使用 Object.observe 所以你的浏览器支持应该很好。而且你没有使用 mustache 模板,所以它很完美。
  • @Benjamin 你最后做了什么?
  • 从 2021 年开始 -> 感谢您提出这个问题和讨论!@#

标签: javascript html dom data-binding


【解决方案1】:
  • 绑定如何对对象起作用?
  • 如何聆听表单的变化?

更新两个对象的抽象

我想还有其他技术,但最终我会有一个对象来保存对相关 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 属性。当您在 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);

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

【讨论】:

  • +1 非常干净的方法,非常简单,简单到可供人们学习,比我的方法干净得多。一个常见的用例是在代码中使用模板来表示对象的视图。我想知道这在这里如何工作?在像 Mustache 这样的引擎中,我做了一些事情 Mustache.render(template,object),假设我想保持一个对象与模板同步(不是特定于 Mustache),我该怎么做呢?
  • @BenjaminGruenbaum:我没有使用过客户端模板,但我想 Mustache 有一些用于识别插入点的语法,并且该语法包含一个标签。所以我认为模板的“静态”部分将呈现为存储在数组中的 HTML 块,而动态部分将在这些块之间移动。然后插入点上的标签将用作对象属性。然后,如果某个input 要更新其中一个点,则将存在从输入到该点的映射。我会看看我能不能想出一个简单的例子。
  • @BenjaminGruenbaum:嗯……我还没想过如何干净地协调两个不同的元素。这比我一开始想的要复杂一些。不过我很好奇,所以我可能需要稍后再处理这个问题。 :)
  • 您会看到有一个主要的Template 构造函数进行解析,保存不同的MyCtor 对象,并提供一个接口来通过其标识符更新每个对象。如果您有任何问题,请告诉我。 :) 编辑: ...使用this link instead...我忘记了每10秒输入值呈指数增长以演示JS更新。这限制了它。
  • ...fully commented version 加上一些小的改进。
【解决方案2】:

我想添加到我的 preposter 中。我建议一种稍微不同的方法,它允许您简单地为您的对象分配一个新值,而无需使用方法。必须注意的是,尤其是较旧的浏览器不支持此功能,并且 IE9 仍需要使用不同的界面。

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

Getter 和 Setter

我的提议利用了getters and setters 相对年轻的功能,特别是仅限于二传手。一般来说,mutators 允许我们“自定义”某些属性如何被赋值和检索的行为。

我将在这里使用的一个实现是Object.defineProperty 方法。它适用于 FireFox、GoogleChrome 和 - 我认为 - 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/

【讨论】:

  • 如果我们有 Harmony Proxy 对象就好了:) Setter 看起来确实是个好主意,但这不需要我们修改实际的对象吗?此外,附带说明 - Object.create 可以在这里使用(再次假设现代浏览器允许第二个参数)。此外,setter/getter 可用于向对象和 DOM 元素“投射”不同的值:)。我想知道您是否也对模板有任何见解,这似乎是一个真正的挑战,尤其是结构良好:)
  • 就像我的骗子一样,我也不经常使用客户端模板引擎,抱歉。 :( 但是,修改实际对象是什么意思?我想了解您对如何理解 setter/getter 可用于... 。这里的 getter/setter 仅用于将所有输入和检索从对象重定向到 DOM 元素,基本上就像 Proxy,就像你说的那样。;) 我理解要保持的挑战两个不同的属性同步。我的方法消除了两者之一。
  • A Proxy 将消除使用 getter/setter 的需要,您可以在不知道它们具有什么属性的情况下绑定元素。我的意思是,getter 可以更改的不仅仅是 bindTo.value,它们可以包含逻辑(甚至可能是一个模板)。问题是如何在考虑模板的情况下维护这种双向绑定?假设我将我的对象映射到一个表单,我想保持元素和表单同步,我想知道我将如何继续这种事情。例如,您可以查看它在淘汰赛learn.knockoutjs.com/#/?tutorial=intro 上的工作原理
  • @BenjaminGruenbaum 明白了。我去看看。
  • @BenjaminGruenbaum 我明白你想了解什么。考虑到模板来设置所有这些变得有点困难。我将在this 脚本上工作一段时间(并不断地对其进行变基)。但现在,我要休息了。我实际上并没有足够的时间。
【解决方案3】:

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

它的用途

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

  • MutationObservers 检测 dom 的变化(也使用事件监听器)
  • Object.observe 检测对象的变化并通知 dom。 危险,因为这个答案已经写成 O.o 已经被 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);

这是working fiddle。请注意,此解决方案非常通用。 Object.observe 和变异观察者匀场可用。

【讨论】:

【解决方案4】:

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

对于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 是否存在可比性问题?
  • 现在它需要一个 shim 或 polyfill 到 Object.observe,因为目前仅在 chrome 中提供支持。 caniuse.com/#feat=object-observe
  • Object.observe 已死。只是想我会在这里注意到这一点。
  • @BenjaminGruenbaum 现在什么是正确的,因为它已经死了?
  • @johnny 如果我没记错的话,那将是代理陷阱,因为它们可以更精细地控制我可以对对象做什么,但我必须对此进行调查。
【解决方案5】:

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

上一个链接以及来自 knockoutjs、backbone.js 和 agility.js 的想法导致this light-weight and fast MVVM framework, ModelView.js 基于 jQuery 与 jQuery 很好地配合,我是其中的谦虚(或者也许不那么谦虚)作者。

复制下面的示例代码(来自blog post link):

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 元素:

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

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

【讨论】:

  • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会失效。
  • @sphanley,请注意,我可能会在我有更多时间时更新,因为它是一个相当长的答案帖子代码
  • @sphanley,从引用的链接中复制了答案示例代码(尽管我认为这在大多数情况下都会创建重复内容)
  • 它确实会创建重复的内容,但这就是重点 - 博客链接经常会随着时间的推移而中断,通过在此处复制 相关 内容可以确保它可用并且对未来的读者有用。现在的答案看起来很棒!
【解决方案6】:

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

玩起来很有趣。

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

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

【讨论】:

    【解决方案7】:

    更改元素的值会触发DOM event。响应事件的侦听器可用于在 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;
      });
    }
    

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

    【讨论】:

      【解决方案8】:

      我已经完成了一些基本的 javascript 示例,使用 onkeypress 和 onchange 事件处理程序将视图绑定到我们的 js 和要查看的 js

      这里的例子 plunker 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>
      

      【讨论】:

        【解决方案9】:
        <!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>
        

        【讨论】:

          【解决方案10】:

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

          【讨论】:

            【解决方案11】:

            在 vanilla javascript 中非常简单的双向数据绑定....

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

            【讨论】:

            • 这肯定只适用于 onkeyup 事件吗?也就是说,如果你做了一个 ajax 请求,然后通过 JavaScript 更改了 innerHTML,那么这将不起作用
            【解决方案12】:

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

            【讨论】:

            • 我喜欢这种优雅的简单性,并认为 KISS 的吸引力使它成为一个有吸引力的解决方案 - 但它并不是真正的模型绑定,它是 DOM 值的包装器,对吧?
            • 是的,它是直接访问 DOM 元素,如果变量发生变化,它会反映 UI 的变化,反之亦然
            【解决方案13】:

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

            小提琴:Here

            【讨论】:

              【解决方案14】:

              晚会,特别是因为我在几个月/几年前编写了 2 个相关的库,我稍后会提到它们,但看起来仍然与我相关。为了使它真正简短的剧透,我选择的技术是:

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

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

              • 处理不同的应用生命周期案例(先是 HTML,然后是 JS,先是 JS,然后是 HTML,动态属性更改等)
              • 允许深度绑定模型,这样就可以绑定user.address.block
              • 应正确支持作为模型的数组(shiftsplice 等)
              • 处理 ShadowDOM
              • 尝试尽可能轻松地进行技术替换,因此任何模板子语言都是对未来更改不友好的方法,因为它与框架的耦合度太高

              考虑到所有这些,在我看来,仅仅抛出几十行 JS 是不可能的。我尝试将其作为 pattern 而不是 lib - 不适合我。

              接下来,删除了Object.observe,但鉴于模型的观察是至关重要的部分 - 这整个部分必须关注分离到另一个库。现在谈谈我如何解决这个问题的原则——正如 OP 所要求的那样:

              模型(JS部分)

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

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

              视图(HTML 部分)

              恕我直言,表达绑定的最简洁方式是通过属性。许多人以前这样做,许多人以后会这样做,所以这里没有消息,这只是一个正确的方法。就我而言,我使用了以下语法:&lt;span data-tie="modelKey:path.to.data =&gt; targerProperty"&gt;&lt;/span&gt;,但这并不重要。 对我来说很重要,HTML 中没有复杂的脚本语法 - 这是错误的,恕我直言。

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

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

              具体细节列表可能会更进一步,但在我看来,这些是主要原则,可以使数据绑定在一方面的功能完整性和另一方面的合理简单之间取得良好平衡。

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

              【讨论】:

                【解决方案15】:

                在过去的 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>

                fiddle here

                我喜欢这种方法,因为:

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

                【讨论】:

                • 好主意 + 很高兴阅读 2020 年的答案!。 (这是 2021 年)。1.您是否想过附加孩子而不是 innerHTML(可能是大树中的性能问题)? 2. 难道 RXJS 不是比创建自定义的 observable powers 更好的选择吗?
                • @EvaCohen 谢谢! 1. innerHTML 只是一个例子,当状态更新时,你可以做任何你想做的事情。 2. RxJS 在某些情况下肯定是合适的。这个问题是“如何实现”,所以回答“使用 x 库”似乎不合适。在实践中,当你像这样实现自己的组件时,事情会很快变得复杂,所以除了最简单的用途之外,最小的库适用于所有用途,我个人在一些个人项目中使用了 lit-element,它非常棒。跨度>
                猜你喜欢
                • 1970-01-01
                • 2023-03-11
                • 2010-09-28
                • 1970-01-01
                • 2023-04-01
                • 1970-01-01
                • 2022-01-26
                • 1970-01-01
                相关资源
                最近更新 更多