<meter id="7ntxv"></meter>

<b id="7ntxv"><listing id="7ntxv"><ol id="7ntxv"></ol></listing></b>
<nobr id="7ntxv"></nobr><mark id="7ntxv"></mark>

    <p id="7ntxv"><menuitem id="7ntxv"><font id="7ntxv"></font></menuitem></p>
    <sub id="7ntxv"><menuitem id="7ntxv"><meter id="7ntxv"></meter></menuitem></sub>

    <mark id="7ntxv"></mark>
    <meter id="7ntxv"><var id="7ntxv"></var></meter>

    知識 分享 互助 懶人建站

      懶人建站專注于網頁素材下載,提供網站模板、網頁設計、ps素材、圖片素材等,服務于【個人站長】【網頁設計師】和【web開發從業者】的代碼素材與設計素材網站。

      懶人建站提供網頁素材下載、網站模板
      知識 分享 互助!

      實現一個雙向數據綁定的簡易MVVM框架

      作者:佳明媽 來源: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>{}</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://www.waidid2022.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框架-最新評論

      亚洲免费的黄色网站_黄色网站在线放久操射视频_A片www.黄色网站成年人_天天干 天天操天天干

      <meter id="7ntxv"></meter>

      <b id="7ntxv"><listing id="7ntxv"><ol id="7ntxv"></ol></listing></b>
      <nobr id="7ntxv"></nobr><mark id="7ntxv"></mark>

        <p id="7ntxv"><menuitem id="7ntxv"><font id="7ntxv"></font></menuitem></p>
        <sub id="7ntxv"><menuitem id="7ntxv"><meter id="7ntxv"></meter></menuitem></sub>

        <mark id="7ntxv"></mark>
        <meter id="7ntxv"><var id="7ntxv"></var></meter>