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

作者:佳杰

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

如何实现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实战项目教学(含视频)| 课程大纲介绍

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,567评论 25 707
  • 王先生,现在是17年12月3日的21点53分,阳台很冷,月亮很圆,我翻开你的号码迟迟不敢播过去……我想,我没有可以...
    如果我是你的王先生阅读 177评论 0 0
  • 复制这条信息,打开手机淘宝即可看到【imagine me and you ——预售】¥AQprG1qrtI¥htt...
    你黄老师阅读 311评论 0 1
  • 今天参加了一场的婚礼,新郎和新娘都是我大学时的好友,证婚人则是我们的老班长。这俩人曾经一度被我们调侃无法“和谐”地...
    福兔纸阅读 198评论 1 2