知识 分享 互助 懒人建站

    懒人建站专注于网页素材下载,提供网站模板、网页设计、ps素材、图片素材等,服务于【个人站长】【网页设计师】和【web开发从业者】的代码素材与设计素材网站。

    懒人建站提供网页素材下载、网站模板
    知识 分享 互助!

    实现一个双向数据绑定的简易MVVM框架

    作者:bbin视讯真人娱乐平台登入 来源:oschina 2017-05-31 人气:
    reactJs、vueJs、Angular.js等MVVM框架大火,先用jquery尝试一下双向数据绑定然后再自己动手实现一下双向数据绑定的mvvm框架,
    reactJs、vueJs、Angular.js等MVVM框架大火,其中vueJs、Angular.js双向数据绑定功能对于数据交互频繁的使用场景真的是非常的酸爽,reactJs默认不支持双向数据绑定,但是实现双向绑定也不难,这里不研究这些框架的使用。下面我们来自己动手实现一下双向数据绑定的mvvm框架。

    用jquery实现一个数据双向绑定

    jquery这个神器,依然活跃很多年,未来依然可以活跃很多年,我们这里就用jquery实现一下双向数据绑定。jquery实现双向数据绑定采用DOM事件的订阅和发布机制。

    jquery实现的原文:https://www.oschina.net/translate/easy-two-way-data-binding-in-javascript

    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;
    }
    对于上面这个实现来说,下面是一个User模型的最简单的实现方法: 
    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;
    }
    现在我们如果想要将User模型属性绑定到UI上,我们只需要将适合的数据特性绑定到对应的HTML元素上。
    // javascript
    var user = new User( 123 );
    user.set( "name", "Wolfgang" );
    
    // html
    <input type="number" data-bind-123="name" />
    
    这样输入值会自动映射到user对象的name属性,反之亦然。到此这个简单实现就完成啦!

    自己实现一个简易MVVM框架

    体验过使用jquery实现一个双向绑定的功能后,我们来研究下,自己实现一个简易MVVM框架

    我们知道的,常见的数据绑定的实现方法

    1、数据劫持(vue):通过Object.defineProperty() 去劫持数据每个属性对应的getter和setter
    2、脏值检测(angular):通过特定事件比如input,change,xhr请求等进行脏值检测。
    3、发布-订阅模式(backbone):通过发布消息,订阅消息进行数据和视图的绑定监听。具体代码实现可以参考我github个人仓库overwrite->my-observer

    一言不合先上代码

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>example</title>
      <script src="./mvvm.js" charset="utf-8"></script>
    </head>
    <body>
      <div id="mvvm">
        <h2>{{b}}</h2>
        <input type="text" x-model="a">
        <input type="text" name="" value="" x-model="a">
        <p x-html="a">{{ a }}</p>
        <button type="button" name="button" x-on:click="testToggle">change b</button>
      </div>
    </body>
    <script>
    var vm = new MVVM({
      el: '#mvvm',
      data: {
        a: 'test model',
        b: 'hello MVVM',
        flag: true
      },
      methods: {
        testToggle: function () {
          this.flag = !this.flag;
          this.b = this.flag ? 'hello MVVM' : 'test success'
        }
      }
    });
    </script>
    </html>
    

    效果图

    实现一个双向数据绑定的简易MVVM框架

    看完效果图之后,接下来我们直接搞事情吧

    一、MVVM框架整体流程图

    要实现一个我们自己的mvvm库,我们首先需要做的事情不是写代码,而是整理一下思路,捋清楚之后再动手绝对会让你事半功倍。先上流程图,我们对着流程图来捋思路
    MVVM框架整体流程图

    如上图所示,我们可以看到,整体实现分为四步

    1、实现一个Observer,对数据进行劫持,通知数据的变化
    2、实现一个Compile,对指令进行解析,初始化视图,并且订阅数据的变更,绑定好更新函数
    3、实现一个Watcher,将其作为以上两者的一个中介点,在接收数据变更的同时,让Dep添加当前Watcher,并及时通知视图进行update
    4、实现MVVM,整合以上三者,作为一个入口函数

    二、mvvm框架技术点实现

    1、实现Observer

    这里我们需要做的事情就是实现数据劫持,并将数据变更给传递下去。那么这里将会用到的方法就是Object.defineProperty()来做这么一件事。先不管三七二十一,咱先用用Object.defineProperty()试试手感。

    function observe (data) {
      if (!data || typeof data !== 'object') {
        return;
      }
      Object.keys(data).forEach(key => {
        observeProperty(data, key, data[key])
      })
    }
    function observeProperty (obj, key, val) {
      observe(val);
      Object.defineProperty(obj, key, {
        enumerable: true,   // 可枚举
        configurable: true, // 可重新定义
        get: function () {
          return val;
        },
        set: function (newVal) {
          if (val === newVal || (newVal !== newVal && val !== val)) {
            return;
          }
          console.log('数据更新啦 ', val, '=>', newVal);
          val = newVal;
        }
      });
    }
    

    调用

    var data = {
      a: 'hello'
    }
    observe(data);

    效果如下

    看完是不是发现JavaScript提供给我们的Object.defineProperty()方法功能巨强大巨好用呢。

    其实到这,我们已经算是完成了数据劫持,完整的Observer则需要将数据的变更传递给Dep实例,然后接下来的事情就丢给Dep去通知下面完成接下来的事情了,完整代码如下所示

    /**
     * @class 发布类 Observer that are attached to each observed
     * @param {[type]} value [vm参数]   
      * 懒人建站http://bbinsxzrylptdr.xsb868.com/ 整理发布
     */
     function observe(value, asRootData) {
       if (!value || typeof value !== 'object') {
         return;
       }
       return new Observer(value);
     }
    
    function Observer(value) {
      this.value = value;
      this.walk(value);
    }
    
    Observer.prototype = {
      walk: function (obj) {
        let self = this;
        Object.keys(obj).forEach(key => {
          self.observeProperty(obj, key, obj[key]);
        });
      },
      observeProperty: function (obj, key, val) {
        let dep = new Dep();
        let childOb = observe(val);
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get: function() {
            if (Dep.target) {
              dep.depend();
            }
            if (childOb) {
              childOb.dep.depend();
            }
            return val;
          },
          set: function(newVal) {
            if (val === newVal || (newVal !== newVal && val !== val)) {
              return;
            }
            val = newVal;
            // 监听子属性
            childOb = observe(newVal);
            // 通知数据变更
            dep.notify();
          }
        })
      }
    }
    /**
     * @class 依赖类 Dep
     */
    let uid = 0;
    function Dep() {
      // dep id
      this.id = uid++;
      // array 存储Watcher
      this.subs = [];
    }
    Dep.target = null;
    Dep.prototype = {
      /**
       * [添加订阅者]
       * @param  {[Watcher]} sub [订阅者]
       */
      addSub: function (sub) {
        this.subs.push(sub);
      },
      /**
       * [移除订阅者]
       * @param  {[Watcher]} sub [订阅者]
       */
      removeSub: function (sub) {
        let index = this.subs.indexOf(sub);
        if (index !== -1) {
          this.subs.splice(index ,1);
        }
      },
      // 通知数据变更
      notify: function () {
        this.subs.forEach(sub => {
          // 执行sub的update更新函数
          sub.update();
        });
      },
      // add Watcher
      depend: function () {
        Dep.target.addDep(this);
      }
    }
    // 结合Watcher
    /** 
    * Watcher.prototype = {
    *   get: function () {
    *     Dep.target = this;
    *     let value = this.getter.call(this.vm, this.vm);
    *     Dep.target = null;
    *     return value;
    *   },
    *   addDep: function (dep) {
    *     dep.addSub(this);
    *   }
    * }
    */

    至此,我们已经实现了数据的劫持以及notify数据变化的功能了。

    2、实现Compile

    按理说我们应该紧接着实现Watcher,毕竟从上面代码看来,Observer和Watcher关联好多啊,但是,我们在捋思路的时候也应该知道了,Watcher和Compile也是有一腿的哦。所以咱先把Compile也给实现了,这样才能更好的让他们3P。

    Compile需要做的事情也很简单
    a、解析指令,将指令模板中的变量替换成数据,对视图进行初始化操作
    b、订阅数据的变化,绑定好更新函数
    c、接收到数据变化,通知视图进行view update

    咱先试着写一个简单的指令解析方法,实现解析指令初始化视图。

    js部分

    function Compile (el, value) {
      this.$val = value;
      this.$el = this.isElementNode(el) ? el : document.querySelector(el);
      if (this.$el) {
        this.compileElement(this.$el);
      }
    }
    Compile.prototype = {
      compileElement: function (el) {
        let self = this;
        let childNodes = el.childNodes;
        [].slice.call(childNodes).forEach(node => {
          let text = node.textContent;
          let reg = /{{((?:.|
    )+?)}}/;
          // 如果是element节点
          if (self.isElementNode(node)) {
            self.compile(node);
          }
          // 如果是text节点
          else if (self.isTextNode(node) && reg.test(text)) {
            // 匹配第一个选项
            self.compileText(node, RegExp.$1.trim());
          }
          // 解析子节点包含的指令
          if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);
          }
        })
      },
      // 指令解析
      compile: function (node) {
        let nodeAttrs = node.attributes;
        let self = this;
    
        [].slice.call(nodeAttrs).forEach(attr => {
          var attrName = attr.name;
          if (self.isDirective(attrName)) {
            var exp = attr.value;
            node.innerHTML = typeof this.$val[exp] === 'undefined' ? '' : this.$val[exp];
            node.removeAttribute(attrName);
          }
        });
      },
      // {{ test }} 匹配变量 test
      compileText: function (node, exp) {
        node.textContent = typeof this.$val[exp] === 'undefined' ? '' : this.$val[exp];
      },
      // element节点
      isElementNode: function (node) {
        return node.nodeType === 1;
      },
      // text纯文本
      isTextNode: function (node) {
        return node.nodeType === 3
      },
      // x-XXX指令判定
      isDirective: function (attr) {
        return attr.indexOf('x-') === 0;
      }
    }

    html部分

    <body>
    <div id="test">
      <h2 x-html="a"></h2>
      <p>{{ a }}</p>
    </div>
    </body>
    <script>
    var data = {
      a: 'hello'
    }
    new Compile('#test', data)
    </script>

    结果如图所示

     
    按照步骤走的我已经实现了指令解析!
    这里我们只是实现了指令的解析以及视图的初始化,并没有实现数据变化的订阅以及视图的更新。完整的Compile则实现了这些功能,详细代码如下

    /**
     * @class 指令解析类 Compile
     * @param {[type]} el [element节点]
     * @param {[type]} vm [mvvm实例]
     */
    function Compile(el, vm) {
      this.$vm = vm;
      this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    
      if (this.$el) {
        this.$fragment = this.nodeFragment(this.$el);
        this.compileElement(this.$fragment);
        // 将文档碎片放回真实dom
        this.$el.appendChild(this.$fragment)
      }
    }
    Compile.prototype = {
      compileElement: function (el) {
        let self = this;
        let childNodes = el.childNodes;
        [].slice.call(childNodes).forEach(node => {
          let text = node.textContent;
          let reg = /{{((?:.|
    )+?)}}/;
    
          // 如果是element节点
          if (self.isElementNode(node)) {
            self.compile(node);
          }
          // 如果是text节点
          else if (self.isTextNode(node) && reg.test(text)) {
            // 匹配第一个选项
            self.compileText(node, RegExp.$1);
          }
          // 解析子节点包含的指令
          if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);
          }
        });
      },
      // 文档碎片,遍历过程中会有多次的dom操作,为提高性能我们会将el节点转化为fragment文档碎片进行解析操作
      // 解析操作完成,将其添加回真实dom节点中
      nodeFragment: function (el) {
        let fragment = document.createDocumentFragment();
        let child;
    
        while (child = el.firstChild) {
          fragment.appendChild(child);
        }
        return fragment;
      },
      // 指令解析
      compile: function (node) {
        let nodeAttrs = node.attributes;
        let self = this;
    
        [].slice.call(nodeAttrs).forEach(attr => {
          var attrName = attr.name;
          if (self.isDirective(attrName)) {
            var exp = attr.value;
            var dir = attrName.substring(2);
            // 事件指令
            if (self.isEventDirective(dir)) {
              compileUtil.eventHandler(node, self.$vm, exp, dir);
            }
            // 普通指令
            else {
              compileUtil[dir] && compileUtil[dir](node, self.$vm, exp);
            }
    
            node.removeAttribute(attrName);
          }
        });
      },
      // {{ test }} 匹配变量 test
      compileText: function (node, exp) {
        compileUtil.text(node, this.$vm, exp);
      },
      // element节点
      isElementNode: function (node) {
        return node.nodeType === 1;
      },
      // text纯文本
      isTextNode: function (node) {
        return node.nodeType === 3
      },
      // x-XXX指令判定
      isDirective: function (attr) {
        return attr.indexOf('x-') === 0;
      },
      // 事件指令判定
      isEventDirective: function (dir) {
        return dir.indexOf('on') === 0;
      }
    }
    // 定义$elm,缓存当前执行input事件的input dom对象
    let $elm;
    let timer = null;
    // 指令处理集合
    const compileUtil = {
      html: function (node, vm, exp) {
        this.bind(node, vm, exp, 'html');
      },
      text: function (node, vm, exp) {
        this.bind(node, vm, exp, 'text');
      },
      class: function (node, vm, exp) {
        this.bind(node, vm, exp, 'class');
      },
      model: function(node, vm, exp) {
        this.bind(node, vm, exp, 'model');
    
        let self = this;
        let val = this._getVmVal(vm, exp);
        // 监听input事件
        node.addEventListener('input', function (e) {
          let newVal = e.target.value;
          $elm = e.target;
          if (val === newVal) {
            return;
          }
          // 设置定时器  完成ui js的异步渲染
          clearTimeout(timer);
          timer = setTimeout(function () {
            self._setVmVal(vm, exp, newVal);
            val = newVal;
          })
        });
      },
      bind: function (node, vm, exp, dir) {
        let updaterFn = updater[dir + 'Updater'];
    
        updaterFn && updaterFn(node, this._getVmVal(vm, exp));
    
        new Watcher(vm, exp, function(value, oldValue) {
          updaterFn && updaterFn(node, value, oldValue);
        });
      },
      // 事件处理
      eventHandler: function(node, vm, exp, dir) {
        let eventType = dir.split(':')[1];
        let fn = vm.$options.methods && vm.$options.methods[exp];
    
        if (eventType && fn) {
          node.addEventListener(eventType, fn.bind(vm), false);
        }
      },
      /**
       * [获取挂载在vm实例上的value]
       * @param  {[type]} vm  [mvvm实例]
       * @param  {[type]} exp [expression]
       */
      _getVmVal: function (vm, exp) {
        let val = vm;
        exp = exp.split('.');
        exp.forEach(key => {
          key = key.trim();
          val = val[key];
        });
        return val;
      },
      /**
       * [设置挂载在vm实例上的value值]
       * @param  {[type]} vm    [mvvm实例]
       * @param  {[type]} exp   [expression]
       * @param  {[type]} value [新值]
       */
      _setVmVal: function (vm, exp, value) {
        let val = vm;
        exps = exp.split('.');
        exps.forEach((key, index) => {
          key = key.trim();
          if (index < exps.length - 1) {
            val = val[key];
          }
          else {
            val[key] = value;
          }
        });
      }
    }
    // 指令渲染集合
    const updater = {
      htmlUpdater: function (node, value) {
        node.innerHTML = typeof value === 'undefined' ? '' : value;
      },
      textUpdater: function (node, value) {
        node.textContent = typeof value === 'undefined' ? '' : value;
      },
      classUpdater: function () {},
      modelUpdater: function (node, value, oldValue) {
        // 不对当前操作input进行渲染操作
        if ($elm === node) {
          return false;
        }
        $elm = undefined;
        node.value = typeof value === 'undefined' ? '' : value;
      }
    }
    

     

    好了,到这里两个和Watcher相关的“菇凉”已经出场了

    3、实现Watcher

    作为一个和Observer和Compile都有关系的“蓝银”,他做的事情有以下几点

    a、通过Dep接收数据变动的通知,实例化的时候将自己添加到dep中
    b、属性变更时,接收dep的notify,调用自身update方法,触发Compile中绑定的更新函数,进而更新视图

    这里的代码比较简短,所以我决定直接上代码

    /**
     * @class 观察类
     * @param {[type]}   vm      [vm对象]
     * @param {[type]}   expOrFn [属性表达式]
     * @param {Function} cb      [回调函数(一半用来做view动态更新)]
     */
    function Watcher(vm, expOrFn, cb) {
      this.vm = vm;
      expOrFn = expOrFn.trim();
      this.expOrFn = expOrFn;
      this.cb = cb;
      this.depIds = {};
    
      if (typeof expOrFn === 'function') {
        this.getter = expOrFn
      }
      else {
        this.getter = this.parseGetter(expOrFn);
      }
      this.value = this.get();
    }
    Watcher.prototype = {
      update: function () {
        this.run();
      },
      run: function () {
        let newVal = this.get();
        let oldVal = this.value;
        if (newVal === oldVal) {
          return;
        }
        this.value = newVal;
        // 将newVal, oldVal挂载到MVVM实例上
        this.cb.call(this.vm, newVal, oldVal);
      },
      get: function () {
        Dep.target = this;  // 将当前订阅者指向自己
        let value = this.getter.call(this.vm, this.vm); // 触发getter,将自身添加到dep中
        Dep.target = null;  // 添加完成 重置
        return value;
      },
      // 添加Watcher to Dep.subs[]
      addDep: function (dep) {
        if (!this.depIds.hasOwnProperty(dep.id)) {
          dep.addSub(this);
          this.depIds[dep.id] = dep;
        }
      },
      parseGetter: function (exp) {
        if (/[^w.$]/.test(exp)) return;
    
        let exps = exp.split('.');
    
        // 简易的循环依赖处理
        return function(obj) {
            for (let i = 0, len = exps.length; i < len; i++) {
                if (!obj) return;
                obj = obj[exps[i]];
            }
            return obj;
        }
      }
    }

    没错就是Watcher这么一个简短的“蓝银”和Observer和Compile两位“菇凉”牵扯不清

    4、实现MVVM部分

    可以说MVVM是Observer,Compile以及Watcher的“boss”了,他才不会去管他们员工之间的关系,只要他们三能给干活,并且干好活就行。他需要安排给Observer,Compile以及Watche做的事情如下

    a、Observer实现对MVVM自身model数据劫持,监听数据的属性变更,并在变动时进行notify
    b、Compile实现指令解析,初始化视图,并订阅数据变化,绑定好更新函数
    c、Watcher一方面接收Observer通过dep传递过来的数据变化,一方面通知Compile进行view update

    具体实现如下

    /**
     * @class 双向绑定类 MVVM
     * @param {[type]} options [description]
     */
    function MVVM (options) {
      this.$options = options || {};
      let data = this._data = this.$options.data;
      let self = this;
    
      Object.keys(data).forEach(key => {
        self._proxyData(key);
      });
      observe(data, this);
      new Compile(options.el || document.body, this);
    }
    MVVM.prototype = {
      /**
       * [属性代理]
       * @param  {[type]} key    [数据key]
       * @param  {[type]} setter [属性set]
       * @param  {[type]} getter [属性get]
       */
      _proxyData: function (key, setter, getter) {
        let self = this;
        setter = setter ||
        Object.defineProperty(self, key, {
          configurable: false,
          enumerable: true,
          get: function proxyGetter() {
            return self._data[key];
          },
          set: function proxySetter(newVal) {
            self._data[key] = newVal;
          }
        })
      }
    }

    至此,一个属于我们自己的mvvm库也算是完成了。由于本文的代码较多,又不太好分小部分抽离出来讲解,所以我将代码的解析都直接写到了代码中。文中一些不够严谨的思考和错误,还请各位小伙伴们拍砖指出,大家一起纠正一起学习。

    ↓ 查看全文

    实现一个双向数据绑定的简易MVVM框架由懒人建站收集整理,您可以自由传播,请主动带上本文链接

    懒人建站就是免费分享,觉得有用就多来支持一下,没有能帮到您,懒人也只能表示遗憾,希望有一天能帮到您。

    实现一个双向数据绑定的简易MVVM框架-最新评论

    网站地图 ag视讯直营登入 bbin视讯手机版下载登入 ag视讯官方网站登入
    太阳城真钱斗牛 澳门网上娱乐打牌 菲律宾申博怎么开户 菲律宾申博sunbet简介
    利华彩票客户端下载 中博娱乐游戏开户登入 天上人间游戏会所直营网 华夏彩票大全
    bbin视讯管理登入 ag旗舰厅游戏网站登入 ag视讯游戏登入 ag视讯客服端下载登入
    ag旗舰厅官方代理登入 ag旗舰厅注册登入 ag旗舰厅真人娱乐登入 ag视讯注册登入
    33sbmsc.com 585sunbet.com 162SUN.COM 277PT.COM XSB5555.COM
    988BBIN.COM S618P.COM 286sunbet.com 986ib.com ib57.com
    785DC.COM 8LSS.COM bq138.com 8NDS.COM 588cw.com
    XSB886.COM 333xsb.com 126jbs.com 986XTD.COM 526SUN.COM