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

IT技术 javascript html dom data-binding
2021-01-27 11:41:40

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

tl;博士

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

数据绑定到 DOM

通过数据绑定到 DOM,我的意思是,例如,拥有一个a带有属性的 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”形式的答案。最理想的是,我希望未来的读者不知道如何使用许多框架来掌握如何自己实现双向数据绑定。我不希望得到一个完整的答案,而是一个能够理解这个想法的答案。

6个回答
  • 绑定如何为对象工作?
  • 如何倾听形式的变化可能会起作用?

更新两个对象的抽象

我想还有其他技术,但最终我会有一个对象来保存对相关 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/

@BenjaminGruenbaum:我没有使用客户端模板,但我认为 Mustache 有一些用于识别插入点的语法,并且该语法包括一个标签。所以我认为模板的“静态”部分将被渲染成存储在数组中的 HTML 块,而动态部分将在这些块之间移动。然后插入点上的标签将用作对象属性。然后,如果有人input要更新这些点之一,则会有从输入到该点的映射。我会看看我是否能想出一个简单的例子。
2021-03-21 11:41:40
+1 非常干净的方法,非常简单,足够简单,可供人们学习,比我所拥有的要干净得多。一个常见的用例是在代码中使用模板来表示对象的视图。我想知道这在这里如何工作?在像 Mustache 这样的引擎中,我做了一些事情Mustache.render(template,object),假设我想保持一个对象与模板同步(不是特定于 Mustache),我将如何继续呢?
2021-03-23 11:41:40
...完全评论的版本加上小的改进。
2021-03-28 11:41:40
您将看到有一个主Template构造函数执行解析、保存不同的MyCtor对象,并提供一个接口以通过其标识符更新每个对象。如果您有任何疑问,请告诉我。:)编辑: ...使用此链接代替...我忘记了每 10 秒输入值呈指数增长以演示 JS 更新。这限制了它。
2021-04-08 11:41:40
@BenjaminGruenbaum:嗯......我还没有想过如何干净地协调两个不同的元素。这比我最初想象的要复杂一些。不过我很好奇,所以我可能需要稍后处理这个。:)
2021-04-09 11:41:40

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

它使用什么

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

  • MutationObservers检测 dom 中的变化(也使用事件侦听器)
  • Object.observe检测对象的变化并通知dom。危险,因为这个答案已经写好了 Oo 已经被 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 和突变观察者匀场。

我碰巧写这个 (es5) 是为了好玩,如果有人觉得它有用的话 - 把自己打倒jsfiddle.net/P9rMm
2021-03-17 11:41:40
可以使用代理代替 Object.observe,或github.com/anywhichway/proxy-observegist.github.com/ebidel/1b553d571f924da2da06或较旧的polyfill,也在 github @JvdBerg
2021-03-18 11:41:40
请记住,当obj.name有一个 setter 时,它不能在外部观察到,但必须从 setter 内部广播它已经改变 - html5rocks.com/en/tutorials/es7/observe/#toc-notifications - 有点在工作中抛出一个扳手对于 Oo() 如果您想要使用 setter 进行更复杂的、相互依赖的行为。此外,当obj.name不可配置时,也不允许重新定义它的设置器(使用各种技巧添加通知) - 因此在这种特定情况下完全废弃带有 Oo() 的泛型。
2021-04-03 11:41:40
Object.observe 已从所有浏览器中删除:caniuse.com/#feat=object-observe
2021-04-05 11:41:40

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

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

吸气剂和吸气剂

我的建议利用了getter 和 setter的相对年轻的特性,尤其是仅使用 setter。一般来说,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/

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

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

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

我希望我有帮助。

Object.observe 已经死了。只是想我会注意到这里。
2021-03-14 11:41:40
@johnny 如果我没有错的话,这将是代理陷阱,因为它们允许更精细地控制我可以对对象做什么,但我必须对此进行调查。
2021-03-26 11:41:40
@BenjaminGruenbaum 现在使用什么是正确的,因为它已经死了?
2021-04-03 11:41:40
使用 .observer 没有可比性问题吗?
2021-04-11 11:41:40
现在它需要一个 shim 或 polyfill,Object.observe因为目前仅在 chrome 中提供支持。caniuse.com/#feat=object-observe
2021-04-12 11:41:40

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

玩它很有趣。

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

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