ホームページ > ウェブフロントエンド > jsチュートリアル > 仮想DOM原理プロセスの分析と実装

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

不言
リリース: 2018-10-11 14:09:15
転載
3800 人が閲覧しました

この記事の内容は、仮想 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 style="color: pink;">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 サイトの他の関連記事を参照してください。

関連ラベル:
ソース:segmentfault.com
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート