序文
合格
- 命令パーサーの実装 コンパイル
- データリスナーの実装 Observer
- ウォッチャーの実装
Vue全体でMVVMのレスポンシブ原則を実装するために
この記事では、命令パーサーCompileの実装について説明します。
関数の実装は非常に完璧ではありません、あなたが興味を持っている場合は、自分自身を追加することができます、このコードは、主にプロセスの全体の応答性の原則を完了するために、オブザーバを理解することです、コンパイル、ウォッチャーは、何に使用され、どのようにこれらの3つを渡すには、全体のMVVMアーキテクチャへのブリッジを構築するように、Vueの応答性の原則を達成するために。
テストとして簡単なhtmlを作成します。
<div id="app">
<div>{{person.name}} -- {{person.age}}</div>
<div>{{person.fav}}</div>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<div v-html="htmlStr" v-bind:style="{backgroundColor:red}"></div>
<div v-text="person.fav"></div>
<!-- <input type="text" v-bind:value='msg'> -->
<input type="text" v-model='msg'>
<div>{{msg}}</div>
<h2 v-text="tip" v-on:click="clickMe"></h2>
</div>
以下のスクリプトタグで、Vueと同じようにVueオブジェクトを作成します。
const Foo = new MVue({
'root': '#app',
'data': {
'bar': {
'name': 'GHkmmm',
'c': '21',
'b': ' '
},
'foo': 'Vueの双方向バインディング原則の理解',
'a': ' ',
'Obj': 'hello\x20world!'
},
'methods': {
'Bar'() {
console.log('click');
}
}
});MVue クラスの作成
受信パラメーターの受信に使用
class MVue{
constructor(options){
this.$options = options;
this.$el = options.el;
this.$data = options.data;
if(this.$el){
// 命令パーサの実装
new Compile(this.$el, this);
}
}
}命令パーサーの実装 コンパイル
class Compile{
constructor(el, vm){
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
/*
ページの内容をコンパイルする場合、オブジェクトを繰り返し変更する必要があるため、各オブジェクトを変更することはできません。
その代わりに、ドキュメントフラグメントを使用して変更を統一し、フラグメントを使用してページ全体を更新します。
*/
// 1.ドキュメントフラグメントオブジェクトをメモリに取得し、ページのリフローと再描画を削減します。
const fragment = this.nodeFragment(this.el);
// 2.コンパイルテンプレート
this.compile(fragment);
// 3.ルート要素に子要素を追加
this.el.append(fragment);
}
}ドキュメント・フラグメントの取得
子ノードを繰り返し処理し、ドキュメント・フラグメント・オブジェクトに追加します。
nodeFragment(node){
// ドキュメント断片化オブジェクトの作成
const f = document.createDocumentFragment();
let firstChild;
// <div id="app">の下の子ノード
の子ノードです。.firstChild){
firstChild = node.firstChild;
// トラバースされたノードをドキュメントフラグメントオブジェクトに追加します。
f.append(firstChild);
}
// ドキュメントフラグメントオブジェクトを返します
return f;
}コンパイルテンプレート
フラグメントを渡した後、フラグメント内のノードをトラバースして、それが要素ノードなのかテキストノードなのかを判断する必要があります。
compile(fragment){
// 取得したドキュメントの断片を渡します。
// 1.子ノードの取得
const childNodes = fragment.childNodes;
// 子ノードの配列を繰り返し処理します。
[...childNodes].forEach(child => {
/*
isElementNode(el){
return el.nodeType === 1;
}
*/
if(this.isElementNode(child)){
// 要素ノード
// console.log('要素ノード', child);
this.compileElement(child);
}else{
// 非要素ノード
// console.log('テキストノード',child);
this.compileText(child);
}
// 子ノードの下に子ノードがあるかどうかを判断し、あれば再帰します。
if(child.childNodes && child.childNodes.length){
this.compile(child);
}
})
}コンパイル要素ノード
compileElement(node){
// ノードのすべての属性を取得
const attributes = node.attributes;
// プロパティの配列に対する反復処理
[...attributes].forEach(attr => {
// attrのname属性とvalue属性を別々に抽出するには、分解された代入を使用します。
// ここでのnameとvalueはattrの属性に対応しており、変更することはできません。
// name: v-text,v-model,v-bind....
// vaule: v-text,v-model..の後に値が続きます。
const { name, value } = attr;
/*
名前が'v'で始まるかどうかの判定-'が先頭にあればコンパイルされ、なければ処理されません。
isDirective{
return attrName.startsWith('v-');
}
*/
if(this.isDirective(name)){
// -'スプリット名、spliteメソッドは配列を返し、最初のパラメータは使用されず、2番目のパラメータはディレクティブに割り当てられます。
// ここでのディレクティブは上のものとは違うので、自分で名前をつけてください。
const [,directive] = name.split('-');
// なぜなら、指導は-textこれは、おそらく-on:clickこの種類は、さらに分割する必要があります。
const [dirName,eventName] = directive.split(':');
// データの更新 データ駆動型ビュー
// dirNameに応じて、compileUtilの異なるメソッドを呼び出します。
// dirName本文を参照,html,bind,on...
complileUtil[dirName](node, value, this.vm, eventName)
// 指示ラベルの属性削除
node.removeAttribute(name);
}
})
}テキストノードのコンパイル
compileText(node){
// テキストコンテンツの取得
const content = node.textContent;
if(/\{\{(.+?)\}\}/.test(content)){
complileUtil['text'](node,content,this.vm)
}
}CompileUtil オブジェクト
異なる命令を処理するための内部実装メソッド
const complileUtil = {
getVal(expr, vm){
/*
exprメッセージかもしれないし、人かもしれません。.nameですから、もう一度exprを分割する必要があります。
分割後は次のような配列になります。['person', 'name']reduceメソッド使用後
最初にvmを返したとき.$data['person']
2度目のVM復帰.$data['person']['name']
この場合も、msg なら配列['msg']つまり、そのままvmに戻るだけです.$data['msg']
*/
return expr.split('.').reduce((data, currentVal) => {
// console.log(data);
// console.log(currentVal);
return data[currentVal];
}, vm.$data//初期値)
},
setVal(expr, vm, inputVal){
return expr.split('.').reduce((data, currentVal) => {
data[currentVal] = inputVal;
}, vm.$data)
},
// -textディレクティブとテキストノードのための mustache 構文{{msg}}
text(node, expr, vm){
let value;
// で始めるかどうかを決定します。{{'を先頭に
if(expr.indexOf("{{")!==-1){
// 二重中括弧を正規表現でマッチさせ、中括弧内の文字列を抽出します。
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// vmのgetValメソッドを渡します。.$dataデータ検索
return this.getVal(args[1], vm);
})
}else{
value = this.getVal(expr, vm);
}
// ビューの更新
this.updater.textUpdater(node, value);
},
// -htmlディレクティブ
html{
const value = this.getVal(expr, vm);
this.updater.htmlUpdater(node, value);
},
// -modelインストラクション
モデル{
const value = this.getVal(expr, vm);
// => =>
// 入力イベントのリスニング
node.addEventListener('input', (e)=>{
this.setVal(expr, vm, e.target.value);
})
this.updater.modelUpdater(node, value);
},
// -on命令
オン{
let fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false)
},
// -modelコマンド
バインド{
...//自分でできる実装方法は以下の通りです。
},
//機能更新
updater: {
textUpdater(node, value){
node.textContent = value;
},
htmlUpdater(node, value){
node.innerHTML = value;
},
modelUpdater(node, value){
node.value = value;
},
bindUpdater(node, attrName, value){
node[attrName] = value
}
}
}データリスナーの実装 Observer
オブザーバークラスの作成
は英語の -ity、-ism、-ization に対応します。
Object.defineProperty を使用して、データ内のすべてのプロパティのゲッターとセッターを設定します。
class Observer{
constructor(data){
this.observer(data);
}
observer(data){
if(data && typeof data === 'object'){
// データオブジェクトの反復処理
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
})
}
}
defineReactive(data, key, value){
// 再帰的走査
this.observer(value);
const dep = new Dep();
// すべてのプロパティをリッスン、ハイジャック
// definePropertyデータを渡すと、キーがあります。[key]対応するゲッターとセッターを追加します。
Object.defineProperty(data, key, {
enumerable: true,
configurable: false, //プロパティが設定されていて、削除可能かどうかを記述します。
get(){
// この関数は、プロパティがアクセスされたときに呼び出されます。
//
// サブスクリプションデータの変更時にDepするオブザーバの追加
Dep.target && dep.addSub(Dep.target);
return value;
},
// この関数は、属性の値が変更されたときに呼び出されます。
// ここでは矢印関数を使用し、Objectではなく上位を指すようにしています。
set: (newVal) => {
this.observer(newVal)
if(newVal !== value){
value = newVal;
}
//デップに変更を通知
dep.notify();
}
});
}
}MVueクラスに実装
class MVue{
constructor(options){
this.$options = options;
this.$el = options.el;
this.$data = options.data;
if(this.$el){
// データオブザーバーの実装
new Observer(this.$data);
// 命令パーサの実装
new Compile(this.$el, this);
}
}
}依存関係コレクターの実装
Depクラスの作成
は英語の -ity、-ism、-ization に対応します。
- オブザーバーの収集
- Observerのハイジャックされたデータが変更された場合、Depは対応するObserverに通知するように通知されます。
class Dep{
constructor() {
this.subs = [];
}
// オブザーバの収集
addSub{
this.subs.push(watcher);
}
// オブザーバーに更新を通知
notify(){
this.subs.forEach(w => w.update())
}
}ビューを更新するWatcherの実装
Watcherクラスの作成
class Watcher{
constructor(vm, expr, callback){
“this”.vm = vm;
“this”.expr = expr;
“this”.callback = callback;
“this”.oldVal = “this”.getOldVal()
}
getOldVal(){
Dep.target = “this”;
const oldVal = complileUtil.getVal(“this”.expr, “this”.vm)
Dep.target = null;
return oldVal;
}
update(){
const newVal = complileUtil.getVal(“this”.expr, “this”.vm);
if(newVal !== “this”.oldVal){
“this”.callback(newVal);
}
}
}
命令を処理する compileUtil のすべてのメソッドで Watcher を実装します。
new Watcher(vm, expr, callback);
const complileUtil = {
getContentVal(expr, vm){
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(args[1], vm)
})
},
text(node, expr, vm){
let value;
if(expr.indexOf("{{")!==-1){
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], ()=>{
this.updater.textUpdater(node, this.getContentVal(expr,vm));
})
return this.getVal(args[1], vm);
})
}else{
value = this.getVal(expr, vm);
}
this.updater.textUpdater(node, value);
},
html(node, expr, vm){
const value = this.getVal(expr, vm);
// 将来データを更新するコールバックをトリガーするウォッチャーにバインドします。
新しいウォッチャー=>{
this.updater.htmlUpdater(node, newVal);
})
this.updater.htmlUpdater(node, value);
},
...
}ショーケース
この時点で、VueのMVVM応答性全体が実装され、データの双方向バインディングも実装されています。
概要
vueは、パブリッシャーサブスクライバーパターンでデータハイジャックを使用することです。Object.defineProperty();を通じて、各プロパティのセッターとゲッターをハイジャック、データが変更されると、依存関係コレクターDepにメッセージをリリースし、オブザーバに通知し、対応するコールバック関数がビューを更新するようにします。





