Vue 仮想 Dom から実際の Dom への変換

Vue 仮想 Dom から実際の Dom への変換

別のツリー構造があるJavascriptオブジェクトでは、このツリーが本物であると伝えるだけでよいDom ツリーはマッピング関係を形成します。前回のmountComponent メソッド:

エクスポート関数 mountComponent(vm, el) {
  vm.$el = el
  ...
  callHook(vm, 'beforeMount')
  ...
  定数updateComponent = 関数() {
    vm._update(vm._render())
  }
  ...
}

vm._render メソッドを実行し、VNode を取得しました。次に、それを vm._update メソッドにパラメータとして渡して実行します。 vm._update メソッドの機能は、VNode を実際の Dom に変換することですが、実行時間は 2 回あります。

最初のレンダリング

新しい Vue が実行されると、それが最初のレンダリングとなり、入力された Vnode オブジェクトが実際の Dom にマッピングされます。

ページを更新

データの変更によってページが変更されますが、これは Vue の最もユニークな機能の 1 つです。比較のために、データの変更前と変更後に 2 つの VNode が生成されます。古い VNode に最小限の変更を加えてページをレンダリングする方法、このような diff アルゴリズムは非常に複雑です。 まずデータ応答性が何であるかを明確にしておかないと、diff を直接使用して Vue の全体的なプロセスを理解するのは難しくなります。 したがって、この章では最初のレンダリングを分析し、次の章ではデータの応答性、そして差分比較を分析します。

まず、vm._update メソッドの定義を見てみましょう。

Vue.prototype._update = 関数(vnode) {
  ... 最初のレンダリング vm.$el = vm.__patch__(vm.$el, vnode) // 元の vm.$el を上書きします
  ...
}

ここでの vm.el は、以前に mount Component メソッドでマウントされたもので、実際の Dom 要素です。 最初のレンダリングでは、以前に ==mountComponent== メソッドでマウントされた実際の ==Dom== 要素である vm.el が渡されます。 最初のレンダリングでは、以前に ==mountComponent== メソッドでマウントされた実際の ==Dom== 要素である vm.el が渡されます。最初のレンダリングでは vm.el と結果の VNode が渡されるので、vm.patch の定義を確認します。

Vue.prototype.__patch__ = createPatchFunction({ nodeOps, modules })

patch は createPatchFunction メソッドによって返されるメソッドで、次のオブジェクトを受け入れます。

nodeOps 属性: 作成、挿入、削除など、ネイティブ Dom を操作するための一連のメソッドをカプセル化します。これらがどこで使用されているかを詳しく説明します。

モジュール属性: 実際の Dom を作成するには、class/attrs/style などの属性を生成する必要もあります。 modules は配列コレクションであり、配列内の各項目はこれらの属性に対応するフック メソッドです。これらの属性の作成、更新、破棄にはすべて対応するフック メソッドがあります。 特定の瞬間に何かを実行する必要がある場合は、対応するフックを実行するだけです。 たとえば、これらすべてに create フック メソッドがあります。これらの create フックが配列に収集されている場合、実際の Dom でこれらの属性を作成する必要があるときに、配列内の各項目が順番に実行され、つまり順番に作成されます。

PS: ここでの modules 属性のフック メソッドはプラットフォーム固有です。Web、weex、SSR はそれぞれ異なる方法で VNode メソッドを呼び出すため、Vue は関数カリー化を再度使用して createPatchFunction のプラットフォームの違いを平滑化し、パッチ メソッドが新しいノードと古いノードのみを受け取るようにしています。

DOMを生成する

ここで覚えておく必要があるのは、VNode がどのようなタイプのノードであっても、要素ノード、コメント ノード、テキスト ノードの 3 種類のノードのみが作成され、Dom に挿入されるということです。

createPatchFunction を見て、どのようなメソッドが返されるかを確認しましょう。

関数createPatchFunction(backend)をエクスポートします。
  ...
  const { modules, nodeOps } = backend // 入力コレクションを分解する return function (oldVnode, vnode) { // 新しいvnodeと古いvnodeを受け取る
    ...
    const isRealElement = isDef(oldVnode.nodeType) // 実際の Dom ですか?
    if(isRealElement) { // $elは実際のDOMです
      oldVnode = emptyNodeAt(oldVnode) // VNode 形式に変換し、自身を上書きします}
    ...
  }
}

初めてレンダリングするときには、oldVnode はありません。OldVnode は、emptyNodeAt(odVnode) メソッドによってラップされる実際の dom である $el です。

関数emptyNodeAt(elm) {
  新しいVNodeを返す(
    nodeOps.tagName(elm).toLowerCase(), // 対応するタグ属性 {}, // 対応するデータ
    [], // 子に対応
    undefined、//テキストに対応
    elm // 実際のDOMはelm属性に割り当てられます)
}

梱包後:
{
  タグ: 'div',
  elm: '<div id="app"></div>' // 実際の DOM
}

-------------------------------------------------------

ノードオペレーション:
export function tagName (node) { // ノードのタグ名を返す return node.tagName  
}

渡された ==$el== 属性を VNode 形式に変換した後、次のように続行します。

関数createPatchFunction(backend)をエクスポートします。 
  ...
  
  return function (oldVnode, vnode) { // 古いvnodeと新しいvnodeを受け取る
  
    const 挿入VnodeQueue = []
    ...
    const oldElm = oldVnode.elm // パッケージ化後の実際の Dom <div id='app'></div>
    const parentElm = nodeOps.parentNode(oldElm) // 最初の親ノードは <body></body> です
  	
    createElm( // 実際のDomを作成する
      vnode, // 2 番目のパラメータ insertedVnodeQueue, // 空の配列 parentElm, // <body></body>
      nodeOps.nextSibling(oldElm) // 次のノード)
    
    return vnode.elm // vm.$el を上書きするために実際の Dom を返します
  }
}
                                              
------------------------------------------------------

ノードオペレーション:
export function parentNode (node) { // 親ノードを取得する return node.parentNode 
}

export function nextSibling(node) { // 次のノードを取得する return node.nextSibling  
}

createElm メソッドは実際の Dom の生成を開始します。VNode が実際の Dom を生成する方法は、要素ノードとコンポーネントの 2 つに分かれているため、前の章で生成された VNode を使用して個別に説明します。

1. 要素ノードはDOMを生成する

{ // 要素ノード VNode
  タグ: 'div',
  子供たち: [{
      タグ: 'h1',
      子供たち: [
        {テキスト: 'タイトル h1'}
      ]
    }, {
      タグ: 'h2',
      子供たち: [
        {テキスト: 'タイトル h2'}
      ]
    }, {
      タグ: 'h3',
      子供たち: [
        {テキスト: 'タイトル h3'}
      ]
    }
  ]
}

まずこのフローチャートを見て印象をつかみ、次に具体的な実装を見るとアイデアがはるかに明確になります (ここではインターネットから画像を借用します)。

ここに画像の説明を挿入

まず Dom から始めて、その定義を見てみましょう。

関数createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) { 
  ...
  const children = vnode.children // [VNode, VNode, VNode]
  const tag = vnode.tag // div
  
  コンポーネントを作成する場合(vnode、挿入されたVnodeQueue、親Elm、refElm) {
    return // コンポーネントの結果が true を返す場合は、続行されません。CreateComponent については後で詳しく説明します。
  }
  
  if(isDef(tag)) { // 要素ノード vnode.elm = nodeOps.createElement(tag) // 親ノードを作成 createChildren(vnode, children, insertVnodeQueue) // 子ノードを作成 insert(parentElm, vnode.elm, refElm) // 挿入 } else if(isTrue(vnode.isComment)) { // コメントノード vnode.elm = nodeOps.createComment(vnode.text) // コメントノードを作成 insert(parentElm, vnode.elm, refElm); // 親ノードに挿入 } else { // テキストノード vnode.elm = nodeOps.createTextNode(vnode.text) // テキストノードを作成 insert(parentElm, vnode.elm, refElm) // 親ノードに挿入 }
  
  ...
}

------------------------------------------------------------------

ノードオペレーション:
export function createElement(tagName) { // ノードを作成する return document.createElement(tagName)
}

export function createComment(text) { //コメントノードを作成する return document.createComment(text)
}

export function createTextNode(text) { // テキストノードを作成する return document.createTextNode(text)
}

function insert (parent, elm, ref) { //DOM 操作を挿入 if (isDef(parent)) { //親ノードがある if (isDef(ref)) { //参照ノードがある if (ref.parentNode === parent) { //参照ノードの親ノードは渡された親ノードと等しい nodeOps.insertBefore(parent, elm, ref) //親ノードの参照ノードの前に elm を挿入
      }
    } それ以外 {
      nodeOps.appendChild(parent, elm) // elmを親に追加します}
  } // 親ノードがない場合は何もしない}
これは多くの場所で使用されているため、比較的重要な方法です。

順番に要素ノード、コメントノード、テキストノードのどれであるかを判断し、それぞれ作成してから親ノードに挿入します。ここでは主に要素ノードの作成について紹介し、他の 2 つには複雑なロジックはありません。 createChild メソッドの定義を見てみましょう。

関数 createChild(vnode, children, insertedVnodeQueue) {
  if(Array.isArray(children)) { // は配列です for(let i = 0; i < children.length; ++i) { // 各vnodeを走査します createElm( // children[i]を再帰的に呼び出します, 
        挿入されたVnodeQueue、 
        vnode.elm、 
        ヌル、 
        true, //子を挿入するルートノードではない、 
        私
      )
    }
  } else if(isPrimitive(vnode.text)) { //typeofは文字列/数値/シンボル/ブール値のいずれかですnodeOps.appendChild( // 親ノードを作成して挿入しますvnode.elm、 
      nodeOps.createTextNode(文字列(vnode.text))
    )
  }
}

---------------------------------------------------------------------------------

ノードオペレーション:
export default appendChild(node, child) { // 子ノードを追加node.appendChild(child)
}

子ノードの作成を開始し、VNode の各項目をトラバースし、前の createElm メソッドを使用して各項目の Dom を作成します。 項目が配列の場合は、createChild を呼び出し続けて項目の子ノードを作成します。項目が配列でない場合は、テキスト ノードを作成し、それを親ノードに追加します。 このように、再帰を使用して、ネストされたすべての VNode を実際の Dom として作成します。

フローチャートを見れば、多くの疑問が軽減されるはずです (ここではインターネットから章の写真を借用しています)。

ここに画像の説明を挿入

簡単に言うと、実際のDomを内側から外側に1つずつ作成し、それを親ノードに挿入し、最後に作成したDomをボディに挿入して作成プロセスを完了します。要素ノードの作成は比較的簡単です。次に、コンポーネントタイプの作成方法を見てみましょう。

コンポーネントVNodeはDomを生成する

{ // コンポーネント VNode
  タグ: 'vue-component-1-app',
  コンテクスト: {...}、
  コンポーネントオプション: {
    Ctor: function(){...}, // サブコンポーネントコンストラクタ propsData: undefined,
    子供: 未定義、
    タグ: 未定義
  },
  データ: {
    on: undefined, // ネイティブイベントフック: { // コンポーネントフック init: function(){...},
      挿入: function(){...},
      プレパッチ: function(){...},
      破棄: 関数(){...}
    }
  }
}

-------------------------------------------

<template> // アプリ コンポーネント内のテンプレート <div>アプリ テキスト</div>
</テンプレート>

まず、簡単なフローチャートを見てみましょう。後で論理的な順序を整理しやすくするために、インパクトだけ残しておきます(ここではインターネットから画像を拝借しています)。

ここに画像の説明を挿入

前の章のコンポーネントを使用して VNode を生成し、createElm でコンポーネントの Dom ブランチ ロジックがどのように作成されるかを確認します。

関数createElm(vnode, insertedVnodeQueue, parentElm, refElm) { 
  ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { // コンポーネントブランチの戻り  
  }
  ...

createComponent メソッドを実行します。要素ノードの場合は何も返されないため、未定義となり、メタノードを作成する次のロジックが続行されます。 次はコンポーネントです。createComponent の実装を見てみましょう。

関数createComponent(vnode、insertedVnodeQueue、parentElm、refElm) {
  i = vnode.dataとする
  if(isDef(i)) {
    if(isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode) // initメソッドを実行する}
    ...
  }
}

まず、コンポーネントの vnode.data が i に割り当てられます。この属性が存在するかどうかによって、それがコンポーネント vnode であるかどうかを判断できます。 次の if(isDef(i = i.hook) && isDef(i = i.init)) は、判定と代入を 1 つにまとめたものです。if 内の i(vnode) は、実行されるコンポーネント init(vnode) メソッドです。 ここで、コンポーネントの init フック メソッドが何を行うかを見てみましょう。

import activeInstance // グローバル変数 const init = vnode => {
  定数子 = vnode.componentInstance = 
    Vnode のコンポーネントインスタンスを作成します (vnode、アクティブインスタンス)
  ...
}

activeInstance はグローバル変数であり、update メソッドで現在のインスタンスに割り当てられます。現在のインスタンスのパッチ処理中にコンポーネントの親インスタンスとして渡され、子コンポーネントの initLifecycle 中にコンポーネントの関係が確立されます。 createComponentInsanceForVnode の結果は vnode.componentInstance に割り当てられるので、何が返されるか見てみましょう。

export createComponentInstanceForVnode(vnode, parent) { // parentはグローバル変数activeInstanceです
  const options = { // コンポーネントオプション
    _isComponent: true, // コンポーネントであることを示すフラグを設定します _parentVnode: vnode, 
    parent // 初期化 initLifecycle が親子関係を確立できるように、子コンポーネントの親 VM インスタンス}
  
  return new vnode.componentOptions.Ctor(options) // サブコンポーネントのコンストラクタはCtorとして定義されます
}

コンポーネントの init メソッドでは、まず craeeteComponentInstanceForVnode メソッドが実行されます。このメソッド内でサブコンポーネントのコンストラクターがインスタンス化されます。サブコンポーネントのコンストラクターは基本クラス Vue のすべての機能を継承するため、これは new Vue({…}) を実行することと同じです。次に、==_init メソッドが実行され、一連のサブコンポーネント初期化ロジックが実行され、その後 _init== メソッドに戻ります。両者の間にはまだいくつかの違いがあるためです。

Vue.prototype._init = 関数(オプション) {
  if(options && options._isComponent) { // コンポーネントのマージ オプション、_isComponent は以前に定義されたマーカーです initInternalComponent(this, options) // 違いは、コンポーネントのマージされた項目がはるかにシンプルになるためです}
  
  initLifecycle(vm) // 親子関係を確立します...
  callHook(vm, '作成済み')
  
  if (vm.$options.el) { // コンポーネントにはel属性がないので、ここで停止します vm.$mount(vm.$options.el)
  }
}

----------------------------------------------------------------------------------------

function initInternalComponent(vm, options) { // サブコンポーネントオプションをマージする
  const opts = vm.$options = Object.create(vm.constructor.options)
  opts.parent = options.parent // コンポーネント初期化代入、グローバル変数 activeInstance
  opts._parentVnode = options._parentVnode // コンポーネント初期化割り当て、コンポーネント vnode 
  ...
}

これまではすべてうまく実行されていましたが、最終的には el 属性がなかったためマウントされず、createComponentInstanceForVnode メソッドが実行されました。 この時点で、コンポーネントの init メソッドに戻り、残りのロジックを完了します。

定数init = vnode => {
  const child = vnode.componentInstance = // コンポーネントインスタンスを取得する createComponentInstanceForVnode(vnode, activeInstance)
    
  child.$mount(undefined) // その後手動でマウントします}

このコンポーネントを init メソッドで手動でマウントし、コンポーネントの ==render()== メソッドを実行してコンポーネント内の要素ノード VNode を取得し、次に vm._update() を実行してコンポーネントの patch メソッドを実行します。$mount メソッドは undefined を渡すため、oldVnode も undefined となり、__patch_ 内のロジックが実行されます。

関数 patch(oldVnode, vnode) を返す {
  ...
  if (isUndef(oldVnode)) {
    createElm(vnode、挿入されたVnodeQueue)
  }
  ...
}

今回は、3 番目のパラメータの親ノードを渡さずに createElm を実行しました。では、コンポーネントによって作成された Dom を有効にするにはどこに配置すればよいでしょうか。 Dom を生成するための親ノード ページはありません。この時点ではコンポーネント パッチが実行されるため、パラメーター vnode はコンポーネント内の要素ノードの vnode になります。

<template> // アプリ コンポーネント内のテンプレート <div>アプリ テキスト</div>
</テンプレート>

-------------------------

{ // アプリ内の要素のvnode
  タグ: 'div',
  子供たち: [
    {テキスト: アプリのテキスト}
  ]、
  parent: { // サブコンポーネントがinitのときにinitLifecycleを実行することで確立された関係 tag: 'vue-component-1-app',
    コンポーネントオプション: {...}
  }
}

明らかに、この時点ではコンポーネントではありません。コンポーネントであっても問題はありません。要素ノードで構成されるコンポーネントは常に存在するため、せいぜい、createComponent のロジックを実行してコンポーネントを作成する必要があります。 このとき、要素ノードを作成するロジックを実行します。3 番目のパラメータの親ノードがないため、コンポーネントの Dom は作成されますが、ここには挿入されません。 この時点ではコンポーネントの init は完了していますが、コンポーネントの createComponent メソッドはまだ完了していないことに注意してください。そのロジックを完成させましょう。

関数createComponent(vnode、insertedVnodeQueue、parentElm、refElm) {
  i = vnode.data とします。
  もし(isDef(i)){
    isDef(i = i.hook) && isDef(i = i.init) の場合 {
      i(vnode) // initが完了しました}
    
    if (isDef(vnode.componentInstance)) { //コンポーネント実行時に割り当てられる init initComponent(vnode) //実際のDOMをvnode.elmに割り当てる
      
      insert(parentElm, vnode.elm, refElm) // コンポーネント Dom がここに挿入されます...
      return true // 直接返される
    }
  }
}

-----------------------------------------------------------------------

関数 initComponent(vnode) {
  ...
  vnode.elm = vnode.componentInstance.$el // __patch__ によって返される実際の DOM
  ...
}

コンポーネントがどれだけ深くネストされていても、コンポーネントに遭遇すると init が実行されます。init のパッチ処理中にネストされたコンポーネントに遭遇すると、ネストされたコンポーネントの init が再度実行されます。ネストされたコンポーネントが __patch__ を完了すると、実際の Dom がその親ノードに挿入されます。次に、外側のコンポーネントのパッチが実行された後、再び親ノードに挿入され、最後に body に挿入されて、ネストされたコンポーネントの作成プロセスが完了します。つまり、内側から外側へのプロセスです。

この写真を振り返ってみると、簡単に理解できると思います。

ここに画像の説明を挿入

次に、この章の最初の mountComponent の後のロジックを完成させます。

エクスポート関数 mountComponent(vm, el) {
  ...
  定数updateComponent = () => {
    vm._update(vm._render())
  }
  
  新しいウォッチャー(vm、updateComponent、noop、{
    前に() {
      vm._isMountedの場合{
        callHook(vm, 'beforeUpdate')
      }
    }   
  }、 真実)
  
  ...
  callHook(vm, 'マウント済み')
  
  戻り値
}

次に、updateComponent を Watcher クラスに渡します。このクラスが何を行うかについては、次の章で紹介します。 次に、マウントされたフック メソッドが実行されます。 この時点で、新しい vue のプロセス全体が完了します。 新しい Vue から実行順序を確認してみましょう。

新しい Vue ==> vm._init() ==> vm.$mount(el) ==> vm._render() ==> vm.update(vnode) 

最後に、この章を次の質問で締めくくります。

親コンポーネントと子コンポーネントは両方とも、beforeCreate、created、beforeMounte、mounted の 4 つのフックを定義します。それらの実行順序は何ですか?

答え:

まず、親コンポーネントの初期化プロセスが実行されるため、beforeCreate と created が順に実行されます。マウント前に、beforeMount フックが再度実行されます。ただし、実際の DOM を生成する __patch__ プロセスでネストされた子コンポーネントに遭遇すると、子コンポーネントの初期化フック beforeCreate と created を実行するようになります。子コンポーネントはマウント前に beforeMounte を実行し、子コンポーネントの Dom 作成が完了したら、mounted を実行します。 親コンポーネントのパッチ処理が完了し、最後に親コンポーネントのマウントされたフックが実行されるという実行順序になります。 次のように:

親 beforeCreate
親が作成した
親 beforeMounte
    子 beforeCreate
    子が作成されました
    マウント前の子供
    子供が乗る
親マウント

Vue 仮想 Dom から実 Dom への変換に関するこの記事はこれで終わりです。Vue 仮想 Dom から実 Dom への変換に関するより関連性の高いコンテンツについては、123WORDPRESS.COM の過去の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • この記事は、Vueの仮想Domとdiffアルゴリズムを理解するのに役立ちます。
  • Vue仮想DOMの原理
  • Vue 仮想 DOM の問題について
  • Vue 仮想 DOM クイックスタート
  • Vueソースコード解析における仮想DOMの詳しい説明
  • Vueにおける仮想DOMの理解のまとめ
  • Vue の仮想 DOM に関する知識ポイントのまとめ

<<:  MybatisはSQLクエリのインターセプションと変更の詳細を実装します

>>:  Linux ディスク管理 LVM の使用

推薦する

MySQL 接続失敗の一般的な障害と原因

==================================================...

Linuxシステムのログの詳細な紹介

目次1. ログ関連サービス2. システム内の共通ログファイル1. ログ関連サービスCentOS 6....

入力と画像を揃えるためにvertical-alignを使用します

input と img を同じ行に配置すると、img タグが常に input より 1 つ上になり、...

CentOS 7 で RPM を使用して mysql5.7.13 をインストールする

0. 環境この記事のオペレーティング システム: CentOS 7.2.1511 x86_64 My...

Ubuntuで余分なカーネルを削除する方法

ステップ1: 現在のカーネルを表示する 読み取る $ uname -a Linux rew 4.15...

React useMemo と useCallback の使用シナリオ

目次メモを使うコールバックの使用メモを使う親コンポーネントが再レンダリングされると、そのすべての要素...

HTML のテキストエリアの改行問題の概要

最近、Textrea に転送したときに、データが本当に行ごとに保存できるかどうかという問題に遭遇しま...

MySQLで数千万のテストデータを素早く作成する方法

述べる:この記事で扱うデータ量は 100 万です。数千万のデータが必要な場合は、量を増やすだけで済み...

W3C チュートリアル (12): W3C SOAP アクティビティ

Web サービスは、アプリケーション間の通信に関係しています。SOAP は、Web サービス間の X...

Nginx で Basic Auth ログイン認証を設定する方法

nginx でファイルサーバーを構築することもありますが、これは一般に公開されていますが、サーバーが...

Mysqlのマージ結果と水平スプライシングフィールドの実装手順

序文最近、レポート機能に取り組んでいたのですが、ある月に各部署に入社した人と退職した人の数をカウント...

ユニークインデックスの S ロックと X ロックによる MySQL デッドロック ルーチンの理解

「初心者向けソースコードからの MySQL デッドロック問題の理解」では、MySQL ソースコードを...

Dockerのクイックガイド

Docker は、安全で繰り返し可能な環境でソフトウェアを自動的にデプロイする方法を提供し、コンピュ...

nginx + fastcgi を使用して画像認識サーバーを実装する

背景ディープラーニング モデルの推論には、特定のデバイスが使用されます。マシンは、モデルの読み込み、...

Echatsチャートの大画面適応を実装する方法

目次説明する成し遂げるプロジェクトのディレクトリ構造は次のとおりです。効果図は以下のとおりです要約す...