どうやって双方向バインディングmvmの原理を実現しますか?


本文は何をしてあげられますか?
1、Vueの双方向データバインディング原理とコアコードモジュールを理解する
2、好奇心を和らげながら、双方向結合の実現方法を知る
原理と実現を説明するために、本明細書の関連コードは主にvueソースコードから抜粋し、簡略化された改造を行いました。比較的に粗末で、行列の処理、データの循環依存などを考慮していません。でも、これらはみんなの読書と理解に影響しません。
vueソースを読む時にもっと役に立ちます。
本論文のすべての関連コードは、githubの上にgithub.com/DMQ/mvmを見つけることができます。
皆さんはmvvm双方向バインディングに慣れていないと思います。一言でコードに合わないと、ここで最終的に実現される効果を見ましょう。vueと同じ文法です。もし双方向バインディングを理解していないなら、Googleを猛突きします。

<div id="mvvm-app">
  <input type="text" v-model="word">
  <p>{{word}}</p>
  <button v-on:click="sayHi">change model</button>
</div>

<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/mvvm.js"></script>
<script>
var vm = new MVVM({
  el: '#mvvm-app',
    data: {
      word: 'Hello World!'
    },
    methods: {
      sayHi: function() {
        this.word = 'Hi, everybody!';
      }
    }
  });
</script>

いくつかの双方向結合を実現する方法
現在いくつかの主流のmvc(vm)フレームワークは一方向データバインディングを実現していますが、私が理解している双方向データバインディングは一方向バインディングに基づいて、入力可能要素(input、textareなど)にchangeイベントを追加して、modelとviewを動的に修正しています。そのため、あまり気を使わなくてもいいです。シングルまたは双方向の結合が実現されます。
データバインディングを実現する方法は、大体以下の通りです。
  • リリース者-予約者モード(backbone.js)
  • 汚値検査(anglar.js)
  • データハイジャック(vue.js)
  • 配布者-購読者モード:普通はsub、pubを通じてデータとビューのバインディングの傍受を実現して、データ方式を更新するのはvm.set('property'、value)です。ここでは文章で話したのが比較的に詳しくて、興味があります。
    このような方式は結局lowすぎます。Vm.property=valueという方式でデータを更新したいです。同時に自動的にビューを更新します。
    汚値検査:anglar.jsは汚値検出によってデータに変更があるかどうかを判定してビューを更新するかを決定します。一番簡単な方法はset Interval()のタイミングポーリングによってデータが変動します。もちろんGoogleはこのようにlowしません。anglarは指定されたものがトリガされた時だけ汚損値検出に入ります。大体以下の通りです。
  • DOMイベント、例えばユーザがテキストを入力し、ボタンをクリックするなどです。ng-click)
  • XHR応答イベント
  • ブラウザLocation変更イベント
  • Timerイベント
  • は、digest()またはappy()
  • を実行する。
    データハイジャック:vue.jsはデータハイジャック結合リリース者-購読者モードを採用し、Object.definePropertyを通じて各属性のsetterをハイジャックし、getterはデータ変動時に購読者にメッセージを発表し、相応の傍受フィードバックをトリガする。
    考えがまとまる
    vueはデータハイジャックによってデータバインディングを行うことがわかっています。その中で最も核心的な方法はObject.definePropertyを通じて属性のハイジャックを実現し、データの変動を監督する目的を達成することです。この方法は本文の中で最も重要で基礎的な内容の一つです。mvmの双方向バインディングを実現するには、次のいくつかの点が必要です。1、データモニターObserverを実現するには、データオブジェクトのすべての属性をモニターすることができます。変更があれば最新の値を入手して、購読者2に通知し、コマンド解析器Compleを実現し、各要素ノードのコマンドをスキャンして解析します。コマンドテンプレートに従ってデータを交換します。また、対応する更新関数3をバインドし、ObserverとCommpileを接続するためのブリッジとして、各属性の変動の通知を購読し、受信し、コマンドバインディングの対応するコールバック関数を実行して、ビュー4、mvmエントリ関数を更新し、上記3つを統合することができます。
    1、Observerを実現する
    ok、構想はすでに整理し終わって、関連しているロジックとモジュールの機能も比較的に明確になりました。let's do itはObeject.defineProperty()を利用して属性の変動を監督することができると知っています。そうすると、observeのデータオブジェクトを再帰的に巡回します。 setterとgetterは、この対象のある値を付与すると、setterをトリガし、データの変化をモニターすることができます。関連コードはこのようにすることができます。
    
    var data = {name: 'kindeng'};
    observe(data);
    data.name = 'dmq'; //    ,        kindeng --> dmq
    
    function observe(data) {
      if (!data || typeof data !== 'object') {
        return;
      }
      //         
      Object.keys(data).forEach(function(key) {
       defineReactive(data, key, data[key]);
     });
    };
    
    function defineReactive(data, key, val) {
      observe(val); //      
      Object.defineProperty(data, key, {
        enumerable: true, //    
        configurable: false, //    define
        get: function() {
          return val;
        },
        set: function(newVal) {
          console.log('   ,        ', val, ' --> ', newVal);
          val = newVal;
        }
      });
    }
    
    
    
    
    これで各データの変化をモニターできます。変化をモニターしてから、購読者にどうやって通知しますか?だから、これからはメッセージ購読器を実現する必要があります。簡単です。配列を維持して、購読者を集めて、データの変動によってnotifyを触発して、購読者のudate方法を呼び出します。コードの改善後はこうです。
    
    // ...   
    function defineReactive(data, key, val) {
     var dep = new Dep();
      observe(val); //      
    
      Object.defineProperty(data, key, {
        // ...   
        set: function(newVal) {
         if (val === newVal) return;
          console.log('   ,        ', val, ' --> ', newVal);
          val = newVal;
          dep.notify(); //        
        }
      });
    }
    
    function Dep() {
      this.subs = [];
    }
    Dep.prototype = {
      addSub: function(sub) {
        this.subs.push(sub);
      },
      notify: function() {
        this.subs.forEach(function(sub) {
          sub.update();
        });
      }
    };
    
    
    問題が来ました。誰が購読者ですか?購読者はどうやって購読者を追加しますか?そうです。上の考えを整理しています。予約者はウオッチであるべきです。そして、var dep=new Dep()です。defineReactiveメソッドの内部で定義されているので、depを通して購読者を追加するには、クローズド内で操作しなければなりません。 ゲテの中で足を動かす:
    
    // Observer.js
    // ...  
    Object.defineProperty(data, key, {
     get: function() {
     //           watcher,    Dep      target  ,  watcher,      
     Dep.target && dep.addDep(Dep.target);
     return val;
     }
      // ...   
    });
    
    // Watcher.js
    Watcher.prototype = {
     get: function(key) {
     Dep.target = this;
     this.value = data[key]; //         getter,       
     Dep.target = null;
     }
    }
    
    
    ここでObserverが実現されました。モニターデータとデータの変化をカスタマイズ者に通知する機能を備えています。完全コードです。これからCopileを実現します。
    2、Compleを実現する
    compleは主にテンプレートコマンドを解析して、テンプレートの変数をデータに置き換えて、レンダリングページビューを初期化して、各コマンドに対応するノードをバインドして更新関数を更新して、モニターデータの購読者を追加します。データが変更されたら、通知を受けて、より新しいビューを図に示します。img 3][img 3]
    解析のプロセスを経ると、domノードを複数回操作し、性能と効率を向上させるために、まずvueインスタンスのルートノードのelをドキュメントの破片fragmentに変換して解析的にコンパイルし、解析が完了したら、fragmentを元のリアルdomノードに追加します。
    
    function Compile(el) {
      this.$el = this.isElementNode(el) ? el : document.querySelector(el);
      if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
      }
    }
    Compile.prototype = {
     init: function() { this.compileElement(this.$fragment); },
      node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(), child;
        //         fragment
        while (child = el.firstChild) {
          fragment.appendChild(child);
        }
        return fragment;
      }
    };
    
    
    compleElement方法は、すべてのノードとそのサブノードを巡回してスキャン解析コンパイルを行い、対応するコマンドレンダリング関数を呼び出してデータレンダリングを行い、対応するコマンド更新関数を呼び出してバインディングし、コードとコメントの説明を詳しく見てください。
    
    Compile.prototype = {
     // ...   
     compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
          var text = node.textContent;
          var reg = /\{\{(.*)\}\}/; //      
          //          
          if (me.isElementNode(node)) {
            me.compile(node);
          } else if (me.isTextNode(node) && reg.test(text)) {
            me.compileText(node, RegExp.$1);
          }
          //        
          if (node.childNodes && node.childNodes.length) {
            me.compileElement(node);
          }
        });
      },
    
      compile: function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
          //   :    v-xxx   
          //   <span v-text="content"></span>      v-text
          var attrName = attr.name; // v-text
          if (me.isDirective(attrName)) {
            var exp = attr.value; // content
            var dir = attrName.substring(2); // text
            if (me.isEventDirective(dir)) {
             //     ,   v-on:click
              compileUtil.eventHandler(node, me.$vm, exp, dir);
            } else {
             //     
              compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
            }
          }
        });
      }
    };
    
    //       
    var compileUtil = {
      text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
      },
      // ...  
      bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        //         
        updaterFn && updaterFn(node, vm[exp]);
        //       ,                       watcher
        new Watcher(vm, exp, function(value, oldValue) {
         //         ,            ,    
          updaterFn && updaterFn(node, value, oldValue);
        });
      }
    };
    
    //     
    var updater = {
      textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
      }
      // ...  
    };
    
    
    ここでは再帰的巡回により、各ノードおよびサブノードが、{}表現宣言を含むテキストノードに解析的にコンパイルされることを保証する。命令の声明は、特定のプレフィクスのノード属性によってマークされることになっています。例えば、「span v-text=「content」other-atrのv-textはコマンドであり、other-atrはコマンドではなく、普通の属性です。モニターデータ、バインディング更新関数の処理は、compleUtil.bind()という方法で、new Watch()にフィードバックを追加してデータ変化の通知を受信する。
    これで簡単なCompleが完成しました。ここです。次にWatchという購読者の具体的な実現を見ます。
    3、Watchを実現する
    Watch購読者はObserverとCompleとの間の通信の架け橋として、主にやることは:1、自分の実用化時に属性購読器に自分を追加してください。2、自分自身にアップロード方法が必要です。ちょっと乱れているなら、前の考えを振り返って整理してもいいです。
    
    function Watcher(vm, exp, cb) {
      this.cb = cb;
      this.vm = vm;
      this.exp = exp;
      //          getter,   dep    ,  Observer    
      this.value = this.get(); 
    }
    Watcher.prototype = {
      update: function() {
        this.run(); //          
      },
      run: function() {
        var value = this.get(); //      
        var oldVal = this.value;
        if (value !== oldVal) {
          this.value = value;
          this.cb.call(this.vm, value, oldVal); //   Compile      ,    
        }
      },
      get: function() {
        Dep.target = this; //           
        var value = this.vm[exp]; //   getter,           
        Dep.target = null; //     ,  
        return value;
      }
    };
    //       Observer Dep,    
    Object.defineProperty(data, key, {
     get: function() {
     //           watcher,     Dep      target  ,  watcher,      
     Dep.target && dep.addDep(Dep.target);
     return val;
     }
      // ...   
    });
    Dep.prototype = {
      notify: function() {
        this.subs.forEach(function(sub) {
          sub.update(); //       update  ,    
        });
      }
    };
    
    
    Watchを実例化する際、get()メソッドを呼び出し、Dep.target=watch Instanceで現在のウォッチの例をマーキングし、属性定義のgetterメソッドを強制的に触発し、getterメソッドを実行すると、プロパティの購読器depに現在のウォッチのインスタンスを追加し、属性値が変化したときに、watch Instanceに更新通知が届きます。
    ok、ウォッチももう実現しました。完全コード。基本的にvueの中でデータの結合に関する比較核心的ないくつかのモジュールもこのいくつかであり、ダッシュ完全コードは、Srcディレクトリでvueソースを見つけることができます。
    最後にMVVMエントランスファイルの関連ロジックと実現を説明します。比較的簡単です。
    4、MVVMの実現
    MVMはデーターバインディングの入口として、Observer、Comple、Watchの3つを統合し、Observerを通じて自分のmodelデータの変化をモニターし、Compleを通じてテンプレートコマンドを解析し、最終的にはWatchを利用してObserverとCompleの間の通信橋を持ち上げて、データの変化->ビューの更新を達成する。ビューのインタラクティブ変化(input)->データmodelの変更による双方向バインディング効果です。
    簡単なMVVMコンストラクタはこのようです。
    
    function MVVM(options) {
      this.$options = options;
      var data = this._data = this.$options.data;
      observe(data, this);
      this.$compile = new Compile(options.el || document.body, this)
    }
    
    
    しかし、ここで問題があります。コードの中から傍受のデータオブジェクトはoptions.dataであり、毎回ビューを更新する必要がある場合、var vm=new MVMを通過しなければなりません。vm._.data.name='dmq'このようにデータを変更します。
    明らかに私達の最初の期待に合わないです。私達が期待しているコール方式はこうです。var vm=new MVM({data:''kideng');vm.name='dmq';
    したがって、ここではMVMのインスタンスに属性エージェントを追加する方法が必要であり、vmにアクセスする属性エージェントをvm._.にアクセスさせる。dataの属性、改造後のコードは以下の通りです。
    
    function MVVM(options) {
      this.$options = options;
      var data = this._data = this.$options.data, me = this;
      //     ,   vm.xxx -> vm._data.xxx
      Object.keys(data).forEach(function(key) {
        me._proxy(key);
      });
      observe(data, this);
      this.$compile = new Compile(options.el || document.body, this)
    }
    
    MVVM.prototype = {
     _proxy: function(key) {
     var me = this;
        Object.defineProperty(me, key, {
          configurable: false,
          enumerable: true,
          get: function proxyGetter() {
            return me._data[key];
          },
          set: function proxySetter(newVal) {
            me._data[key] = newVal;
          }
        });
     }
    };
    
    
    ここでは主にObject.defineProperty()を利用してvmインスタンスオブジェクトの属性の読み書き権をハイジャックし、読み書きvmインスタンスの属性を読み書きvm.dataの属性値は、魚眼混珠の効果を達成します。
    以上が本文の全部です。皆さんの勉強に役に立つように、私たちを応援してください。