Vue2.x における双方向バインディングの原理と実装

Vue2.x における双方向バインディングの原理と実装

Vue はObject.defineProperty()メソッドを使用してデータを乗っ取り、set と get を使用してデータの読み取りと書き込みを検出します。

https://jsrun.net/RMIKp/embedded/all/light

MVVM フレームワークには、主に、データが変更されたときにビューを更新することと、ビューが変更されたときにデータを更新することという 2 つの側面が含まれます。

ビューはデータを変更して更新します。input のようなタグの場合は、 oninputイベントを使用できます。

データが変更されたときにビューを更新するには、 Object.definProperty()setメソッドを使用してデータの変更を検出します。データが変更されると、この関数がトリガーされ、ビューが更新されます。

1. 実施プロセス

双方向バインディングを実装する方法がわかったので、まずデータをハイジャックして監視する必要があるため、すべての属性の変更を監視するためのObserver関数を設定する必要があります。

プロパティが変更された場合、サブスクライバーwatcherに通知して、データの更新が必要かどうかを確認する必要があります。サブスクライバーが複数ある場合は、これらのサブスクライバーを収集し、 observerwatcher間で均一に管理するために Dep が必要です。

監視する必要があるノードと属性をスキャンして解析するには、ディレクティブ パーサーのcompileも必要です。

したがって、プロセスはおそらく次のようになります。

  • すべてのプロパティをハイジャックして監視し、変更が発生した場合にサブスクライバーに通知するObserverを実装します。
  • サブスクライバーWatcherを実装します。プロパティの変更通知を受け取ったら、対応する関数を実行してビューを更新します。これらのWatcherを収集するには、 Dep を使用します。
  • パーサーCompileを実装して、ノードの関連命令をスキャンおよび解析し、初期化テンプレートに従って対応するサブスクライバーを初期化します。

2. オブザーバーを表示する

Observerはデータ リスナーです。コア メソッドはObject.defineProperty()を使用して、監視対象のすべてのプロパティにsettergetterメソッドを再帰的に追加することです。

var ライブラリ = {
  本1: {
    名前: ""、
  },
  本2: "",
};
観察(ライブラリ);
library.book1.name = "vue authoritative guide"; // プロパティ名は監視されており、現在の値は "vue authoritative guide" です
library.book2 = "そのような本はありません"; // 属性 book2 は監視されており、現在の値は "そのような本はありません" です

// データの検出機能を追加 defineReactive(data, key, val) {
  observe(val); // すべてのサブ属性を再帰的に走査する let dep = new Dep(); // 新しい dep を作成する
  Object.defineProperty(データ、キー、{
    列挙可能: true、
    設定可能: true、
    取得: 関数() {
      if (依存ターゲット) {
        // サブスクライバーを追加するかどうかを決定します。これは最初の 1 回のみ必要で、次回は必要ありません。詳細については、Watcher 関数を参照してください。 dep.addSub(Dep.target); // サブスクライバーを追加します }
      戻り値:
    },
    設定: 関数(newVal) {
      if (val == newVal) return; // 値が変更されていない場合は戻ります
      val = 新しいVal;
      コンソール.log(
        「プロパティ」+ キー + 「」が監視されており、現在の値は次のとおりです: 「」+ newVal.toString() + 「」」
      );
      dep.notify(); // データが変更された場合は、すべてのサブスクライバーに通知します。
    },
  });
}

// 監視オブジェクトのすべてのプロパティ function observe(data) {
  if (!data || typeof data !== "object") {
    return; // オブジェクトでない場合は戻ります
  }
  Object.keys(データ).forEach(関数(キー) {
    defineReactive(データ、キー、データ[キー]);
  });
}
// Dep は、サブスクライバーを収集し、プロパティが変更されたときに更新関数をトリガーする役割を担います。
関数Dep() {
  this.subs = {};
}
派生プロトタイプ = {
  addSub: 関数(sub) {
    this.subs.push(sub);
  },
  通知: 関数() {
    this.subs.forEach((sub) => sub.update());
  },
};

アイデア分析では、サブスクライバー メッセージを収容できるサブスクライバー Dep が必要です。これは、サブスクライバーを収集し、属性が変更されたときに対応する更新機能を実行するために使用されます。

コードから、 getterにサブスクライバー Dep を追加するのは、初期化時にWatcherをトリガーするためです。したがって、サブスクライバーが必要かどうかを判断する必要があります。

setterでは、データが変更されるとすべてのサブスクライバーに通知され、サブスクライバーは対応する関数を更新します。

これまでのところ、比較的完全なObserverが完成しました。次に、Watcher の設計を開始します。

3. ウォッチャーを実装する

サブスクライバーWatcher 、初期化中にサブスクライバー Dep に自身を追加する必要があります。リスナーObserver get 中に実行される Watcher 操作であることは既にわかっているので、 Watcherが初期化されるときに対応するサブスクライバー操作を追加するには、対応する get 関数をトリガーするだけで済みます。

では、get をトリガーするにはどうすればよいでしょうか? Object.defineProperty()すでに設定してあるので、それをトリガーするには対応するプロパティ値を取得するだけで済みます。

サブスクライバー Watcher が初期化されたときにのみ Dep.target にサブスクライバーをキャッシュし、正常に追加された後に削除する必要があります。

関数ウォッチャー(vm, exp, cb) {
  this.cb = cb;
  this.vm = vm;
  this.exp = exp;
  this.value = this.get(); // サブスクライバーの操作に自分自身を追加します}

ウォッチャー.プロトタイプ = {
  更新: 関数() {
    これを実行してください。
  },
  実行: 関数() {
    var 値 = this.vm.data[this.exp];
    var oldVal = this.value;
    if (値 !== oldVal) {
      this.value = 値;
      this.cb.call(this.vm、値、oldVal);
    }
  },
  取得: 関数() {
    Dep.target = this; // ウォッチャーを追加するかどうかを決定するために自身をキャッシュします。
    var value = this.vm.data[this.exp]; // リスナーで get 関数を強制的に実行します。 Dep.target = null; // 自分自身を解放します。 return value;
  },
};

ここまでで、シンプルなWatcher設計が完了し、 ObserverWatcherを関連付けることで、シンプルな双方向バインディングを実装できます。

パーサーCompileまだ設計されていないため、最初にテンプレート データをハードコードできます。

コードを ES6 コンストラクターに変換してプレビューします。

https://jsrun.net/8SIKp/embed...

このコードはコンパイラを実装せず、バインドされた変数を直接渡すため、バインド用のノードにデータ ( name ) を設定し、ページ上でnew MyVueを実行して双方向バインドを実現します。

2 秒後に変更を加えます。ページも変更されたことがわかります。

// マイビュー
プロキシキー(キー) {
    var self = this;
    Object.defineProperty(this, キー, {
        列挙可能: false、
        設定可能: true、
        取得: 関数 proxyGetter() {
            self.data[キー]を返します。
        },
        設定: 関数 proxySetter(newVal) {
            self.data[キー] = newVal;
        }
    });
}

上記のコードの目的は、 this.dataのキーを this にプロキシして、 this.xxを使用してthis.data.xx簡単に取得できるようにすることです。

4. コンパイルを実装する

上記では双方向データバインディングを実装していますが、DOM ノードはプロセス全体で解析されるのではなく、固定的に置き換えられます。そのため、データを解析してバインドするにはパーサーが必要です。

パーサーcompileの実装手順:

  • テンプレート命令を解析し、テンプレート データを置き換えて、ビューを初期化します。
  • 対応する更新関数をテンプレートで指定されたノードにバインドし、対応するサブスクライバーを初期化します。

テンプレートを解析するには、まず DOM データを解析し、次に DOM 要素の対応する指示を処理する必要があります。したがって、DOM 操作全体は比較的頻繁に行われます。新しいフラグメントを作成し、必要な解析済みDOM fragmentに保存して処理することができます。

関数 nodeToFragment(el) {
  var フラグメント = document.createDocumentFragment();
  var child = el.firstChild;
  (子)の間{
    // Dom 要素をフラグメントに移動します。fragment.appendChild(child);
    子 = el.firstChild;
  }
  フラグメントを返します。
}

次に、各ノードをトラバースし、関連する命令とテンプレート構文を含むノードに対して特別な処理を実行する必要があります。まず、最も単純なテンプレート構文処理を実行し、正規表現を使用して「{{variable}}」の形式で構文を解析します。

関数compileElement(el){
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
        var reg = /\{\{(.*)\}\}/; // {{xx}} に一致
        var テキスト = node.textContent;
        if (self.isTextNode(node) && reg.test(text)) { // この形式の命令であるかどうかを判断します {{}} self.compileText(node, reg.exec(text)[1]);
        }
        (node.childNodes && node.childNodes.length)の場合{
            self.compileElement(node); // 子ノードを再帰的に走査し続けます}
    });
},
関数compileText(ノード, exp) {
    var self = this;
    var initText = this.vm[exp];
    updateText(node, initText); // 初期化されたデータをビューに初期化します。 new Watcher(this.vm, exp, function (value) { // サブスクライバーを生成し、更新関数をバインドします。 self.updateText(node, value);
    });
},
関数 updateText (ノード, 値) {
    node.textContent = typeof value == 'undefined' ? '' : value;
}

最も外側のノードを取得した後、 compileElement関数を呼び出してすべての子ノードを判断します。ノードがテキスト ノードであり、{{}} 形式の指示に一致する場合は、コンパイルして対応するパラメーターを初期化します。

次に、データが変更されたときに対応する DOM を更新するために、現在のパラメータに対応する更新関数サブスクライバーを生成する必要があります。

これで、解析、初期化、コンパイルの 3 つのプロセスが完了します。

次に、双方向データ バインディングにテンプレート変数を使用するように myVue を変更します。

https://jsrun.net/K4IKp/embed...

5. 解析イベントを追加する

compileを追加すると、双方向のデータバインディングが基本的に完了します。次のステップは、 Compilev-modelv-onv-bindなどの解析とコンパイルのための命令をさらに追加することです。

v-model と v-on 解析を追加します。

関数コンパイル(ノード) {
  var nodeAttrs = node.attributes;
  var self = this;
  Array.prototype.forEach.call(nodeAttrs, function(attr) {
    var attrName = attr.name;
    if (isDirective(attrName)) {
      var exp = attr.value;
      var dir = attrName.substring(2);
      if (isEventDirective(dir)) {
        // イベント命令 self.compileEvent(node, self.vm, exp, dir);
      } それ以外 {
        // v-model ディレクティブ self.compileModel(node, self.vm, exp, dir);
      }
      node.removeAttribute(attrName); // 解析が完了したので、属性を削除します}
  });
}
// v-ディレクティブ解析関数 isDirective(attr) {
  attr.indexOf("v-") == 0 を返します。
}
// on: ディレクティブ解析関数 isEventDirective(dir) {
  dir.indexOf("on:") === 0 を返します。
}

上記のcompile関数は、現在のdomのすべてのノード属性をトラバースし、属性がディレクティブ属性であるかどうかを判定するために使用されます。ディレクティブ属性である場合は、対応する処理が実行されます (イベントはイベントとして監視され、データはデータとして監視されます...)

6. myVueのフルバージョン

MyVuemountedメソッドを追加し、すべての操作が完了したらそれを実行します。

クラスMyVue {
  コンストラクタ(オプション) {
    var self = this;
    オプションデータ
    this.methods = オプション.methods;
    Object.keys(this.data).forEach(function(key) {
      self.proxyKeys(キー);
    });
    これを観察します。
    新しいコンパイル(options.el、this);
    options.mounted.call(this); //すべてが完了したらマウントされた関数を実行します}
  プロキシキー(キー) {
    // this.data プロパティを this にプロキシします。var self = this;
    Object.defineProperty(this, キー, {
      列挙可能: false、
      設定可能: true、
      取得: 関数 getter() {
        self.data[キー]を返します。
      },
      設定: 関数 setter(newVal) {
        self.data[キー] = newVal;
      },
    });
  }
}

その後、テストすることができます。

https://jsrun.net/Y4IKp/embed...

プロセスをまとめてみましょう。この図をもう一度見てみると、さらに明確になります。

コードアドレスを表示できます: Vue2.x 双方向バインディングの原則と実装

これで、 Vue2.xの双方向バインディングの原則と実装に関するこの記事は終了です。Vue データの双方向バインディングの原則に関するより関連性の高いコンテンツについては、123WORDPRESS.COM の以前の記事を検索するか、次の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • v-model 双方向バインディングデータを実装する vue カスタム コンポーネントのサンプル コード
  • フロントエンドフレームワーク Vue における親子コンポーネントデータの双方向バインディングの実装
  • Vue の双方向イベントバインディング v-model の原理についての簡単な説明
  • Vue2.0でデータの双方向バインディング機能をjsを使って実装する
  • 純粋な JS を使用して vue.js で双方向バインディング機能を実装する方法
  • Vue双方向バインディングの詳細な説明

<<:  MySQL スロークエリ関連パラメータの原理の分析

>>:  MySQLの大規模テーブル最適化ソリューションについての簡単な説明

推薦する

MySQLでよく使われる4つのストレージエンジンについて簡単に説明します。

よく使われる4つのMySQLエンジンの紹介(1):MyISAMストレージエンジン:トランザクションや...

MySQL 8.0.13 のインストールと設定のグラフィックチュートリアル

Msyqlデータベースのインストール、参考までに具体的な内容は次のとおりです。 ①ブラウザでhttp...

Mysqlデータベースの文字化けに対処する方法

MySQL では、データベースの文字化けは一般的に文字セットを設定することで修正できますが、文字化け...

Linux インストール MongoDB の起動と一般的な問題の解決

MongoDB のインストール プロセスと問題記録1. MongoDBのインストールMongoDBを...

DockerでRedisをデプロイして起動する方法

DockerでRedisをデプロイするまずLinuxにDockerをインストールし、次にDocker...

Vueはページの部分的なリフレッシュを実装します(ルータビューのページリフレッシュ)

Vue でprovide+inject組み合わせを使用するまず、App.vue を変更する必要があ...

Vue での mixin の応用について議論する

Mixin は、再利用可能な機能を Vue コンポーネント間で分散する非常に柔軟な方法を提供します。...

Linux で Spring Boot プロジェクトを開始および停止するためのスクリプトの例

Springboot プロジェクトを開始するには、次の 3 つの方法があります。 1. メインメソッ...

デザインにおけるユーザーエクスペリエンスの背後にある8つのユーザー本能について話す

編集者注: この記事は、Teambition チームの @娄昊川 が寄稿したものです。Teambit...

UDP シンプル サーバー クライアント コード例

UDP の理論については詳しく説明しません。UDP に関する HelloWorld プログラムを紹介...

XHTML 入門チュートリアル: シンプルな Web ページの作成

1 分で最初の Web ページを作成します。簡単な Web ページを作ってみましょう。ぜひフォローし...

フロントエンドJSサンドボックスを実装するいくつかの方法についての簡単な説明

目次序文iframeはサンドボックスを実装しますdiffメソッドを使用したサンドボックスの実装プロキ...

MySQL/MariaDB でピボット テーブルを実装する方法のサンプル コード

前回の記事では、Oracle でピボット テーブルを実装するいくつかの方法を紹介しました。今日は、同...

MySQL inndbジョイントインデックスを正しく使用する方法を徹底的に理解するためのケーススタディ

最近確認された5件のデータを照会するビジネスがあります。 `id`、`title` を選択 `th_...