Vue ソースコード学習でレスポンシブ性を実装する方法

Vue ソースコード学習でレスポンシブ性を実装する方法

序文

フロントエンド開発者としての私たちの日々の仕事は、ページにデータをレンダリングし、ユーザーとのやり取りを処理することです。 Vue では、データが変更されるとページが再レンダリングされます。たとえば、ページに数字を表示し、その横にクリック ボタンがあるとします。ボタンをクリックするたびに、ページに表示される数字が 1 つずつ増加します。これを実現するにはどうすればよいでしょうか。
ネイティブ JS のロジックによると、クリック イベントをリッスンし、イベント処理関数でデータを変更し、DOM を手動で変更して再レンダリングするという 3 つのことを行う必要があります。これと Vue を使用する場合の最大の違いは、[DOM を手動で変更して再レンダリングする] ステップがもう 1 つあることです。このステップは単純に見えますが、いくつかの問題を考慮する必要があります。

  • どの DOM を変更する必要がありますか?
  • データが変更されるたびに DOM を変更する必要がありますか?
  • DOM 変更のパフォーマンスを確保するにはどうすればよいですか?

レスポンシブなシステムを実装するのは簡単ではありません。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 に更新を通知できます。
Watcher と Dep は、非常に古典的なオブザーバー デザイン パターンの実装です。

2. 仮想DOMと差分

1. 仮想 DOM とは何ですか?

仮想 DOM は、JS 内のオブジェクトを使用して実際の DOM を表します。データの変更がある場合は、まず仮想 DOM で変更し、次に実際の DOM を変更します。良いアイデアです! 💡

仮想 DOM の利点については、Youda 氏の意見を聞くのがよいでしょう。

私の意見では、仮想 DOM の本当の価値はパフォーマンスではなく、1) 機能的な UI プログラミングへの扉を開くこと、2) DOM 以外のバックエンドにレンダリングできることです。

例えば:

<テンプレート>
  <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 を実行します。
関数 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. メインスレッドの他に、「タスクキュー」もあります。非同期タスクに実行結果がある限り、イベントは「タスク キュー」に配置されます。
  3. 「実行スタック」内のすべての同期タスクが実行されると、システムは「タスク キュー」を読み取って、そこにどのようなイベントが含まれているかを確認します。対応する非同期タスクは待機状態を終了し、実行スタックに入り、実行を開始します。
  4. メインスレッドは上記の手順 3 を繰り返します。

メインスレッドの実行プロセスはティックであり、すべての非同期結果は「タスク キュー」を通じてスケジュールされます。 メッセージ キューにはタスクが 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をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • Vueは配列とオブジェクトの値をレスポンシブに追加および変更します
  • Vue の応答性に関する簡単な説明 (配列の変更方法)
  • Vue.jsは内部の応答性の原則を探求するために毎日学習する必要があります
  • Vueのレスポンシブ原則についての簡単な説明
  • Vue レスポンシブデータ更新の誤解について
  • Vueでレスポンシブシステムを実装する方法
  • Vueのレスポンシブ原則の詳細な説明
  • Vue3.0データ応答原則の詳細な説明
  • Vue のデータ応答性の原理についての簡単な説明

<<:  MySQL SHOW STATUSステートメントの使用

>>:  自己終了XHTMLタグを書くときに注意すべきこと

推薦する

vue-routeルーティング管理のインストールと設定方法

導入Vue Router 、 Vue.jsの公式ルーティング マネージャーです。 Vue.jsのコア...

MySQL 高可用性クラスタの展開とフェイルオーバーの実装

目次1. 内閣府1. コンセプト2. MHAの構成3. MHAの特徴2. MySQL+MHAをビルド...

MySQL ソート機能の詳細

目次1. 問題のシナリオ2. 原因分析3. 解決策4. 知識を広げる4.1 クエリの最適化を制限する...

一般的な XHTML タグの紹介

<br />しばらくの間、多くの人が XHTML の使い方を知らないことに気付きました。...

アイデアを通じてプロジェクトをDockerにパッケージ化する方法

多くの友人が、Docker でプロジェクトを実行する方法をずっと知りたがっていました。今日は、自分の...

CentOS 8が利用可能になりました

CentOS 8 が利用可能になりました! CentOS 8 と RedHat Enterprise...

Vueカスタムツリーコントロールの使い方の詳細な説明

この記事では、Vueカスタムツリーコントロールの使い方を参考までに紹介します。具体的な内容は次のとお...

Vueのカスタムイベントコンテンツ配信の詳細な説明

1. これは理解するのが少し複雑なので、原理を注意深く読んで自分で入力していただければ幸いです。 &...

Vue 2.0 の基礎を詳しく解説

目次1. 特徴2. 例3. オプション4. 基本的な文法5. ライフサイクル6. ルーティング管理 ...

CentOS 上での MySQL 5.6 のコンパイルとインストール、および複数の MySQL インスタンスのインストールの詳細な説明

--1. mysql用の新しいグループとユーザーを作成する # ユーザー追加 -M -s /sbin...

JavaScript でフォロー広告を実装するためのサンプルコード

フローティング広告は、ウェブサイト上で非常に一般的な広告形式です。フローティング広告は、ユーザーの閲...

Vueブラウザが監視を再開するための具体的な手順

序文ページを共有するときに、ブラウザの戻るボタンをクリックしてプロジェクトのホームページに戻り、訪問...

Docker Swarm を使用して分散クローラー クラスターを構築する例

クローラーの開発プロセス中に、クローラーを複数のサーバーに展開する必要がある状況に遭遇したことがある...

HTML の隠しフィールドの紹介と例

基本的な構文: <input type="hidden" name=&qu...