目次
背景
仮想 dom 原理プロセス
JavaScript を使用して DOM ツリーをシミュレートし、ページ上にレンダリングします
I am H1
このように、patch(rootnode, patches) を呼び出すと、対象を絞った方法でさまざまなノードを直接変更できます。
ホームページ ウェブフロントエンド jsチュートリアル 仮想DOM原理プロセスの分析と実装

仮想DOM原理プロセスの分析と実装

Oct 11, 2018 pm 01:52 PM
javascript

この記事の内容は、仮想 dom 原理プロセスの分析と実装に関するものであり、必要としている友人が参考になれば幸いです。

背景

ご存知のとおり、DOM ノードは Web ページ上で最も高価なブラウザ リソースであり、Web ページのパフォーマンスの問題のほとんどは JavaScript によって引き起こされます。 DOM を変更すると発生します。 DOM の操作には Javascript を使用しますが、DOM はツリー構造で表現されるため、DOM 内の何かが毎回変更されるため、DOM への変更は非常に高速ですが、変更された要素は非常に効率的ではありません。子はリフロー/レイアウト フェーズを経る必要があり、その後ブラウザで変更を再描画する必要がありますが、これには時間がかかります。したがって、リフロー/再描画の回数が増えるほど、アプリケーションの遅延が増大します。ただし、JavaScript は非常に高速に実行され、仮想 DOM は JS と HTML の間に配置されるレイヤーです。新旧の DOM を比較することで比較後の差異オブジェクトを取得し、実際に差異部分を目的の方法でページにレンダリングすることで、実際の DOM 操作を削減し、最終的にパフォーマンスの最適化という目的を達成できます。

仮想 dom 原理プロセス

簡単にまとめると 3 つのポイントがあります。

  1. JavaScript を使用して DOM ツリーをシミュレートし、DOM ツリーをレンダリングします

  2. 古い DOM ツリーと新しい DOM ツリーを比較し、比較された差分オブジェクトを取得します。

  3. 差分オブジェクトをレンダリングされた DOM ツリーに適用します。

以下はフローチャートです:

仮想DOM原理プロセスの分析と実装

以下では、コードを使用してフローチャートのステップを実装します。ステップごとに

JavaScript を使用して DOM ツリーをシミュレートし、ページ上にレンダリングします

##実際、仮想 DOM は JS オブジェクト構造のマッピングです。このプロセスをステップごとに実装してみましょう。

JS を使用して DOM ツリーの構造を簡単にシミュレートできます。たとえば、createEl(tagName, props, Children) などの関数を使用して DOM 構造を作成します。

tagName タグ名、props は属性のオブジェクト、children は子ノードです。

次に、それをページにレンダリングします。コードは次のとおりです。

const createEl = (tagName, props, children) => new CreactEl(tagName, props, children)

const vdom = createEl('p', { 'id': 'box' }, [
  createEl('h1', { style: 'color: pink' }, ['I am H1']),
  createEl('ul', {class: 'list'}, [createEl('li', ['#list1']), createEl('li', ['#list2'])]),
  createEl('p', ['I am p'])
])

const rootnode = vdom.render()
document.body.appendChild(rootnode)
ログイン後にコピー
上記の関数を通じて、vdom.render() を呼び出して、以下に示すような DOM ツリーを構築します。ページにレンダリングします

<div id="box">
  <h1 id="I-nbsp-am-nbsp-H">I am H1</h1>
  <ul class="list">
    <li>#list1</li>
    <li>#list2</li>
  </ul>
  <p>I am p</p>
</div>
ログイン後にコピー

CreactEl.js コード プロセスを見てみましょう:

import { setAttr } from &#39;./utils&#39;
class CreateEl {
  constructor (tagName, props, children) {
    // 当只有两个参数的时候 例如 celement(el, [123])
    if (Array.isArray(props)) {
      children = props
      props = {}
    }
    // tagName, props, children数据保存到this对象上
    this.tagName = tagName
    this.props = props || {}
    this.children = children || []
    this.key = props ? props.key : undefined

    let count = 0
    this.children.forEach(child => {
      if (child instanceof CreateEl) {
        count += child.count
      } else {
        child = &#39;&#39; + child
      }
      count++
    })
    // 给每一个节点设置一个count
    this.count = count
  }
  // 构建一个 dom 树
  render () {
    // 创建dom
    const el = document.createElement(this.tagName)
    const props = this.props
    // 循环所有属性,然后设置属性
    for (let [key, val] of Object.entries(props)) {
      setAttr(el, key, val)
    }
    this.children.forEach(child => {
      // 递归循环 构建tree
      let childEl = (child instanceof CreateEl) ? child.render() : document.createTextNode(child)
      el.appendChild(childEl)
    })
    return el
  }
}
ログイン後にコピー

上記の

render 関数の機能は、ノードを作成し、次にノードのプロパティを設定し、最後に再帰的に作成します。この方法で DOM ツリーを取得し、(appendChild) をページに挿入します。

古い DOM ツリーと新しい DOM ツリーを比較して、差分オブジェクトを取得します。

上記では、DOM ツリーを作成し、次に別の DOM ツリーを作成し、それを比較して差分オブジェクト オブジェクトを取得しました。 。

2 つの DOM ツリー間の差異の比較は、仮想 DOM の核心部分です。これは、よく仮想 DOM の差分アルゴリズムと呼ばれるものでもあります。2 つの完全なツリー間の差異を比較する時間計算量は O(n) です。 ^ 3)。ただし、レベル間の DOM ツリー比較が Web で使用されることはほとんどないため、あるレベルと別のレベルを比較すると、アルゴリズムの複雑さが O(n) に達する可能性があります。以下に示すように、

仮想DOM原理プロセスの分析と実装#実際、コードではルート ノードから走査を開始し、走査時に各ノードの差分 (テキストの違い、異なる属性、異なるノードを含む) レコードが保存されます。以下に示すように:

仮想DOM原理プロセスの分析と実装2 つのノードの違いは次のように要約できます。たとえば、次の 2 つです。木 違いを比較して記録します。

主にインデックスを走査し (図 3 を参照)、次にルート ノードから比較を開始し、比較後の差異オブジェクトを記録して、次から続行します。左側 サブツリーを比較し、相違点を記録し、トラバースを続けます。主なプロセスは次のとおりです。 仮想DOM原理プロセスの分析と実装

0 直接替换原有节点
1 调整子节点,包括移动、删除等
2 修改节点属性
3 修改节点文本内容
ログイン後にコピー
次に、最終的な差分オブジェクトが次のようになるまで

diff(tree, newTree)

を呼び出します。

// 这是比较两个树找到最小移动量的算法是Levenshtein距离,即O(n * m)
// 具体请看 https://www.npmjs.com/package/list-diff2
import listDiff from &#39;list-diff2&#39;
// 比较两棵树
function diff (oldTree, newTree) {
  // 节点的遍历顺序
  let index = 0
  // 在遍历过程中记录节点的差异
  let patches = {}
  // 深度优先遍历两棵树
  deepTraversal(oldTree, newTree, index, patches)
  // 得到的差异对象返回出去
  return patches
}

function deepTraversal(oldNode, newNode, index, patches) {
  let currentPatch = []
  // ...中间有很多对patches的处理
  // 递归比较子节点是否相同
  diffChildren(oldNode.children, newNode.children, index, patches, currentPatch)
  if (currentPatch.length) {
    // 那个index节点的差异记录下来
    patches[index] = currentPatch
  }
}

// 子数的diff
function diffChildren (oldChildren, newChildren, index, patches, currentPatch) {
  const diffs = listDiff(oldChildren, newChildren)
  newChildren = diffs.children
  // ...省略记录差异对象
  let leftNode = null
  let currentNodeIndex = index
  oldChildren.forEach((child, i) => {
    const newChild = newChildren[i]
    // index相加
    currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1
    // 深度遍历,递归
    deepTraversal(child, newChild, currentNodeIndex, patches)
    // 从左树开始
    leftNode = child
  })
}
ログイン後にコピー

key はそのノードを表します。ここでは 2 番目のノードを示します。つまり、

h1

h3 に変更され、一部は省略 2 つの差分オブジェクト コードは掲載されていません~~次に、次のように diff.js の完全なコードを見てください。<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">{ &quot;1&quot;: [ { &quot;type&quot;: 0, &quot;node&quot;: { &quot;tagName&quot;: &quot;h3&quot;, &quot;props&quot;: { &quot;style&quot;: &quot;color: green&quot; }, &quot;children&quot;: [ &quot;I am H1&quot; ], &quot;count&quot;: 1 } } ] ... }</pre><div class="contentsignin">ログイン後にコピー</div></div>差分オブジェクトを取得したら、残るのは だけです。差分オブジェクトをノードの上の dom に適用します。

差分オブジェクトをレンダリングされた dom ツリーに適用します

実際には、ここでの作業ははるかに簡単です。上記で取得した差分オブジェクトの後、同じ深度のトラバーサルを選択します。そのノードに差分がある場合は、それが上記の 4 つのタイプのどれであるかを判断し、差分オブジェクトに従ってノードを直接変更します。

import listDiff from &#39;list-diff2&#39;
// 每个节点有四种变动
export const REPLACE = 0 // 替换原有节点
export const REORDER = 1 // 调整子节点,包括移动、删除等
export const PROPS = 2 // 修改节点属性
export const TEXT = 3 // 修改节点文本内容

export function diff (oldTree, newTree) {
  // 节点的遍历顺序
  let index = 0
  // 在遍历过程中记录节点的差异
  let patches = {}
  // 深度优先遍历两棵树
  deepTraversal(oldTree, newTree, index, patches)
  // 得到的差异对象返回出去
  return patches
}

function deepTraversal(oldNode, newNode, index, patches) {
  let currentPatch = []
  if (newNode === null) { // 如果新节点没有的话直接不用比较了
    return
  }
  if (typeof oldNode === &#39;string&#39; && typeof newNode === &#39;string&#39;) {
    // 比较文本节点
    if (oldNode !== newNode) {
      currentPatch.push({
        type: TEXT,
        content: newNode
      })
    }
  } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
    // 节点类型相同
    // 比较节点的属性是否相同
    let propasPatches = diffProps(oldNode, newNode)
    if (propasPatches) {
      currentPatch.push({
        type: PROPS,
        props: propsPatches
      })
    }
    // 递归比较子节点是否相同
    diffChildren(oldNode.children, newNode.children, index, patches, currentPatch)
  } else {
    // 节点不一样,直接替换
    currentPatch.push({ type: REPLACE, node: newNode })
  }

  if (currentPatch.length) {
    // 那个index节点的差异记录下来
    patches[index] = currentPatch
  }

}

// 子数的diff
function diffChildren (oldChildren, newChildren, index, patches, currentPatch) {
  var diffs = listDiff(oldChildren, newChildren)
  newChildren = diffs.children
  // 如果调整子节点,包括移动、删除等的话
  if (diffs.moves.length) {
    var reorderPatch = {
      type: REORDER,
      moves: diffs.moves
    }
    currentPatch.push(reorderPatch)
  }

  var leftNode = null
  var currentNodeIndex = index
  oldChildren.forEach((child, i) => {
    var newChild = newChildren[i]
    // index相加
    currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1
    // 深度遍历,从左树开始
    deepTraversal(child, newChild, currentNodeIndex, patches)
    // 从左树开始
    leftNode = child
  })
}

// 记录属性的差异
function diffProps (oldNode, newNode) {
  let count = 0 // 声明一个有没没有属性变更的标志
  const oldProps = oldNode.props
  const newProps = newNode.props
  const propsPatches = {}

  // 找出不同的属性
  for (let [key, val] of Object.entries(oldProps)) {
    // 新的不等于旧的
    if (newProps[key] !== val) {
      count++
      propsPatches[key] = newProps[key]
    }
  }
  // 找出新增的属性
  for (let [key, val] of Object.entries(newProps)) {
    if (!oldProps.hasOwnProperty(key)) {
      count++
      propsPatches[key] = val
    }
  }
  // 没有新增 也没有不同的属性 直接返回null
  if (count === 0) {
    return null
  }

  return propsPatches
}
ログイン後にコピー

このように、patch(rootnode, patches) を呼び出すと、対象を絞った方法でさまざまなノードを直接変更できます。

path.js の完全なコードは次のとおりです:

import {REPLACE, REORDER, PROPS, TEXT} from &#39;./diff&#39;
import { setAttr } from &#39;./utils&#39;

export function patch (node, patches) {
  // 也是从0开始
  const step = {
    index: 0
  }
  // 深度遍历
  deepTraversal(node, step, patches)
}

// 深度优先遍历dom结构
function deepTraversal(node, step, patches) {
  // 拿到当前差异对象
  const currentPatches = patches[step.index]
  const len = node.childNodes ? node.childNodes.length : 0
  for (let i = 0; i < len; i++) {
    const child = node.childNodes[i]
    step.index++
    deepTraversal(child, step, patches)
  }
  //如果当前节点存在差异
  if (currentPatches) {
    // 把差异对象应用到当前节点上
    applyPatches(node, currentPatches)
  }
}

// 把差异对象应用到当前节点上
function applyPatches(node, currentPatches) {
  currentPatches.forEach(currentPatch => {
    switch (currentPatch.type) {
      // 0: 替换原有节点
      case REPLACE:
        var newNode = (typeof currentPatch.node === &#39;string&#39;) ?  document.createTextNode(currentPatch.node) : currentPatch.node.render()
        node.parentNode.replaceChild(newNode, node)
        break
      // 1: 调整子节点,包括移动、删除等
      case REORDER: 
        moveChildren(node, currentPatch.moves)
        break
      // 2: 修改节点属性
      case PROPS:
        for (let [key, val] of Object.entries(currentPatch.props)) {
          if (val === undefined) {
            node.removeAttribute(key)
          } else {
            setAttr(node, key, val)
          }
        }
        break;
      // 3:修改节点文本内容
      case TEXT:
        if (node.textContent) {
          node.textContent = currentPatch.content
        } else {
          node.nodeValue = currentPatch.content
        }
        break;
      default: 
        throw new Error(&#39;Unknow patch type &#39; + currentPatch.type);
    }
  })
}

// 调整子节点,包括移动、删除等
function moveChildren (node, moves) {
  let staticNodelist = Array.from(node.childNodes)
  const maps = {}
  staticNodelist.forEach(node => {
    if (node.nodeType === 1) {
      const key = node.getAttribute(&#39;key&#39;)
      if (key) {
        maps[key] = node
      }
    }
  })
  moves.forEach(move => {
    const index = move.index
    if (move.type === 0) { // 变动类型为删除的节点
      if (staticNodeList[index] === node.childNodes[index]) {
        node.removeChild(node.childNodes[index]);
      }
      staticNodeList.splice(index, 1);
    } else {
      let insertNode = maps[move.item.key] 
          ? maps[move.item.key] : (typeof move.item === &#39;object&#39;) 
          ? move.item.render() : document.createTextNode(move.item)
      staticNodelist.splice(index, 0, insertNode);
      node.insertBefore(insertNode, node.childNodes[index] || null)
    }
  })
}
ログイン後にコピー

到这里,最基本的虚拟DOM原理已经讲完了,也简单了实现了一个虚拟DOM.

以上が仮想DOM原理プロセスの分析と実装の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

Video Face Swap

Video Face Swap

完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

WebSocket と JavaScript を使用してオンライン音声認識システムを実装する方法 WebSocket と JavaScript を使用してオンライン音声認識システムを実装する方法 Dec 17, 2023 pm 02:54 PM

WebSocket と JavaScript を使用してオンライン音声認識システムを実装する方法 はじめに: 技術の継続的な発展により、音声認識技術は人工知能の分野の重要な部分になりました。 WebSocket と JavaScript をベースとしたオンライン音声認識システムは、低遅延、リアルタイム、クロスプラットフォームという特徴があり、広く使用されるソリューションとなっています。この記事では、WebSocket と JavaScript を使用してオンライン音声認識システムを実装する方法を紹介します。

WebSocket と JavaScript: リアルタイム監視システムを実装するための主要テクノロジー WebSocket と JavaScript: リアルタイム監視システムを実装するための主要テクノロジー Dec 17, 2023 pm 05:30 PM

WebSocketとJavaScript:リアルタイム監視システムを実現するためのキーテクノロジー はじめに: インターネット技術の急速な発展に伴い、リアルタイム監視システムは様々な分野で広く利用されています。リアルタイム監視を実現するための重要なテクノロジーの 1 つは、WebSocket と JavaScript の組み合わせです。この記事では、リアルタイム監視システムにおける WebSocket と JavaScript のアプリケーションを紹介し、コード例を示し、その実装原理を詳しく説明します。 1.WebSocketテクノロジー

JavaScript と WebSocket を使用してリアルタイムのオンライン注文システムを実装する方法 JavaScript と WebSocket を使用してリアルタイムのオンライン注文システムを実装する方法 Dec 17, 2023 pm 12:09 PM

JavaScript と WebSocket を使用してリアルタイム オンライン注文システムを実装する方法の紹介: インターネットの普及とテクノロジーの進歩に伴い、ますます多くのレストランがオンライン注文サービスを提供し始めています。リアルタイムのオンライン注文システムを実装するには、JavaScript と WebSocket テクノロジを使用できます。 WebSocket は、TCP プロトコルをベースとした全二重通信プロトコルで、クライアントとサーバー間のリアルタイム双方向通信を実現します。リアルタイムオンラインオーダーシステムにおいて、ユーザーが料理を選択して注文するとき

WebSocketとJavaScriptを使ったオンライン予約システムの実装方法 WebSocketとJavaScriptを使ったオンライン予約システムの実装方法 Dec 17, 2023 am 09:39 AM

WebSocket と JavaScript を使用してオンライン予約システムを実装する方法 今日のデジタル時代では、ますます多くの企業やサービスがオンライン予約機能を提供する必要があります。効率的かつリアルタイムのオンライン予約システムを実装することが重要です。この記事では、WebSocket と JavaScript を使用してオンライン予約システムを実装する方法と、具体的なコード例を紹介します。 1. WebSocket とは何ですか? WebSocket は、単一の TCP 接続における全二重方式です。

JavaScript と WebSocket: 効率的なリアルタイム天気予報システムの構築 JavaScript と WebSocket: 効率的なリアルタイム天気予報システムの構築 Dec 17, 2023 pm 05:13 PM

JavaScript と WebSocket: 効率的なリアルタイム天気予報システムの構築 はじめに: 今日、天気予報の精度は日常生活と意思決定にとって非常に重要です。テクノロジーの発展に伴い、リアルタイムで気象データを取得することで、より正確で信頼性の高い天気予報を提供できるようになりました。この記事では、JavaScript と WebSocket テクノロジを使用して効率的なリアルタイム天気予報システムを構築する方法を学びます。この記事では、具体的なコード例を通じて実装プロセスを説明します。私たちは

簡単な JavaScript チュートリアル: HTTP ステータス コードを取得する方法 簡単な JavaScript チュートリアル: HTTP ステータス コードを取得する方法 Jan 05, 2024 pm 06:08 PM

JavaScript チュートリアル: HTTP ステータス コードを取得する方法、特定のコード例が必要です 序文: Web 開発では、サーバーとのデータ対話が頻繁に発生します。サーバーと通信するとき、多くの場合、返された HTTP ステータス コードを取得して操作が成功したかどうかを判断し、さまざまなステータス コードに基づいて対応する処理を実行する必要があります。この記事では、JavaScript を使用して HTTP ステータス コードを取得する方法を説明し、いくつかの実用的なコード例を示します。 XMLHttpRequestの使用

JavaScriptでinsertBeforeを使用する方法 JavaScriptでinsertBeforeを使用する方法 Nov 24, 2023 am 11:56 AM

使用法: JavaScript では、insertBefore() メソッドを使用して、DOM ツリーに新しいノードを挿入します。このメソッドには、挿入される新しいノードと参照ノード (つまり、新しいノードが挿入されるノード) の 2 つのパラメータが必要です。

JavaScript と WebSocket: 効率的なリアルタイム画像処理システムの構築 JavaScript と WebSocket: 効率的なリアルタイム画像処理システムの構築 Dec 17, 2023 am 08:41 AM

JavaScript は Web 開発で広く使用されているプログラミング言語であり、WebSocket はリアルタイム通信に使用されるネットワーク プロトコルです。 2 つの強力な機能を組み合わせることで、効率的なリアルタイム画像処理システムを構築できます。この記事では、JavaScript と WebSocket を使用してこのシステムを実装する方法と、具体的なコード例を紹介します。まず、リアルタイム画像処理システムの要件と目標を明確にする必要があります。リアルタイムの画像データを収集できるカメラ デバイスがあるとします。

See all articles