blog

Vue2.x-ソース学習 - シンプルなレスポンシブ・フレームワークの実装

これらのプロパティをすべて変換するために使用します。 はシムできないES5の機能で、これがVueがIE8以下のブラウザをサポートしない理由です。 オブジェクト ......

Oct 29, 2020 · 9 min. read
シェア

基本概念

データドライバー

データのレスポンシブ
  • データモデルは単なるjsオブジェクトです。
  • データが変更されるとビューが更新されます
  • 煩雑なDOM操作を回避し、開発効率を向上
双方向バインディング
  • データの変更、ビューの変更
  • ビューの変更、データの変更
  • 双方向データバインディングを作成するための V モデル構文シュガー
データドライバー
  • どのようにビューにレンダリングされるかではなく、データそのものにのみ関心があります。

vue2.xレスポンシブ・コアの原則

  • 通常のJavaScriptオブジェクトをデータオプションとしてVueインスタンスに渡すと、Vueはこのオブジェクトのすべてのプロパティを繰り返し処理します。
  • Object.definePropertyこれらのプロパティをすべてゲッター/セッターに変換するために使用します。
  • Object.definePropertyはシムできないES5の機能であるため、VueはIE8以下のブラウザをサポートしていません。
let data = {
 msg: 'hello'
};
let vm = {};
Object.defineProperty(vm, 'msg', {
 enumerable: true,
 configurable: true,
 get() {
 console.log('get---', data.msg);
 return data.msg;
 },
 set(val) {
 console.log('set---', val);
 if (val === data.msg) {
 return;
 }
 data.msg = val;
 document.getElementById('app').textContent = data.msg;
 }
})
//  
vm.msg = 'tctj';
console.log(vm.msg)
  • 複数のプロパティのゲッター/セッターを変換する方法
let data = {
 msg: 'hello',
 count: 10
};
...
function proxyData(data) {
 Object.keys(data).forEach((key) => {
 Object.defineProperty(vm, key, {
 enumerable: true,
 configurable: true,
 get() {
 console.log('get---', key, data[key]);
 return data[key];
 },
 set(val) {
 console.log('set---', val);
 if (val === data[key]) {
 return;
 }
 data[key] = val;
 document.getElementById('app').textContent = data[key];
 }
 })
 });
}

Object. defineProperty

  • Object.defineProperty()ネイティブでアレイをレスポンスよくリッスンできない
  • 再帰は、実装中に深くネストされたデータに対して多くのパフォーマンスを消費します。
  • Object.defineProperty()この方法では、インデックスを直接設定し、値を変更するビューの更新によってトリガされませんを介して、配列などの属性要素のその後の手動追加および削除を聞く方法はありません

vue3.xレスポンシブ・コアの原則

プロキシは、特定の操作のデフォルトの振る舞いを変更するために使用されます。これは、言語レベルで変更を加えることと同じなので、一種の「メタプログラミング」、つまりプログラミング言語のプログラミングに属します。プロキシは、対象となるオブジェクトに "ブロッキング "のレイヤーを設定し、外部からのオブジェクトへのアクセスは、まずこのブロッキングのレイヤーを通過する必要があるため、外部へのアクセスをフィルタリングして書き換えるメカニズムを提供するものとして理解することができます。

  • プロキシを使ったproxy
  • Proxy オブジェクト全体のプロキシ
let data = {
 msg: 'hello',
 count: 10
};
let vm = new Proxy(data, {
 get(target, key) {
 console.log('get---', key, target[key]);
 return target[key];
 },
 set(target, key, val) {
 console.log('set---', key, val);
 if (target[key]=== val) {
 return;
 }
 target[key] = val;
 document.getElementById('app').textContent = target[key];
 }
})
vm.msg = 'tctj';
console.log(vm.msg);

サブスクリプションモデルの発行

  • サブスクライバー
  • パブリッシャー
  • シグナル・センター
class EventEmitter { constructor() { this.subs = Object.create(null); } // イベントを登録する $on(eventType, handler) { this.subs[eventType] = this.subs[eventType] || []; this.subs[eventType].push(handler); } // イベントをトリガーする $emit(eventType) { if (this.subs[eventType]) { this.subs[eventType].forEach((handler) => { handler(); }); } } } // let bus = new EventEmitter(); bus.$on('custom-click', () => { console.log('on --- custom-click 1 ---'); }); bus.$on('custom-click', () => { console.log('on --- custom-click 2 ---'); }); bus.$emit('custom-click')

ウォッチャーモデル

  • ウォッチャー - Watcher
    • update: イベントが発生したときに特別に委譲されるもの
  • ターゲット - Dep
    • subs 配列: すべてのオブザーバを格納します。
    • addSub()ウォッチャーの追加
    • notify(): イベントが発生したら、すべてのオブザーバーに対して update() をコールします。

Vue

機能
  • 初期化パラメータの受け取りを担当
  • データ内のすべての属性の変更をリッスンするオブザーバの呼び出しを担当します。
  • 命令/差分式を解析するためにコンパイラを呼び出す役割を担当。
構造
  • オプション
  • $options
  • $el
  • _proxyData()
コード
class Vue {
 constructor(options) {
 // 1. プロパティでオプション・データを保存する
 this.$options = options || {};
 this.$data = options.data || {};
 this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
 // 2. データ内のメンバをgetter/setterに変換してvueインスタンスに注入する
 this._proxyData(this.$data);
 // 3. オブザーバー・オブジェクトを呼び出し、データの変更をリッスンする。
 // 4. コンパイラー・オブジェクトを呼び出し、コマンドと差分式を解析する
 }
 _proxyData(data) {
 // データ内の属性を反復処理する
 Object.keys(data).forEach(key => {
 // データのすべての属性をvuewインスタンスに注入する
 Object.defineProperty(this, key, {
 enumerable: true,
 configurable: true,
 get() {
 return data[key]
 },
 set(newValue) {
 if (newValue === data[key]) {
 return;
 }
 data[key] = newValue;
 }
 });
 });
 }
}
export default Vue;

Observer

機能
  • _proxyData()
  • データ内の属性もオブジェクトであり、再帰的な呼び出しによって、その属性のすべての属性をレスポンシブ・データに変換します。
  • データ変更の通知を送信
構造
  • walk(データ)
  • walk(data)

Observer-defineReactive

プロパティへのアクセス
  • defineReactive(data, key, value)
  • オブザーバの defineReactive で定義された get() メソッドをトリガします。
  • defineReactiveのgetメソッドがdata[key]を返すと、再帰呼び出しが発生し、オーバーフローを引き起こします。
  • defineReactiveのgetは、valのスコープを拡張するクロージャです。
コード
class Observer {
 constructor(data) {
 this.walk(data)
 }
 walk(data) {
 // 1. データがオブジェクトかどうかを判断する
 if (!data || typeof data !== 'object') {
 return;
 }
 // 2. データのすべての属性を繰り返し処理する
 Object.keys(data).forEach(key => {
 this.defineReactive(data, key, data[key]);
 });
 }
 defineReactive(obj, key, val) {
 const me = this;
 // valがオブジェクトの場合、再帰的な呼び出しでvalの属性もレスポンシブデータに変換する
 this.walk(val);
 Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get() {
 return val;
 },
 set(newValue) {
 if (newValue === val) {
 return;
 }
 val = newValue;
 // 後から手動でプロパティを追加しても、そのプロパティをリッスンできない問題を解決する
 me.walk(val);
 // TODO: 通知
 }
 })
 }
};
export default Observer;
  • vueの初期化で呼び出されます。
import Observer from './observer'; class Vue { constructor(options) { ... // 3. オブザーバー・オブジェクトを呼び出し、データの変更をリッスンする。 new Observer(this.$data); ... } }

Compiler

機能
  • vueの初期化で呼び出されます
  • ページの最初のレンダリングを担当
  • データが変更された場合、ビューを再レンダリングします。
構造
  • エル
  • el
  • vm
  • compile(el)
  • compileElement(node)
  • isDirective(attrName)
  • isTextNode(node)
  • isElementNode(node)
具体的な処理
  • isElementNode(node)
  • 最初のレンダリングの初期化時に Compile() が実行されます。
    • コンパイル・メソッドのすべての子ノードを繰り返し処理します。
      • テキスト・ノードであれば、compileText() が実行されます。
        • compileText では、regular を使用し、キーにマッチし、値を置換します。
      • 要素ノードである場合は、compileElement() が実行されます。
        • compileElement では、要素の属性を繰り返し処理し、ディレクティブ属性の場合はコンパイルを更新します。
コード
class Compiler { constructor(vm) { this.el = vm.$el; this.vm = vm; this.compile(this.el); } // テンプレートのコンパイル、テキスト・ノード要素ノードの処理 compile(el) { let childNodes = el.childNodes; Array.from(childNodes).forEach(node => { if (this.isTextNode(node)) { // テキスト・ノードを扱う this.compileText(node); } else if (this.isElementNode(node)) { // 要素ノードを扱う this.compileElement(node); } // 子ノードがあるかどうかを判断し、再帰的にコンパイルを呼び出す if (node.childNodes && node.childNodes.length) { this.compile(node); } }); } // 要素ノードをコンパイルし、ディレクティブを処理する compileElement(node) { Array.from(node.attributes).forEach(attr => { // ディレクティブかどうかを判断する let attrName = attr.name; if (this.isDirective(attrName)) { // v-text -> text attrName = attrName.substr(2); let key = attr.value; this.update(node, key, attrName); } }); } // update(node, key, attrName) { const updateFn = this[attrName + 'Updater']; updateFn && updateFn.call(this, node, this.vm[key]); } // v-textディレクティブを扱う textUpdater(node, value) { node.textContent = value; } // Vモデルのディレクティブを処理する modelUpdater(node, value) { node.value = value; } // テキストノードをコンパイルし、差分式を処理する compileText(node) { // console.dir(node) // 通常のマッチング {{xx}} let reg = /\{\{(.+?)\}\}/ig; let value = node.textContent; if (reg.test(value)) { let key = RegExp.$1.trim(); node.textContent = value.replace(reg, this.vm[key]); } } // 要素属性がディレクティブかどうかを判断する isDirective(attrName) { return attrName.startsWith('v-') } // ノードがテキスト・ノードかどうかを判断する isTextNode(node) { return node.nodeType === 3; } // 要素ノードかどうかを判断する isElementNode(node) { return node.nodeType === 1; } }; export default Compiler;

Dep

機能
  • 依存関係の収集、オブザーバの追加
  • すべてのオブザーバーに通知
構造
  • すべてのウォッチャーに通知
  • addSub(sub)
  • addSub(sub)
コード
class Dep {
 constructor() {
 // すべてのウォッチャーを保存する
 this.subs = [];
 }
 // ウォッチャーを追加する
 addSub(sub) {
 if (sub && sub.update) {
 this.subs.push(sub);
 }
 }
 // 通知を送る
 notify() {
 this.subs.forEach(sub => {
 sub.update();
 });
 }
};
export default Dep;
  • notify()
// observer.js import Dep from './dep'; class Observer { ... defineReactive(obj, key, val) { ... const dep = new Dep(); ... Object.defineProperty(obj, key, { ... get() { // 依存関係を収集する Dep.target && dep.addSub(Dep.target); ... } set() { ... // データを更新し、通知を送る dep.notify(); } } } }

Watcher

機能
  • データ変更が依存関係をトリガーすると、depはすべてのWatcherインスタンスにビューを更新するよう通知します。
  • selfインスタンスが変更されたときのdepオブジェクトへのselfの追加
構造
  • ブイエム
  • vm
  • cb
  • cb
  • oldValue
コード
import Dep from "./dep";
class Watcher {
 constructor(vm, key, cb) {
 this.vm = vm;
 // コールバック関数 - ビューの更新を担当する
 this.cb = cb;
 // datavモデルの属性名は
 this.key = key;
 // 1. ウォッチャー・オブジェクトをDepクラスの静的プロパティtargetに記録する
 Dep.target = this;
 // 2. getをトリガーし、addSubを呼び出して依存関係を収集する
 this.oldValue = vm[key];
 // 3. 収集後、空にする
 Dep.target = null;
 }
 // データが変更されたらビューを更新する
 update() {
 let newValue = this.vm[this.key];
 if (this.oldValue === newValue) {
 return;
 }
 this.cb(newValue);
 }
};
export default Watcher;
  • update()
// compiler.js
import Watcher from './watcher';
class Compiler {
 ...
 textUpdater(node, key, value) {
 node.textContent = value;
 new Watcher(this.vm, key, newValue => {
 node.textContent = newValue
 });
 }
 modelUpdater(node, key, value) {
 node.value = value;
 new Watcher(this.vm, key, newValue => {
 node.value = newValue
 });
 }
 compileText(node) {
 ...
 if (reg.test(value)) {
 let key = RegExp.$1.trim();
 node.textContent = value.replace(reg, this.vm[key]);
 // データが変更されたときにビューを更新するウォッチャー・オブジェクトを作成する
 new Watcher(this.vm, key, (newValue) => {
 node.textContent = newValue;
 });
 }
 }
}

双方向バインディング

  • Vモデルの双方向バインディングの処理
// compiler.js ... modelUpdater() { ... // 双方向バインディング node.addEventListener('input', ev => { this.vm[key] = node.value; }); }
Read next

Vue Composition APIの落とし穴

React Hooksの登場以来、批判も多く、従来のClassの書き方とは異なり、実行順序に依存するため、かなりわかりにくく、精神的に負担がかかるという声も多く聞かれます。これに対して、Vue3のAPI RFCでは、Vue3の公式な記述であるCom...

Oct 29, 2020 · 7 min read