美文网首页前端跟我学让前端飞
如何实现VM框架中的数据绑定

如何实现VM框架中的数据绑定

作者: iKcamp | 来源:发表于2018-01-17 10:24 被阅读25次

    作者:佳杰

    本文原创,转载请注明作者及出处

    如何实现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类的代码不是很理解,可以先了解下观察者模式以及实现原理;
    最后,感谢大家的阅读!!

    移动Web前端高效开发实战.png

    推荐: 翻译项目Master的自述:

    1. 干货|人人都是翻译项目的Master

    2. iKcamp出品微信小程序教学共5章16小节汇总(含视频)

    3. 开始免费连载啦~每周2更共11堂iKcamp课|基于Koa2搭建Node.js实战项目教学(含视频)| 课程大纲介绍

    相关文章

      网友评论

        本文标题:如何实现VM框架中的数据绑定

        本文链接:https://www.haomeiwen.com/subject/fgpyoxtx.html