序文フロントエンド開発者としての私たちの日々の仕事は、ページにデータをレンダリングし、ユーザーとのやり取りを処理することです。 Vue では、データが変更されるとページが再レンダリングされます。たとえば、ページに数字を表示し、その横にクリック ボタンがあるとします。ボタンをクリックするたびに、ページに表示される数字が 1 つずつ増加します。これを実現するにはどうすればよいでしょうか。
レスポンシブなシステムを実装するのは簡単ではありません。Vueのソースコードを組み合わせて、Vueの優れたアイデアを学びましょう〜 1. レスポンシブシステムの重要な要素1. データの変更を監視する方法明らかに、すべてのユーザーインタラクションイベントを監視してデータの変更を取得するのは非常に面倒であり、一部のデータの変更はユーザーによってトリガーされない可能性があります。では、Vue はデータの変更をどのように監視するのでしょうか? ——オブジェクト.defineProperty Object.defineProperty メソッドがデータの変更を監視できるのはなぜですか?このメソッドは、オブジェクトに新しいプロパティを直接定義したり、オブジェクトの既存のプロパティを変更したりして、オブジェクトを返すことができます。まずはその構文を見てみましょう。 オブジェクト.defineProperty(obj, prop, 記述子) // obj は渡されたオブジェクト、prop は定義または変更されるプロパティ、descriptor はプロパティ記述子です ここでの核となるのは、多くのオプションのキー値を持つ記述子です。ここで最も注意すべき点は、get と set です。get は、プロパティに提供される getter メソッドです。プロパティにアクセスすると、getter メソッドがトリガーされます。set は、プロパティに提供される setter メソッドです。プロパティを変更すると、setter メソッドがトリガーされます。 つまり、データ オブジェクトにゲッターとセッターがあれば、その変更を簡単に監視でき、レスポンシブ オブジェクトと呼ぶことができます。具体的にはどうすればいいのでしょうか? 関数 observe(データ) { if (isObject(データ)) { Object.keys(data).forEach(キー => { defineReactive(データ、キー) }) } } 関数defineReactive(obj, prop) { val = obj[prop]とします let dep = new Dep() // 依存関係を収集するために使用しますObject.defineProperty(obj, prop, { 得る() { // オブジェクト プロパティにアクセスすると、現在のオブジェクト プロパティが依存しており、依存関係が収集されることを示します dep.depend() 戻り値 } set(newVal) { (newVal === val)の場合、戻り値 // データが変更されたので、関係する担当者に通知して対応するビューを更新する必要があります val = newVal dep.notify() } }) // ディープモニタリング if (isObject(val)) { 観察(値) } オブジェクトを返す } ここでは依存関係の収集を行うために Dep クラス (依存関係) が必要です🎭 PS: Object.defineProperty は既存のプロパティのみを監視できますが、新しく追加されたプロパティに対しては無力です。また、配列の変更も監視できません (この問題は、Vue2 で配列プロトタイプのメソッドを書き換えることで解決されています)。そのため、Vue3 ではより強力な Proxy に置き換えられています。 2. 依存関係を収集する方法 - Depクラスを実装するコンストラクターの実装に基づいて: 関数Dep() { // deps配列を使用してさまざまな依存関係を保存します this.deps = [] } // Dep.target は、グローバルに一意の Watcher である実行中のウォッチャーインスタンスを記録するために使用されます。 // これは非常に巧妙な設計です。JS はシングルスレッドなので、同時に計算できるグローバル Watcher は 1 つだけです。Dep.target = null // プロトタイプにdependメソッドを定義し、各インスタンスがアクセスできるようにします。Dep.prototype.depend = function() { if (依存ターゲット) { this.deps.push(依存関係ターゲット) } } // プロトタイプに通知メソッドを定義してウォッチャーに更新を通知します Dep.prototype.notify = function() { this.deps.forEach(ウォッチャー => { ウォッチャー.更新() }) } // Vue にはコンポーネントのネストなどのネストされたロジックがあるため、スタックを使用してネストされたウォッチャーを記録します。 // スタック、先入れ後出し const targetStack = [] 関数pushTarget(_target) { if (Dep.target) targetStack.push(Dep.target) 依存関係ターゲット = _target } 関数popTarget() { Dep.target = targetStack.pop() } ここでは、主にプロトタイプの 2 つのメソッド、depend と notification について理解します。1 つは依存関係を追加するためのもので、もう 1 つは更新を通知するためのものです。 「依存関係」の収集について話しましたが、 this.deps 配列には正確に何が保存されるのでしょうか? Vue は依存関係の表現に Watcher の概念を設定します。つまり、this.deps に収集されるものは Watcher です。 3. データが変更されたときに更新する方法 - Watcherクラスの実装Vue には、ページのレンダリングに使用される 3 種類の Watcher と、computed および watch という 2 つの API があります。これらを区別するために、用途の異なる Watcher はそれぞれ renderWatcher、computedWatcher、watchWatcher と呼ばれます。 クラスを使用して実装します。 クラスウォッチャー{ コンストラクタ(expOrFn) { // ここで渡されたパラメータが関数でない場合は解析する必要があり、parsePath は省略されます this.getter = typeof expOrFn === 'function' ? expOrFn : parsePath(expOrFn) この.get() } // クラス内で関数を定義する場合は関数を書く必要はありません 得る() { // 実行のこの時点では、これは現在のウォッチャーインスタンスであり、Dep.target でもあります。 pushTarget(これ) this.value = this.getter() ポップターゲット() } アップデート() { この.get() } } この時点で、シンプルなレスポンシブ システムが形になりました。要約すると、Object.defineProperty を使用すると、誰がデータにアクセスしたか、いつデータが変更されたかを知ることができ、Dep は特定のデータに関連する DOM を記録でき、Watcher はデータが変更されたときに DOM に更新を通知できます。 2. 仮想DOMと差分1. 仮想 DOM とは何ですか?仮想 DOM は、JS 内のオブジェクトを使用して実際の DOM を表します。データの変更がある場合は、まず仮想 DOM で変更し、次に実際の DOM を変更します。良いアイデアです! 💡 仮想 DOM の利点については、Youda 氏の意見を聞くのがよいでしょう。
例えば: <テンプレート> <div id="app" class="container"> <h1>こんにちは世界! </h1> </div> </テンプレート> // 対応するvnode { タグ: 'div', プロパティ: { id: 'app', クラス: 'container' }, 子: { タグ: 'h1'、 子: 'HELLO WORLD! '} } 次のように定義できます。 関数 VNode(タグ、データ、子、テキスト、elm) { this.tag = タグ this.data = データ this.childern = 子供 this.text = テキスト this.elm = elm // 実際のノードへの参照} 2. 差分アルゴリズム - 新旧ノードの比較データが変更されると、レンダリング ウォッチャー コールバックがトリガーされ、ビューが更新されます。 Vue ソースコードでは、ビューを更新するときに、新しいノードと古いノードの類似点と相違点を比較するために patch メソッドが使用されます。 (1)新ノードと旧ノードが同じノードであるかどうかを判断する 関数sameVNode() 関数sameVnode(a, b) { a.key === b.key && を返します ( a.タグ === b.タグ && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && 同じ入力タイプ(a, b) ) } (2)新ノードと旧ノードが異なる場合 古いノードを置き換える: 新しいノードを作成する --> 古いノードを削除する (3)新旧ノードが同じ場合
関数 updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { oldStartIdx = 0 とする newStartIdx = 0 とします oldEndIdx = oldCh.length - 1 とします。 oldStartVnode = oldCh[0]とする oldEndVnode = oldCh[oldEndIdx]とする newEndIdx = newCh.length - 1 とします。 newStartVnode = newCh[0]とする newEndVnode = newCh[newEndIdx]とします。 oldKeyToIdx、idxInOld、vnodeToMove、refElm を使用します。 // 上記は、新しい Vnode と古い Vnode の先頭と末尾のポインタ、新しい Vnode と古い Vnode の先頭と末尾のノードです while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // while条件が満たされない場合は、古いVnodeと新しいVnodeの少なくとも1つが1回走査されたことを意味し、ループを終了します。if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnodeが左に移動されました } そうでない場合 (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } そうでない場合 (sameVnode(oldStartVnode, newStartVnode)) { // 古いスタートと新しいスタートを比較して、同じノードであるかどうかを確認します patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) 古い開始Vノード = 古いCh[++古い開始Idx] 新しい開始Vノード = 新しいCh[++新しい開始Idx] } そうでない場合 (sameVnode(oldEndVnode, newEndVnode)) { // 古い端と新しい端を比較して、同じノードであるかどうかを確認します patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnodeが右に移動 // 古い開始ノードと新しい終了ノードを比較して、同じノードであるかどうかを確認します。patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(親Elm、oldStartVnode.elm、nodeOps.nextSibling(oldEndVnode.elm)) 古い開始Vノード = 古いCh[++古い開始Idx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnodeが左に移動 // 古い終了と新しい開始を比較して、同じノードであるかどうかを確認します patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(親Elm、古い終了Vnode.elm、古い開始Vnode.elm) oldEndVnode = oldCh[--oldEndIdx] 新しい開始Vノード = 新しいCh[++新しい開始Idx] } それ以外 { // キーを設定する場合と設定しない場合の違い: // キーを設定しないと、newCh と oldCh は先頭と末尾のみを比較します。キーを設定すると、先頭と末尾の比較に加えて、キーによって生成されたオブジェクト oldKeyToIdx から一致するノードが検索されます。したがって、ノードにキーを設定すると、DOM をより効率的に使用できます。 if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? 古いキーからIdx[新しい開始Vノード.キー] : findIdxInOld(新しい開始ノード、古いCh、古い開始Idx、古い終了Idx) // 古い Vnode シーケンスからキーを持つノードを抽出し、マップに配置してから、新しい vnode シーケンスをトラバースします // vnode のキーがマップ内にあるかどうかを判断します。ある場合は、キーに対応する古い Vnode を見つけます。古い Vnode がトラバースされた vnode と同じ場合は、dom を再利用し、dom ノードの位置を移動します if (isUndef(idxInOld)) { // 新しい要素 createElm(newStartVnode、insertedVnodeQueue、parentElm、oldStartVnode.elm、false、newCh、newStartIdx) } それ以外 { vnodeToMove = 古いCh[idxInOld] 同じVnode(vnodeToMove, newStartVnode)の場合 パッチVnode(vnodeToMove、newStartVnode、insertedVnodeQueue) oldCh[idxInOld] = 未定義 canMove && nodeOps.insertBefore(親Elm、vnodeToMove.elm、oldStartVnode.elm) } それ以外 { // 同じキーだが要素が異なる。新しい要素として扱う createElm(newStartVnode、insertedVnodeQueue、parentElm、oldStartVnode.elm、false、newCh、newStartIdx) } } 新しい開始Vノード = 新しいCh[++新しい開始Idx] } } (古い開始ID>古い終了ID)の場合{ refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm Vnodesを追加します(parentElm、refElm、newCh、newStartIdx、newEndIdx、insertedVnodeQueue) } そうでない場合 (newStartIdx > newEndIdx) { Vnodes を削除します (親 Elm、古い Ch、古い開始 Idx、古い終了 Idx) } } ここでの主なロジックは、新しいノードの先頭と末尾を古いノードの先頭と末尾と比較して、同じノードであるかどうかを確認することです。同じ場合は、Vnode を直接パッチします。そうでない場合は、マップを使用して古いノードのキーを保存し、新しいノードのキーをトラバースして、古いノードに存在するかどうかを確認します。同じ場合は、それらを再利用します。ここでの時間計算量は O(n) で、空間計算量も O(n) であり、空間を使用して時間を交換します。 diff アルゴリズムは主に、更新の量を減らし、差異が最小の DOM を見つけて、差異のみを更新するために使用されます。 3. 次のティックいわゆる nextTick は次のティックを意味しますが、ティックとは何でしょうか? JS の実行はシングルスレッドであることがわかっています。JS はイベント ループに基づいて非同期ロジックを処理します。これは主に次の手順に分かれています。
メインスレッドの実行プロセスはティックであり、すべての非同期結果は「タスク キュー」を通じてスケジュールされます。 メッセージ キューにはタスクが 1 つずつ保存されます。 仕様では、タスクはマクロタスクとマイクロタスクの 2 つのカテゴリに分けられ、各マクロタスクが完了したらすべてのマイクロタスクをクリアする必要があることが規定されています。 for (macroTask の macroTaskQueue) { // 1. 現在のマクロタスクを処理する マクロタスク処理() // 2. すべてのMICRO-TASKを処理する (microTaskQueue の microTask) の場合 { マイクロタスクを処理する(マイクロタスク) } } ブラウザ環境では、一般的なマクロ タスクには setTimeout、MessageChannel、postMessage、setImmediate、setInterval が含まれ、一般的なマイクロ タスクには MutationObsever と Promise.then が含まれます。 データの変更による DOM の再レンダリングは、次のティックで発生する非同期プロセスであることがわかっています。たとえば、開発プロセス中にサーバー インターフェイスからデータを取得すると、データが変更されます。一部のメソッドがデータの変更後の DOM の変更に依存する場合は、nextTick の後に実行する必要があります。たとえば、次の疑似コード: getData(res).then(() => { this.xxx = res.データ this.$nextTick(() => { // ここで変更された DOM を取得できます }) }) IV. 結論Vueソースコード学習におけるレスポンシブ実装方法についての記事はこれで終了です。Vueのレスポンシブ実装に関するより関連性の高い内容については、123WORDPRESS.COMの過去の記事を検索するか、以下の関連記事を引き続き閲覧してください。皆様、今後とも123WORDPRESS.COMをよろしくお願いいたします。 以下もご興味があるかもしれません:
|
<<: MySQL SHOW STATUSステートメントの使用
導入Vue Router 、 Vue.jsの公式ルーティング マネージャーです。 Vue.jsのコア...
目次1. 内閣府1. コンセプト2. MHAの構成3. MHAの特徴2. MySQL+MHAをビルド...
目次1. 問題のシナリオ2. 原因分析3. 解決策4. 知識を広げる4.1 クエリの最適化を制限する...
<br />しばらくの間、多くの人が XHTML の使い方を知らないことに気付きました。...
多くの友人が、Docker でプロジェクトを実行する方法をずっと知りたがっていました。今日は、自分の...
CentOS 8 が利用可能になりました! CentOS 8 と RedHat Enterprise...
この記事では、Vueカスタムツリーコントロールの使い方を参考までに紹介します。具体的な内容は次のとお...
1. これは理解するのが少し複雑なので、原理を注意深く読んで自分で入力していただければ幸いです。 &...
レイアウト部分: <div id="スライダー"> <!-- ...
目次1. 特徴2. 例3. オプション4. 基本的な文法5. ライフサイクル6. ルーティング管理 ...
--1. mysql用の新しいグループとユーザーを作成する # ユーザー追加 -M -s /sbin...
フローティング広告は、ウェブサイト上で非常に一般的な広告形式です。フローティング広告は、ユーザーの閲...
序文ページを共有するときに、ブラウザの戻るボタンをクリックしてプロジェクトのホームページに戻り、訪問...
クローラーの開発プロセス中に、クローラーを複数のサーバーに展開する必要がある状況に遭遇したことがある...
基本的な構文: <input type="hidden" name=&qu...