如何實現VM框架中的數據綁定

做者:佳傑

本文原創,轉載請註明做者及出處javascript

如何實現VM框架中的數據綁定

一:數據綁定概述

視圖(view)和數據(model)之間的綁定

二:數據綁定目的

不用手動調用方法渲染視圖,提升開發效率;統一處理數據,便於維護

三:數據綁定中的元素

視圖(view):說白了就是html中dom元素的展現
數據(model):用於保存數據的引用類型

四:數據綁定分類

view > model的數據綁定:view改變,致使model改變
model > view的數據綁定:model改變,致使view改變

五:數據綁定實現方法

view > model的數據綁定實現方法
        修改dom元素(input,textarea,select)的數據,致使model產生變化,
        只要給dom元素綁定change事件,觸發事件的時候修改model便可,不細講

model > view的數據綁定實現方法
        1.發佈訂閱模式(backbone.js用到);
        2.數據劫持(vue.js用到);
        3.髒值檢查(angular.js用到);

六:model > view數據綁定demo講解 (如何實現數據改變,致使UI界面從新渲染)

簡易思路 
> 1.經過defineProperty來監控model中的全部屬性(對每個屬性都監控)
> 2.編譯template生成DOM樹,同時綁定dom節點和model(例如<div id="{{model.name}}"></div>),
    defineProperty中已經給「model.name」綁定了對應的function,
    一旦model.name改變,該funciton就操做上面這個dom節點,改變view


主要js模塊:Observer,Compile,ViewModel

    1.Observer
        用到了發佈訂閱模式和數據監控,defineProperty用於「監控model", dom元素執行"訂閱"操做,給model中
        的屬性綁定function;model中屬性變化的時候,執行"發佈"這個操做,執行以前綁定的那個function

      源碼以下:
    var Observer = function(opts) {
        this.id = (opts && opts.id) ? opts.id : +new Date();
        this.opts = opts;
        this.subs = []; //觀察者數組
        /*this.subs包含了全部觀察者,每一個觀察者的結構以下:
        {
            key:"person.age.range",//這個key表明model.person.age.range這個屬性

            /*
             和key綁定的函數數組,每一個函數操做一個dom節點,
             一個key對應多個dom節點,因此actionList是個function數組;
             */
            actionList:[function(){},function(){}]
        }*/
    }
    Observer.prototype = {

        //遍歷model中全部的屬性,每一個屬性用defineKey來監控全部屬性
        monit: function(data, baseUrl) {
            var me = this;
            baseUrl = baseUrl || "";
            var isTypeMatch = (data && typeof data === "object");
            if (isTypeMatch) {
                Object.keys(data).forEach(function(key) {
                    var base = baseUrl ? (baseUrl + "." + key) : key;
                    me.defineKey(data, key, data[key], baseUrl); //定義本身
                    me.monit(data[key], base); //遞歸【定義的是下一層】
                });
            }
        },

        //用到了Object.defineProperty來定義屬性,這樣屬性改變的時候,就會自動執行裏面的set方法
        defineKey: function(data, key, val, baseUrl) {
            var me = this;
            var base = baseUrl ? (baseUrl + "." + key) : key;

            Object.defineProperty(data, key, {
                enumerable: true,
                configurable: false,
                get: function() {
                    return val;
                },

                //更新並監控新的值,執行publish函數
                set: function(newVal) {
                    if (newVal !== val) {
                        val = newVal;

                        //設置新值須要從新監控
                        me.monit(newVal, base); 

                        //(baseUrl+"."+key)做爲觀察者模式中的監聽的那個key,也能夠說是監聽的那個事件
                        me.publish(base, newVal); 
                    }
                }
            });
        },

        /*
         根據key來執行綁定在這個key上的全部函數,好比說person.age.range這個key,
         它變更的時候,publish會執行綁定在person.age.range這個key上全部的function
         */
        publish: function(key, newVal) {
            (this.subs || []).forEach(function(sub) {
                if (sub.key == key) {
                    (sub.actionList || []).forEach(function(action) {
                        action(newVal);
                    });
                }
            });
        },

        //給model中的某個key(例如person.age.range)添加綁定的function 
        subscribe: function(key, callback) {
            var tgIdx;
            var hasExist = this.subs.some(function(unit, idx) {
                tgIdx = (unit.key === key) ? idx : -1;
                return (unit.key === key)
            });
            if (hasExist) {
                if (Object.prototype.toString.call(this.subs[tgIdx].actionList)=="[object Array]"){
                    this.subs[tgIdx].actionList.push(callback);
                } else {
                    this.subs[tgIdx].actionList = [callback];
                }
            } else {
                this.subs.push({
                    key: key,
                    actionList: [callback]
                });
            }
        },

        //取消訂閱
        remove: function(key) {
            var removeIdx;
            this.subs.forEach(function(sub, idx) {
                removeIdx = sub.key === key ? idx : -1;
                return sub.key === key
            });
            if (removeIdx !== -1) {
                this.subs.splice(removeIdx, 1);
            }
        },

        isObject: function(data) {
            return data && typeof data === "object"
        }
    };



    2.Compile: 模板編譯器
    var Compile = function(opts) {
        this.opts = opts;
        this.data = this.opts.data;
        this.observer = this.opts.observer;
        this.regExp = /\{\{([\s\S]*)\}\}/;
        this.ele = document.createElement("div");
        this.ele.innerHTML = opts.template; //渲染頁面
        this.fragment = this.transToFrament(this.ele);
        this.travelAllNodes(this.fragment);
        this.ele.appendChild(this.fragment);
    };
    Compile.prototype = {

        //把頁面上的dom節點轉化成文檔碎片,防止dom頻繁操做影響頁面性能
        transToFrament: function(el) {
            var fragment = document.createDocumentFragment(),
                child;
            // 將原生節點拷貝到fragment
            while (child = el.firstChild) {
                fragment.appendChild(child);
            }
            return fragment;
        },

        //遍歷文檔碎片節點下全部的node節點(用到了函數遞歸調用),執行compileNode
        travelAllNodes: function(ele) {
            this.compileNode(ele);
            ([].slice.call(ele.childNodes) || []).forEach(function(node) {
                this.compileNode(node);
                if (node.childNodes && node.childNodes.length) {
                    this.travelAllNodes(node);
                }
            }.bind(this));
        },

        /*包含功能
         1.渲染node節點
         2.給key設置callback函數,函數內操做node節點
         */
        compileNode: function(node) {
            if (this.isElement(node)) {
                this.compileElementNode(node);
            } else if (this.isText(node)) {
                this.compileTextNode(node);
            }
        },

        /*
          編譯element類型的node節點,
          須要處理屬性綁定v-bind="{{data.name}}"和
          事件v-event="{{data.event}}"
         */
        compileElementNode: function(node) {
            var me = this,
                nodeAttrs = node.attributes;
            [].slice.call(nodeAttrs).forEach(function(attr) {
                var attrName = attr.name;
                var attrValue = attr.value;
                var key = me.getKey(attrValue);
                me.bindKeyToNode(key, attr);
                attr.value = me.compileString(attrValue); //渲染node
            });
        },

        //編譯文本類型的node節點,裏面放了對應的"{{data.name}}"這種數據格式
        compileTextNode: function(ele) {
            var key = this.getKey(ele.textContent);
            this.bindKeyToNode(key, ele);
            ele.textContent = this.compileString(ele.textContent);
        },

        //解析「{{}}」,把它變成對應的數據值
        compileString: function(str) {
            var key = this.getKey(str);
            return str.replace(this.regExp, this.getValueByKey(key));
        },

        //綁定key和node節點,key一旦改變,就會觸發對應的函數,修改node節點
        bindKeyToNode: function(key, node) {
            if (!!key.trim()) {
                console.log(key);
                var nodeType = node.nodeType;
                var regExp = new RegExp("\\{\\{" + key + "\\}\\}");
                var originTextConetnt;
                if (nodeType === 2) {
                    originTextConetnt = node.value;
                } else if (nodeType === 3) {
                    originTextConetnt = node.textContent;
                }

                this.observer.subscribe(key, function(newVal) {
                    var tgValue = originTextConetnt.replace(regExp, newVal);
                    if (nodeType === 2) {
                        node.value = tgValue;
                    } else if (nodeType === 3) {
                        node.textContent = tgValue;
                    }
                });
            }
        },

        //從{{name.age.sex}}中獲取name.age.sex
        getKey: function(str) {
            return str.match(this.regExp) ? str.match(this.regExp)[1] : "";
        },

        //獲取key對應的value值
        getValueByKey: function(key) {
            var arr = key ? key.split(".") : [];
            var temp = this.data;
            for (var i = 0; i < arr.length; i++) {
                if (temp) {
                    temp = temp[arr[i]];
                } else {
                    temp = undefined;
                    break
                }
            }
            return temp;
        },


        isElement: function(ele) {
            return ele.nodeType === 1 ? true : false;
        },
        isText: function(ele) {
            return ele.nodeType === 3 ? true : false;
        },
        getElement: function() {
            return this.ele;
        }
    }




    3.ViewModel:結合Observer與Compile,實現model > view的數據單向綁定
    var ViewModel = function(opts) {
        this.opts = opts;
        this.data = opts.data;
        this.wrapper = opts.wrapper;
        this.template = opts.template;
        this.Observer = (typeof Observer != undefined) ? Observer : opts.Observer;
        this.Compile = (typeof Compile != undefined) ? Compile : opts.Compile;
        this.init();
    }

    ViewModel.prototype = {
        init: function() {
            var opts = this.opts;
            this.observer = new this.Observer(opts);
            this.observer.monit(this.data); //監控數據變化,數據已經改變了
            this.compiler = new this.Compile(Object.assign(opts, {
                observer: this.observer
            })); //編譯生成節點
            if (this.wrapper) {
                this.wrapper.appendChild(this.compiler.getElement());
            }
        },
        get: function() {
            return this.compiler.getElement();
        }
    };

總結

簡單地調用new ViewModel({data:data,template:template}),完成了model和view的綁定,
ViewModel內部大體執行順序是:

1. 建立數據監控對象this.observer,該對象監控data(監控之後,data的屬性改變,
   就會執行defineProperty中的set函數,set函數裏面添加了publish發佈函數)

2. 建立模板編譯器對象this.compiler,該對象編譯template,生成最終的dom樹,
   而且給每一個須要綁定數據的dom節點添加了subscribe訂閱函數

3. 最後,改變data裏面的屬性,會自動觸發defineProperty中的set函數,set函數調用publish函數,
   publish會根據key的名稱,找到對應的須要執行的函數列表,依次執行全部函數

Git地址

https://github.com/devil1989/databind/

demo

<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
        <link rel="stylesheet" type="text/css" href="demo.css">
        <script type="text/javascript" src="./observe.js"></script>
    </head>
    <body>
        <template id="inner" type="text/template">
            
            <div title="{{des}}">
                <div>
                    <ul id="list">
                        <li >
                            <span >age:</span>
                            <input  type="text" name="" value="{{age}}" >
                            <span id="age" style="float: left;">+</span>
                        </li>
                        <li>
                            <span>name:</span>
                            <input id="firstName" type="text" name="" value="{{name}}">
                        </li>
                        <li><span>{{name}}</span></li>
                    </ul>
                </div>
                
            </div>
        </template>
        <script type="text/javascript">
            (function(){
                window.data={name:"jeffrey",age:28,des:"測試"};
                var vm=new VM({
                    data:data,
                    template:document.getElementById("inner").innerHTML
                    /* wrapper:document.body//能夠指定對應容器,也能夠不指定容器,
                    直接獲取元素,再手動插入對應dom元素*/
                });
                document.body.appendChild(vm.get());

                document.getElementById("age").addEventListener("click",function(){
                    data.age++;//只須要修改屬性,html就會從新渲染
                });

                document.getElementById("firstName").addEventListener("keyup",function(e){
                    data.name=this.value;//只須要修改屬性,html就會從新渲染
                });
            })();
        </script>
    </body>
    </html>

使用場景說明:

當咱們想要修改頁面某個元素的信息,但又不想費勁地查找dom元素再去修改元素的值,
這種狀況下,能夠用demo中的數據綁定,只需修改數據的值,就實現了頁面元素從新渲染
請看下面的gif動畫中展現的,只要修改data.age和data.name,頁面元素就自動從新渲染了

avatar

結束語

本demo只是簡單實現數據綁定,不少功能並未實現,只是提供一種思路,拋磚引玉;
若是對上述代碼中的Observer類的代碼不是很理解,能夠先了解下觀察者模式以及實現原理;
最後,感謝你們的閱讀!!css

推薦: 翻譯項目Master的自述:

1. 乾貨|人人都是翻譯項目的Master

2. iKcamp出品微信小程序教學共5章16小節彙總(含視頻)

3. 開始免費連載啦~每週2更共11堂iKcamp課|基於Koa2搭建Node.js實戰項目教學(含視頻)| 課程大綱介紹

相關文章
相關標籤/搜索