この記事の内容は、仮想 dom 原理プロセスの分析と実装に関するものであり、必要としている友人が参考になれば幸いです。
ご存知のとおり、DOM ノードは Web ページ上で最も高価なブラウザ リソースであり、Web ページのパフォーマンスの問題のほとんどは JavaScript によって引き起こされます。 DOM を変更すると発生します。 DOM の操作には Javascript を使用しますが、DOM はツリー構造で表現されるため、DOM 内の何かが毎回変更されるため、DOM への変更は非常に高速ですが、変更された要素は非常に効率的ではありません。子はリフロー/レイアウト フェーズを経る必要があり、その後ブラウザで変更を再描画する必要がありますが、これには時間がかかります。したがって、リフロー/再描画の回数が増えるほど、アプリケーションの遅延が増大します。ただし、JavaScript は非常に高速に実行され、仮想 DOM は JS と HTML の間に配置されるレイヤーです。新旧の DOM を比較することで比較後の差異オブジェクトを取得し、実際に差異部分を目的の方法でページにレンダリングすることで、実際の DOM 操作を削減し、最終的にパフォーマンスの最適化という目的を達成できます。
簡単にまとめると 3 つのポイントがあります。
JavaScript を使用して DOM ツリーをシミュレートし、DOM ツリーをレンダリングします
古い DOM ツリーと新しい DOM ツリーを比較し、比較された差分オブジェクトを取得します。
差分オブジェクトをレンダリングされた DOM ツリーに適用します。
以下はフローチャートです:
以下では、コードを使用してフローチャートのステップを実装します。ステップごとに
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)
<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>
import { setAttr } from './utils' 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 = '' + 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) をページに挿入します。
#実際、コードではルート ノードから走査を開始し、走査時に各ノードの差分 (テキストの違い、異なる属性、異なるノードを含む) レコードが保存されます。以下に示すように:
2 つのノードの違いは次のように要約できます。たとえば、次の 2 つです。木 違いを比較して記録します。
主にインデックスを走査し (図 3 を参照)、次にルート ノードから比較を開始し、比較後の差異オブジェクトを記録して、次から続行します。左側 サブツリーを比較し、相違点を記録し、トラバースを続けます。主なプロセスは次のとおりです。 0 直接替换原有节点
1 调整子节点,包括移动、删除等
2 修改节点属性
3 修改节点文本内容
を呼び出します。
// 这是比较两个树找到最小移动量的算法是Levenshtein距离,即O(n * m) // 具体请看 https://www.npmjs.com/package/list-diff2 import listDiff from 'list-diff2' // 比较两棵树 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 番目のノードを示します。つまり、
は h3
に変更され、一部は省略 2 つの差分オブジェクト コードは掲載されていません~~次に、次のように diff.js の完全なコードを見てください。
<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">{
"1": [
{
"type": 0,
"node": {
"tagName": "h3",
"props": {
"style": "color: green"
},
"children": [
"I am H1"
],
"count": 1
}
}
]
...
}</pre><div class="contentsignin">ログイン後にコピー</div></div>
差分オブジェクトを取得したら、残るのは だけです。差分オブジェクトをノードの上の dom に適用します。
差分オブジェクトをレンダリングされた dom ツリーに適用します
実際には、ここでの作業ははるかに簡単です。上記で取得した差分オブジェクトの後、同じ深度のトラバーサルを選択します。そのノードに差分がある場合は、それが上記の 4 つのタイプのどれであるかを判断し、差分オブジェクトに従ってノードを直接変更します。
import listDiff from 'list-diff2' // 每个节点有四种变动 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 === 'string' && typeof newNode === 'string') { // 比较文本节点 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 }
path.js の完全なコードは次のとおりです:
import {REPLACE, REORDER, PROPS, TEXT} from './diff' import { setAttr } from './utils' 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 === 'string') ? 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('Unknow patch type ' + 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('key') 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 === 'object') ? move.item.render() : document.createTextNode(move.item) staticNodelist.splice(index, 0, insertNode); node.insertBefore(insertNode, node.childNodes[index] || null) } }) }
到这里,最基本的虚拟DOM原理已经讲完了,也简单了实现了一个虚拟DOM.
以上が仮想DOM原理プロセスの分析と実装の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。