مفاهیم اولیه‌ی اتصال داده (data binding) در جاوااسکریپت

یکی از مهم‌ترین الگوهای توسعه‌ی اپلیکیشن‌های جاوااسکریپتی، جدا کردن داده‌ها و ظاهر اپلیکیشن از همدیگر و اتصال این دو به یکدیگر به شکل یک‌سویه یا دوسویه است. به طوری که با تغییر داده، ظاهر مرتبط با آن داده عوض و به شکل متقابل با تغییر در ظاهر برنامه (مانند پر کردن فرم یا تغییر مکان یک شی) داده مرتبط با آن دچار تغییر شود. امروزه اتصال داده در اپلیکیشن‌های تحت وب برای برنامه‌نویس از نان شب واجب‌تر است! البته فریم‌ورک‌های متداول مانند ReactJs، این اتصال داده را به خوبی برای برنامه‌نویسان پیش‌بینی کرده‌اند و در این مقاله قرار نیست فریمورک توسعه بدهیم. اما به دلایلی خوب است مفاهیم اولیه‌ی اتصال داده (data binding) در جاوااسکریپت را درک کنیم.

اول این‌که فهمیدن این موضوع که اصول اولیه‌ی اتصال داده از کجا آب می‌خورد جذاب است! در کنار این موضوع به تقویت دانش جاوااسکریپتی برنامه‌نویس نیز کمک می‌کند و برنامه‌نویس می‌تواند در مواقعی که کار در قد و قواره‌ی استفاده از فریمورک نیست اتصال داده را برای خود انجام داده و اصول MVC را پیاده‌سازی کند.

نموداری از اتصال داده دوسویه

اتصال داده‌ی یک‌سویه
(one-way data binding)

یکی از مهم‌ترین مشوق‌های استفاده از فریمورک‌ها، کمبودهایی است که در جاوااسکریپت وجود داشته است. فریمورک‌ها با مرتفع کردن این کمبود‌ها (که انصافا کم هم نبوده است) برنامه‌نویس را در عرصه‌ی توسعه‌ی آسان‌تر اپلیکیشن یاری داده‌اند. اما جاوااسکریپت از زمان پیدایش خود پیشرفت‌های زیادی داشته و بسیاری از کمبودها را پوشش داده است. بنابراین ممکن است بخواهیم در برخی موارد اپلیکیشن خود را با جاوااسکریپت خالص توسعه دهیم. در این صورت باید بتوانیم اتصال داده را پیاده‌سازی کنیم.

مفاهیم مربوط به اتصال داده ساده است. در یک سمت یک مدل داده‌ای را داریم و در سمت دیگر رابط کاربری (اینترفیس) را که معمولا اسمش را می‌گذاریم ویو (View). حال می‌خواهیم کاری کنیم زمانی که داده را تغییر می‌دهیم، بخشی از رابط کاربری که با آن داده در ارتباط است نیز تغییر کند و به روز شود. این مفهوم معمولا برای داده‌هایی مفید خواهند بود که صرفا قرار است خوانده شوند (نوشتن داده از طریق رابط کاربری نداریم). به این تکنیک اتصال داده‌ی یک‌سویه می‌گوییم.

راز ایجاد اتصال داده‌ی یک طرفه در getter و setter برای propertyها در جاوااسکریپت است. جاوااسکریپت از مدت‌های مدید Object.defineProperty داشته است. این تابع به برنامه‌نویس اجازه می‌دهد توابع دلخواه getter و setter برای propertyهای آبجکت‌های جاوااسکریپت تعریف کند. کد زیر نمونه‌ای از به کارگیری getter و setterها در اتصال داده یک‌طرفه است:

function Binding(b) {
    //element ke mikhahim be datat bind shavad:
    this.element = b.element   

    //data ke be shekle yek property az yek object ast: 
    this.value = b.object[b.property]
    
    //attribute az element ke mikhahim data be
    // aan bind shavad.
    this.attribute = b.attribute
    
    this.valueGetter = function(){
        return this.value;
    }.bind(this);

    this.valueSetter = function(val){
        // meqdaar-dehi be data;
        this.value = val
        // meqdaar-dehi be view;
        this.element[this.attribute] = val
    }.bind(this);
    
    //afzudane function-haye getter va setter be
    // property-ye object
    Object.defineProperty(b.object, b.property, {
        get: this.valueGetter,
        set: this.valueSetter
    }); 

    //meqdaar-dehiye avaliye be data;
    b.object[b.property] = this.value;

    //meqdaar-dehiye avaliye be view;
    this.element[this.attribute] = this.value
}

این کد یک رفرنس به آبجکت اولیه ایجاد کرده و property آبجکت رفرنس را طوری درست می‌کند که هنگام مقداردهی به آن property تابع setter فعال شود. در نتیجه هم View و هم data آپدیت می‌شوند. کد زیر نمونه‌ای از استفاده از تابع بالا است. اگر یک div با آیدی container داشته باشیم و بخواهیم پراپرتی html از آبجکت a را به innerHTML آن bind (متصل) کنیم می‌توانیم بنویسیم:

var a = {html: 'this is innerHTML of a div'}
Binding({
    element: document.getElementById('container'),
    attribute: 'innerHTML',
    object: a,
    property: 'html'
})

لذا با هر تغییر a.html، محتوای داخل div نیز تغییر خواهد کرد.

اتصال داده دو سویه
(two-way data binding)
مدل محدود:

اتصال داده (data binding) در جاوااسکریپت تنها به مدل یک‌سویه خلاصه نمی‌شود. با اضافه کردن یک Event Listener به DOM مورد نظر خود، می‌توانیم کد بالا رو طوری توسعه دهیم که اطلاعات به شکل دو سویه در آن جریان داشته باشد. به طوری که هر زمان آبجکت آپدیت شود، View تغییر کند و هر موقع View باعث فراخوانی Event Listener شود، محتویات آبجکت تغییر کند که تغییر محتویات آبجکت دوباره View را آپدیت خواهد کرد.

function Binding(b) {
    this.element = b.element
    this.value = b.object[b.property]
    this.attribute = b.attribute
    this.valueGetter = function(){
        return this.value;
    }.bind(this);
    this.valueSetter = function(val){
        this.value = val
        this.element[this.attribute] = val
    }.bind(this);

    //agar event set shode bashad:
    if(b.event){
        let _this = this;
        this.element.addEventListener(b.event, function(event){
            _this.value = _this.element[_this.attribute]
        });        
    }

    Object.defineProperty(b.object, b.property, {
        get: this.valueGetter,
        set: this.valueSetter
    }); 
    b.object[b.property] = this.value;

    this.element[this.attribute] = this.value
}

حال اگر یک input از نوع text و با آی‌دی text داشته باشیم، می‌توانیم این اتصال دو سویه را با کد زیر تحقیق کنیم:

var a = {value: 'i am here'}
// baraye moshahedeye taqirate 'a':
setInterval(()=>{console.log(a.value)}, 1000);
Binding({
    element: document.getElementById('text'),
    attribute: 'value',
    event: 'keyup',
    object: a,
    property: 'value'
})

اما کدی که تابحال توسعه داده‌ایم تا حدودی محدود است. به طوری که تنها یک attribute از یک المان را به یک property از یک آبجکت اتصال می‌دهد. بیایید اندکی روی موضوع کار کنیم.

اتصال داده دو سویه
(two-way data binding)
مدل بهتر:

مدل بهتر به این شکل است که هر داده باید بتواند به المان‌های DOM متعددی متصل شود. بنابراین هر تغییر در داده همه‌ی آن المان‌ها را تغییر دهد؛ چه این تغییر در اثر فعال شدن یک Event از سمت یکی از این المان‌ها باشد یا نه.

function Binding(b) {
    this.elementBindings = []
    this.value = b.object[b.property]
    this.valueGetter = function(){
        return _this.value;
    }
    this.valueSetter = function(val){
        this.value = val;
        for (var i = 0; i < this.elementBindings.length; i++) {
            var binding = this.elementBindings[i];
            binding.element[binding.attribute] = val
        }
    }.bind(this);
    this.addBinding = function(element, attribute, event){
        var binding = {
            element: element,
            attribute: attribute
        }
        if (event){
            let _this = this;
            element.addEventListener(event, function(event){
                _this.valueSetter(element[attribute]);
            })
            binding.event = event
        }       
        this.elementBindings.push(binding)
        element[attribute] = this.value
        return this
    }.bind(this);

    Object.defineProperty(b.object, b.property, {
        get: this.valueGetter,
        set: this.valueSetter
    }); 

    b.object[b.property] = this.value;
}

در کد بالا آرایه‌ی elementBindings را داریم که با هربار فراخوانی تابع addBinding و عبور دادن آرگومان‌های attribute ،element و event، این سه مقدار را به آن آرایه اضافه می‌کند. بعدها در setter آبجکت، مقادیر همه‌ی اعضای آرایه به همراه مقدار خود آبجکت به روز می‌شوند. با این روال، تنها کافیست در Event Listener هر DOM تابع setter را فراخوانی کنیم و همچنین برای آبجکت، setter و getter تعریف کنیم تا هنگام تغییر مقدار آبجکت از جای دیگر، setter فراخوانی و اجرا شود. با کد نمونه زیر می‌توانیم از تابع بالا استفاده کنیم:

var obj = {a:123}
var myInputElement1 = document.getElementById("myText1")
var myInputElement2 = document.getElementById("myText2")
var myDOMElement = document.getElementById("myDomElement")

new Binding({
	object: obj,
	property: "a"
})
.addBinding(myInputElement1, "value", "keyup")
.addBinding(myInputElement2, "value", "keyup")
.addBinding(myDOMElement, "innerHTML")

obj.a = 456;

در کد بالا دو input با آی‌دی myText1 و myText2 و یک span با آی‌دی myDomElement تعریف کرده‌ایم. هر سه این المان‌ها با تغییر داده obj.a چه از طرف خود المان‌ها یا از جای دیگر، به مقدار جدید آپدیت خواهند شد.

این کد کار را بهتر کرد. هرچند که کارهای خیلی زیاد دیگری هنوز باقی‌ست. مثلا هنوز هیچ صحت‌سنجی برای داده وجود ندارد، آرایه پشتیبانی نمی‌شود، Template نمی‌توان ساخت و هزاران مورد دیگر که اگر ایجاد می‌شد نطفه‌ی فریمورک جدیدی بسته می‌شد. هرچند که فرمورک‌های زیادی وجود دارند و نیازی به توسعه مورد جدیدی نیست اما همین کدی که با هم بررسی کردیم نیز برای بسیاری از کارهای ساده، کار ما را راه خواهد انداخت.

منبع : How To Do Data Binding in Pure JavaScript

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *