Vue.js データ双方向バインディングの実装

序文

vue を使用すると、データが変更されるとインターフェイスも更新されますが、これは当然のことではありません. データを変更する場合、vue はデータの変更をどのように監視し、データが変更されたときに vue はどのようにインターフェイスを更新しますか? ?の?

データを変更すると、vue はObject.definePropertyes5 のメソッドを通じてデータの変更を監視します. データが変更发布订阅模式されると、統計サブスクライバー インターフェイスを通じて更新されます. これは設計パターンです.

下図のように new Vue から Vue インスタンスを作成すると、el と data が渡され、observer オブジェクトに data が渡され、Object.definpropertydata 内のデータが getter/setter に変換されてデータがハイジャックされます。 、およびデータ内の各属性が作成されます Dep インスタンスは、監視インスタンスを保存するために使用されます

el が compile に渡され、命令が compile で解析され、el で使用される data 内のデータが解析されると、ゲッターがトリガーされ、ウォッチャーが依存関係に追加されます。データが変更されると、セッターが依存関係通知を発行し、ウォッチャーに通知し、ウォッチャーは通知を受信した後にビューに通知を送信し、ビューを更新します。

データハイジャック

The html part creates a div tag with an id with app, which includes span and input tags. span タグは補間式を使用し、input タグは v-model を使用します

<div class="container" id="app"><span>内容:{
   
   {content}}</span><input type="text" v-model="content">
</div> 

js 部分は、データの双方向バインディングを実現するコードが記述されている vue.js ファイルを導入し、Vue インスタンス vm を作成し、データを div タグにマウントします。

const vm=new Vue({el:'#app',data:{content:'请输入开机密码'}
}) 

新しい Vue インスタンスは当然コンストラクタを使用する必要があります. Vue のソース コードの定義クラスはfunctionで定義されています. ここでは ES6クラスを使用してこの Vue インスタンスを作成します.

次に set constructor、新しい Vue インスタンスが渡されたときに渡されたオブジェクトとして、正式なパラメーターが obj_instance に設定され、渡されたオブジェクトのデータがインスタンスの $data 属性に割り当てられます。

javascript のオブジェクトのプロパティが変更されました。変更されたプロパティを dom ノードに更新できることを伝える必要があるため、インスタンスを初期化するときに、リスニング関数を呼び出しとして定義し、監視対象のデータを渡します。呼び出し

class Vue{//创建Vue实例constructor(obj_instance){this.$data=obj_instance.dataObserver(this.$data)}
}
function Observer(data_instance){//监听函数} 

このインスタンス vm を出力

インスタンスは作成されましたが、$data の各プロパティを監視する必要があります. これは、データ監視を実装するために使用されますObject.defineProperty.Object.definePropertyオブジェクトの既存のプロパティを変更できます. 構文形式はObject.defineProperty(obj, prop, descriptor)です.

  • obj: プロパティが定義されるオブジェクト
  • prop: 定義または変更するプロパティの名前
  • descriptor: 定義または変更される属性記述子

オブジェクト内の各プロパティを監視するには、Object.keys と foreach を使用してオブジェクト内の各プロパティを走査し、Object.defineProperty を使用して各プロパティのデータを監視します。

function Observer(data_instance){Object.keys(data_instance).forEach(key=>{Object.defineProperty(data_instance,key,{enumerable:true,//设置为true表示属性可以枚举configurable:true,//设置为true表示属性描述符可以被改变get(){},//访问该属性的时候触发,get和set函数就是数据监听的核心set(){},//修改该属性的时候触发})})
} 

その前Object.definePropertyに、属性に対応する値を保存して get 関数で返す必要があります。そうしないと、get 関数の後に属性の値がなくなり、属性に返される値が未定義になります。

let value=data_instance[key]
Object.defineProperty(data_instance,key,{enumerable:true,configurable:true,get(){console.log(key,value);return value},set(){}
}) 

$data の属性名をクリックすると、get 関数がトリガーされます

次に、set 関数を設定し、set の仮パラメータを設定します. この仮パラメータは、新しく渡された属性値を表し、新しい属性値を変数値に割り当てます. 何も返す必要はなく、変更するだけで、 return は get にアクセスすることです. return 時に、get は変更後の最新の値変数の値にもアクセスします

set(newValue){console.log(key,value,newValue);value = newValue
} 

しかし、現在 $data の最初のレイヤー属性に設定されるのは get と set のみです。

obj:{a:'a',b:'b'
} 

このようなものは set get と set ではありません. レイヤーごとにデータをアトリビュートにハイジャックする必要があるため、再帰を使用して再度自分自身を監視し、トラバースする前に条件判断を行います. サブアトリビュートがない場合やオブジェクトがない場合は、検出された場合、再帰を終了します

function Observer(data_instance){//递归出口if(!data_instance || typeof data_instance != 'object') returnObject.keys(data_instance).forEach(key=>{let value=data_instance[key]Observer(value)//递归-子属性的劫持Object.defineProperty(data_instance,key,{enumerable:true,configurable:true,get(){console.log(key,value);return value},set(newValue){console.log(key,value,newValue);value = newValue}})})
} 

別の詳細があります。 $data の content プロパティを文字列からオブジェクトに書き換えると、この新しいオブジェクトには get と set がありません

変更時に get と set をまったく設定しなかったため、set でリッスン関数を呼び出す必要があります。

set(newValue){console.log(key,value,newValue);value = newValueObserver(newValue)
} 

テンプレートの解析

データをハイジャックした後、Vue インスタンスのデータ アプリケーションをページに移動する必要があります。また、ページをレンダリングする前にすべてのデータを更新するために一時メモリ領域を追加して、DOM 操作を減らす必要があります。

解析関数を作成し、Vue インスタンスにマウントされた要素と Vue インスタンスの 2 つのパラメーターを設定し、関数内の要素を取得してインスタンスの $el に保存し、一時メモリに配置します。要素を取得した後[createDocumentFragment]、新しい空のドキュメント フラグメントを作成するために必要です

次に、$el の子ノードをフラグメント変数に 1 つずつ追加します。ページにはコンテンツがなく、コンテンツは一時的にフラグメントに格納されます。

class Vue{constructor(obj_instance){this.$data=obj_instance.dataObserver(this.$data)Compile(obj_instance.el,this)}
}
function Compile(ele,vm){vm.$el=document.querySelector(ele)const fragment=document.createDocumentFragment()let child;while (child=vm.$el.firstChild){fragment.append(child)}console.log(fragment);console.log(fragment.childNodes);
} 

変更が必要なコンテンツをドキュメント フラグメントに直接適用し、適用後に再レンダリングします. 変更する必要があるのは、フラグメントの childNodes 子ノードのテキスト ノードのみです. テキスト ノードのタイプは 3. できます.関数を作成し、それを呼び出してフラグメント内のコンテンツを変更します

ノードにノードが存在する可能性があるため、ノード タイプが 3 であるかどうかを判断し、そうでない場合は、この解析関数を再帰的に呼び出します

ノードタイプが3の場合は操作を変更しますが、ノード全体のテキストを変更することはできません.補間式の内容を変更するだけでよいため、正規表現マッチングを使用して、マッチング結果を保存する必要があります.変数で. 一致する結果は配列であり、インデックス 1 の要素は抽出する必要がある要素です. この要素は { {}} とスペースを削除した文字列であり、Vue インスタンスを直接使用して対応する属性の値にアクセス 変更後 リターン後、外に出て再帰を終了

function Compile(ele,vm){vm.$el=document.querySelector(ele) //获取元素保存在实例了的$el里const fragment=document.createDocumentFragment() //创建文档碎片let child;while (child=vm.$el.firstChild){//循环将子节点添加到文档碎片里fragment.append(child)}fragment_compile(fragment)function fragment_compile(node){ //修改文本节点内容const pattern = /\{\{\s*(\S*)\s*\}\}/ //检索字符串中正则表达式的匹配,用于匹配插值表达式if(node.nodeType===3){const result = pattern.exec(node.nodeValue)if(result){console.log('result[1]')const value=result[1].split('.').reduce(//split将对象里的属性分布在数组里,链式地进行排列;reduce进行累加,层层递进获取$data的值(total,current)=>total[current],vm.$data)node.nodeValue=node.nodeValue.replace(pattern,value) //replace函数将插值表达式替换成$data里的属性的值}return }node.childNodes.forEach(child=>fragment_compile(child))}vm.$el.appendChild(fragment) //将文档碎片应用到对应的dom元素里面
} 

ページの内容が再び出てきて、補間式が vm インスタンスのデータに置き換えられます

パブリッシャー パターンにサブスクライブする

データの乗っ取りが行われ、ページにデータが適用されますが、データが変更されたときにデータの更新が間に合わず、サブスクリプション パブリッシャー モードを実装する必要があります

最初にサブスクライバーを収集して通知するクラスを作成します. インスタンスを生成するとき, サブスクライバー情報を格納する配列が必要です. この配列にサブスクライバーを追加するメソッドとサブスクライバーに通知するメソッドが必要です. このメソッドを呼び出してリターン サブスクライバーの配列をトラバースし、サブスクライバーが独自の更新メソッドを呼び出して更新できるようにします

class Dependency{constructor(){this.subscribers=[] //存放订阅者的信息}addSub(sub){this.subscribers.push(sub) //将订阅者添加到这个数组里}notify(){this.subscribers.forEach(sub=>sub.update()) //遍历订阅者的数组,调用自身的update函数进行更新}
} 

サブスクライバー クラスを設定するには、Vue インスタンスのプロパティを使用する必要があります。Vue インスタンスと Vue インスタンスの対応するプロパティ、およびコールバック関数がパラメーターとして必要であり、パラメーターをインスタンスに割り当てます。

次に、サブスクライバーの更新関数を作成し、関数で渡されたコールバック関数を呼び出すことができます

class Watcher{constructor(vm,key,callback){//将参数都赋值给Watcher实例this.vm=vmthis.key=keythis.callback=callback}update(){this.callback() }
} 

ドキュメント フラグメント コンテンツを置換する場合、サブスクライバーに更新方法を伝える必要があるため、テンプレートがノード値を解析してコンテンツを置換するときにサブスクライバー インスタンスが作成され、vm インスタンスが渡されます。インデックス値 1 と実行ステートメントがコールバック関数にコピーされ、更新がサブスクライバーに通知されると、コールバック関数が呼び出されます。

コールバック関数の nodeValue は事前に保存する必要があります。そうしないと、置換されたコンテンツは補間式ではなく、置換されたコンテンツになります。

その後、サブスクライバーを Dependency インスタンスの配列に格納する方法を見つける必要があります. Watcher インスタンスを構築するときに、インスタンスをサブスクライバー配列に保存できます.

Dependency.temp=this //设置一个临时属性temp 

新しいサブスクライバーをサブスクライバー配列に追加し、すべてのサブスクライバーで同じ操作を実行すると、get をトリガーするときにサブスクライバーをサブスクライバー配列に追加できます。対応するプロパティ get を正しくトリガーするには、reduce メソッドを使用して実行する必要があります。キーの同じ操作

コンソールに Watcer インスタンスが出力され、各インスタンスが異なり、異なる属性値に対応していることがわかります。

Dependency クラスはまだインスタンスを作成しておらず、内部のサブスクライバー配列も存在しないため、最初にインスタンスを作成してからサブスクライバーをサブスクライバー配列に追加します

データを変更するときは、サブスクライバーに更新を通知し、セット内の依存関係の通知メソッドを呼び出します。通知メソッドは配列を通過し、サブスクライバーは独自の更新メソッドを実行してデータを更新します

ただし、更新呼び出しのコールバック関数には正式なパラメーターの設定がなく、split メソッドと reduce メソッドを使用して属性値を取得しています。

update(){const value =this.key.split('.').reduce((total,current)=>total[current],this.vm.$data)this.callback(value)
} 

コンソールで属性値を変更すると正常に変更され、ページが自動的に更新されます

テキストのバインドが完了したら、入力ボックスをバインドできます. vue では、v-model を介してバインドできるため、v-model を持つノードを特定する必要があります. 要素ノードの型は 1. nodeName を使用して入力要素に一致させることができます. 判定テキスト ノードの直下に新しい判定を作成します

if(node.nodeType===1&&node.nodeName==='INPUT'){const attr=Array.from(node.attributes)console.log(attr);
} 

ノード名 nodeName は v-model、nodeValue は name で、データ内の属性名です。

したがって、配列がトラバースされ、v-model が照合され、nodeValue に従って対応する属性値が検索され、属性値がノードに割り当てられます。同時に、サブスクライバーが自分自身を更新することを知るためにデータが更新された後、新しい Watcher インスタンスも INPUT ノードに追加されます。

attr.forEach(i=>{if(i.nodeName==='v-model'){const value=i.nodeValue.split('.').reduce((total,current)=>total[current],vm.$data)node.value=valuenew Watcher(vm,i.nodeValue,newValue=>{node.value=newValue})}
}) 

属性値を変更すると、ページも変更されます

最後に、ビューを使用してデータを変更します。addEventListener を使用して、v-model ノードに入力リスニング イベントを追加するだけです。

node.addEventListener('input',e=>{const arr1=i.nodeValue.split('.')const arr2=arr1.slice(0,arr1.length - 1)const final=arr2.reduce((total,current)=>total[current],vm.$data)final[arr1[arr1.length - 1]]=e.target.value
}) 

やっと

75のJSの高頻度インタビュー質問を整理し、回答と分析を提供しました.これは、JSに関するインタビューアーの質問に基本的に対処できることを保証できます.



困っている友達は、下のカードをクリックして無料で受け取り、共有できます

おすすめ

転載: blog.csdn.net/web22050702/article/details/128705627