[javascript] JavaScript에서 DOM 데이터 바인딩을 구현하는 방법

이 질문을 엄격하게 교육적인 것으로 취급하십시오. 나는 아직도 이것을 구현하기위한 새로운 답변과 아이디어를 듣는 데 관심이 있습니다.

tl; dr

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.b자바 스크립트의 변화, 입력이 변경됩니다.

질문

평범한 JavaScript에서 이것을 달성하는 몇 가지 기본 기술은 무엇입니까?

특히, 나는 다음을 언급하는 좋은 대답을 원합니다.

  • 객체에 대한 바인딩은 어떻게 작동합니까?
  • 양식의 변화를 듣는 것이 어떻게 효과가 있을까요?
  • 템플릿 수준에서 HTML 만 수정하는 것이 간단한 방법으로 가능합니까? HTML 문서 자체의 바인딩을 추적하지 않고 JavaScript (DOM 이벤트 및 JavaScript가 사용되는 DOM 요소 참조)에서만 바인딩을 추적하고 싶습니다.

내가 무엇을 시도 했습니까?

나는 Mustache의 열렬한 팬이므로 템플릿 사용에 사용해 보았습니다. 그러나 Mustache가 HTML을 문자열로 처리하기 때문에 데이터 바인딩 자체를 수행하려고 할 때 문제가 발생하여 결과를 얻은 후 뷰 모델의 객체 위치에 대한 참조가 없습니다. 내가 생각할 수있는 유일한 해결 방법은 HTML 문자열 (또는 생성 된 DOM 트리) 자체를 속성으로 수정하는 것입니다. 다른 템플릿 엔진을 사용하는 것은 마음에 들지 않습니다.

기본적으로 나는 당면한 문제를 복잡하게 만들고 있다는 간단한 느낌을 받았으며 간단한 해결책이 있습니다.

참고 : 외부 라이브러리, 특히 수천 줄의 코드를 사용하는 답변은 제공하지 마십시오. AngularJS와 KnockoutJS를 사용했습니다. ‘use framework x’형식의 답변을 정말로 원하지 않습니다. 최적의 방법으로, 양방향 데이터 바인딩 자체를 구현하는 방법을 파악하기 위해 많은 프레임 워크를 사용하는 방법을 모르는 미래의 독자가 필요합니다. 나는 완전한 대답을 기대하지는 않지만 아이디어를 얻습니다.



답변

  • 객체에 대한 바인딩은 어떻게 작동합니까?
  • 양식의 변화를 듣는 것이 어떻게 효과가 있을까요?

두 객체를 모두 업데이트하는 추상화

다른 기술이 있다고 가정하지만 궁극적으로 관련 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/


답변

그래서 나는 냄비에 내 자신의 솔루션을 던지기로 결정했습니다. 여기에 작동하는 바이올린이 있습니다. 이것은 최신 브라우저에서만 실행됩니다.

그것이 사용하는 것

이 구현은 매우 현대적입니다. (매우) 최신 브라우저가 필요하며 사용자에게는 두 가지 새로운 기술이 필요합니다.

  • MutationObserver이야 는 DOM의 변화 (이벤트 리스너가 아니라 사용되는) 검출
  • Object.observe객체의 변화를 감지하고 돔에 알리는 것입니다. 위험은이 답변이 작성되었으므로 ECMAScript TC가 Oo에 대해 논의하고 결정 했으므로 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' />

자바 스크립트 :

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

여기에 작동하는 바이올린이 있습니다. 이 솔루션은 매우 일반적입니다. Object.observe 및 mutation 관찰자 shimming을 사용할 수 있습니다.


답변

프리 포스터에 추가하고 싶습니다. 메소드를 사용하지 않고 단순히 객체에 새로운 값을 할당 할 수있는 약간 다른 접근법을 제안합니다. 이것은 특히 오래된 브라우저에서 지원되지 않으며 IE9는 여전히 다른 인터페이스를 사용해야합니다.

가장 주목할만한 것은 내 접근 방식이 이벤트를 사용하지 않는다는 것입니다.

게터와 세터

내 제안은 게터와 세터 , 특히 세터 의 비교적 젊은 기능을 사용 합니다. 일반적으로 뮤 테이터를 사용하면 특정 속성에 값이 할당되고 검색되는 방식의 동작을 “사용자 정의”할 수 있습니다.

여기서 사용할 하나의 구현은 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/


답변

내 대답은 더 기술적이지만 다른 사람들은 다른 기술을 사용하여 동일한 것을 제시하므로 다르지 않습니다.
먼저,이 문제에 대한 해결책은 “관찰자”라는 디자인 패턴을 사용하는 것입니다. 프리젠 테이션에서 데이터를 분리하여 한 가지 변경 사항을 청취자에게 브로드 캐스트 할 수 있습니다. 양방향으로 만들어졌습니다.

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

도움이 되었기를 바랍니다.


답변

어제 데이터 바인딩 방법을 직접 작성하기 시작했습니다.

그것을 가지고 노는 것은 매우 재미있다.

나는 그것이 아름답고 매우 유용하다고 생각합니다. 적어도 파이어 폭스와 크롬을 사용한 테스트에서 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 값 바인딩을 추가했습니다.

개선 방법을 알고 있다면 알려주십시오.


답변

이 링크 “자바 스크립트에서 쉬운 양방향 데이터 바인딩” 에 2-way 데이터 바인딩의 매우 간단한 베어 본 구현이 있습니다.

knockoutjs, backbone.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 요소에 적절한 데이터 속성을 설정해야합니다.

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

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


답변

요소의 값을 변경하면 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 객체와 바인딩하는 방법을 보여주는 코드와 데모입니다.