双方向バインディングを実装するための vuejs の原則: データ ハイジャックとパブリッシュ/サブスクライブ モードを使用して、「Object.defineProperty()」を通じて各プロパティのセッターとゲッターをハイジャックし、データが取得されたときにサブスクライバーにメッセージをパブリッシュします。ビューを更新するために、対応するリスニング コールバックが使用されます。
このチュートリアルの動作環境: Windows7 システム、vue2.9.6 バージョン、DELL G3 コンピューター。
Vue は主に、Object.defineProperty()
を使用したデータ ハイジャックとパブリッシュ/サブスクライブ モードを使用して双方向データ バインディングを実装します。メソッド データ ハイジャックを実行し、すべてのオブザーバーに通知するようにパブリッシャー (トピック オブジェクト) に通知します。オブザーバーが通知を受信した後、ビューが更新されます。
https://jsrun.net/RMIKp/embedded/all/light
MVVM フレームワークには主に 2 つの側面が含まれています。データの変更によるビューの更新と、ビューの変更によるデータの更新です。
ビューの変更によりデータが更新されます。入力のようなラベルの場合は、oninput イベントを使用できます。
データの変更によりビューが更新され、Object のセットを使用できます。 definProperty()
このメソッドはデータの変更を検出できます。データが変更されると、この関数がトリガーされ、ビューが更新されます。
双方向バインディングの実装方法はわかっています。まず、データをハイジャックして監視する必要があるため、すべてのプロパティの変更を監視するオブザーバー関数を設定する必要があります。 。
属性が変更された場合は、データを更新する必要があるかどうかをサブスクライバー ウォッチャーに指示する必要があります。複数のサブスクライバーがある場合は、これらのサブスクライバーを収集する Dep が必要であり、リスナー オブザーバーとリスナー オブザーバーの間でウォッチャーの一元管理。
監視する必要があるノードと属性をスキャンして解析するためのコマンド パーサー、コンパイルも必要です。
つまり、プロセスは大まかに次のようになります。
リスナー オブザーバーを実装して、すべてのプロパティをハイジャックして監視し、変更が発生した場合にサブスクライバーに通知します。
サブスクライバ ウォッチャーを実装します。プロパティ変更の通知を受信したら、対応する関数を実行してビューを更新し、Dep を使用してこれらのウォッチャーを収集します。
パーサー Compile を実装します。これは、ノードの関連命令をスキャンして解析し、初期化テンプレートに従って対応するサブスクライバーを初期化するために使用されます。
オブザーバーはデータ リスナーであり、中心的なメソッドは Object.defineProperty( )
監視のためにすべてのプロパティに setter メソッドと getter メソッドを再帰的に追加します。
var library = { book1: { name: "", }, book2: "", }; observe(library); library.book1.name = "vue权威指南"; // 属性name已经被监听了,现在值为:“vue权威指南” library.book2 = "没有此书籍"; // 属性book2已经被监听了,现在值为:“没有此书籍” // 为数据添加检测 function defineReactive(data, key, val) { observe(val); // 递归遍历所有子属性 let dep = new Dep(); // 新建一个dep Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { if (Dep.target) { // 判断是否需要添加订阅者,仅第一次需要添加,之后就不用了,详细看Watcher函数 dep.addSub(Dep.target); // 添加一个订阅者 } return val; }, set: function(newVal) { if (val == newVal) return; // 如果值未发生改变就return val = newVal; console.log( "属性" + key + "已经被监听了,现在值为:“" + newVal.toString() + "”" ); dep.notify(); // 如果数据发生变化,就通知所有的订阅者。 }, }); } // 监听对象的所有属性 function observe(data) { if (!data || typeof data !== "object") { return; // 如果不是对象就return } Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); } // Dep 负责收集订阅者,当属性发生变化时,触发更新函数。 function Dep() { this.subs = {}; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach((sub) => sub.update()); }, };
アイデア分析では、加入者を収容できる加入者メッセージ加入者 Dep が必要です。加入者を収集し、属性が変化したときに対応する更新関数を実行するために使用されます。
コードの観点から見ると、サブスクライバ Dep をゲッターに追加することは、初期化時にウォッチャーをトリガーすることになるため、サブスクライバが必要かどうかを判断する必要があります。
セッターでは、データが変更されるとすべてのサブスクライバーに通知され、サブスクライバーは対応する関数を更新します。
この時点で、比較的完成したオブザーバーが完成しました。次に、ウォッチャーの設計を開始します。
サブスクライバー ウォッチャーは、初期化中にそれ自体を追加する必要があります。サブスクライバ Dep の場合、オブザーバは取得時に実行されるウォッチャー操作であることはすでにわかっているため、ウォッチャーが初期化されるときに対応する取得関数をトリガーして、対応するサブスクライバー操作を追加するだけで済みます。
get をトリガーするにはどうすればよいですか?すでに Object.defineProperty() を設定しているため、これをトリガーするには、対応するプロパティ値を取得するだけで済みます。
サブスクライバ Watcher が初期化されるときにサブスクライバを Dep.target にキャッシュし、追加が成功した後にサブスクライバを削除するだけで済みます。
function Watcher(vm, exp, cb) { this.cb = cb; this.vm = vm; this.exp = exp; this.value = this.get(); // 将自己添加到订阅器的操作 } Watcher.prototype = { update: function() { this.run(); }, run: function() { var value = this.vm.data[this.exp]; var oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } }, get: function() { Dep.target = this; // 缓存自己,用于判断是否添加watcher。 var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数 Dep.target = null; // 释放自己 return value; }, };
ここまでで、単純な Watcher の設計が完了しました。次に、Observer と Watcher を関連付けることで、単純な双方向バインディングを実現できます。
パーサー Compile はまだ設計されていないため、最初にテンプレート データをハードコーディングできます。
コードを ES6 コンストラクター記述メソッドに変換し、プレビューして試してください。
https://jsrun.net/8SIKp/embedded/all/light
このコードはコンパイラを実装せず、バインドされた変数を直接渡します。データ (名前) を設定するだけです。バインド用のノード上で新しい MyVue を実行し、双方向バインディングを実現します。
そして 2 秒後に値を変更すると、ページも変更されたことがわかります。
// MyVue proxyKeys(key) { var self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get: function proxyGetter() { return self.data[key]; }, set: function proxySetter(newVal) { self.data[key] = newVal; } }); }
上記のコードの機能は、this.xx を使用して this.data.xx を簡単に取得できるように、this.data のキーを this にプロキシすることです。
上記では双方向データ バインディングが実装されていますが、プロセス全体では DOM セクション ストアが解析されず、固定的に置き換えられるため、次のステップでは、次のステップでパーサーを実装します。データの解析とバインド作業を行います。
パーサーコンパイルの実装手順:
テンプレート命令を解析し、テンプレートデータを置換し、ビューを初期化します。
将模板指定对应的节点绑定对应的更新函数,初始化相应的订阅器。
为了解析模板,首先需要解析 DOM 数据,然后对含有 DOM 元素上的对应指令进行处理,因此整个 DOM 操作较为频繁,可以新建一个 fragment 片段,将需要的解析的 DOM 存入 fragment 片段中在进行处理。
function nodeToFragment(el) { var fragment = document.createDocumentFragment(); var child = el.firstChild; while (child) { // 将Dom元素移入fragment中 fragment.appendChild(child); child = el.firstChild; } return fragment; }
接下来需要遍历各个节点,对含有相关指令和模板语法的节点进行特殊处理,先进行最简单模板语法处理,使用正则解析“{{变量}}”这种形式的语法。
function compileElement (el) { var childNodes = el.childNodes; var self = this; [].slice.call(childNodes).forEach(function(node) { var reg = /\{\{(.*)\}\}/; // 匹配{{xx}} var text = node.textContent; if (self.isTextNode(node) && reg.test(text)) { // 判断是否是符合这种形式{{}}的指令 self.compileText(node, reg.exec(text)[1]); } if (node.childNodes && node.childNodes.length) { self.compileElement(node); // 继续递归遍历子节点 } }); }, function compileText (node, exp) { var self = this; var initText = this.vm[exp]; updateText(node, initText); // 将初始化的数据初始化到视图中 new Watcher(this.vm, exp, function (value) { // 生成订阅器并绑定更新函数 self.updateText(node, value); }); }, function updateText (node, value) { node.textContent = typeof value == 'undefined' ? '' : value; }
获取到最外层的节点后,调用 compileElement 函数,对所有的子节点进行判断,如果节点是文本节点切匹配{{}}这种形式的指令,则进行编译处理,初始化对应的参数。
然后需要对当前参数生成一个对应的更新函数订阅器,在数据发生变化时更新对应的 DOM。
这样就完成了解析、初始化、编译三个过程了。
接下来改造一个 myVue 就可以使用模板变量进行双向数据绑定了。
https://jsrun.net/K4IKp/embedded/all/light
添加完 compile 之后,一个数据双向绑定就基本完成了,接下来就是在 Compile 中添加更多指令的解析编译,比如 v-model、v-on、v-bind 等。
添加一个 v-model 和 v-on 解析:
function compile(node) { 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); } else { // v-model 指令 self.compileModel(node, self.vm, exp, dir); } node.removeAttribute(attrName); // 解析完毕,移除属性 } }); } // v-指令解析 function isDirective(attr) { return attr.indexOf("v-") == 0; } // on: 指令解析 function isEventDirective(dir) { return dir.indexOf("on:") === 0; }
上面的 compile 函数是用于遍历当前 dom 的所有节点属性,然后判断属性是否是指令属性,如果是在做对应的处理(事件就去监听事件、数据就去监听数据..)
在 MyVue 中添加 mounted 方法,在所有操作都做完时执行。
class MyVue { constructor(options) { var self = this; this.data = options.data; this.methods = options.methods; Object.keys(this.data).forEach(function(key) { self.proxyKeys(key); }); observe(this.data); new Compile(options.el, this); options.mounted.call(this); // 所有事情处理好后执行mounted函数 } proxyKeys(key) { // 将this.data属性代理到this上 var self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get: function getter() { return self.data[key]; }, set: function setter(newVal) { self.data[key] = newVal; }, }); } }
然后就可以测试使用了。
https://jsrun.net/Y4IKp/embedded/all/light
总结一下流程,回头在哪看一遍这个图,是不是清楚很多了。
可以查看的代码地址:Vue2.x 的双向绑定原理及实现
相关推荐:《vue.js教程》
以上がvuejs で双方向バインディングを実装する原理は何ですかの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。