做個簡易 Virtual DOM
以前看了別的人教學影片之後沒有很懂也沒有實際寫寫看,趁機來厄補一下
所以 Virtual DOM 是啥
-
用 JS Object 去代表 DOM Node
-
資料更新的時候,去比對新的跟舊的 virtual dom
-
把整個 tree 裡面有變化的更新到真實 DOM 上面
舉個例子
const virtualNode = {
tagName: 'div',
attributes: {
id: 'unique',
className: [
'square',
'yellow',
'bounce',
],
dataVariant: 'box'
},
children: [
'一個很讚的標題'
]
}
Virtual DOM 的運作流程
-
create element —— 把 DOM 屬性做成 object (v-node)
-
render —— 把 v-node 轉成真的 HTML Element
-
mount —— 把這個 HTML Element 更新到 DOM 上
-
diff —— 資料更新的時候,進行比對,然後做1、2類似的動作
-
patch —— 更新變動過的 HTML Element
跟 React 的 API 不一樣唷
-
create element —— 把 DOM 屬性做成 object (v-node)
-
render —— 把 v-node 轉成真的 HTML Element
-
mount —— 把這個 HTML Element 更新到 DOM 上
(useEffect 觸發時間) -
diff —— 資料更新的時候,進行比對,然後做1、2類似的動作
-
patch —— 更新變動過的 HTML Element
(useEffect 觸發時間)
createElement()
把 DOM 屬性做成 object (v-node)
重點就是三個:
1. 是什麼 HTML 元素
2. 有什麼屬性
3. children 是誰
export default function createElement(
tagName,
options
) => {
return {
tagName,
attrs: options.attrs,
children: options.children
}
}
render()
把 v-node 轉成真的 HTML Element
- 主要當然是使用 document.createElement,然後把 v-node 上面的屬性跟children 掛上去
- attr 用 setAttributes
-
children 用 appendChild,但因為 child 也會是 v-node,所以他們也要 render
React 有分好幾種 children,這裡為了簡化只分成兩種:text node & element。
export default function render(vNode) {
// if the node type is string, call create text node
if (typeof vNode === 'string') {
return document.createTextNode(vNode);
}
// create actual element
const $el = document.createElement(vNode.tagName);
// append attributes
const attrs = Object.entries(vNode.attrs);
attrs.forEach(([key, value]) => {
$el.setAttribute(key, value)
})
// append childs
const { children } = vNode;
if (children && children.length) {
children.forEach((childVNode) => {
const $child = render(childVNode);
$el.appendChild($child);
})
}
return $el
}
mount()
把 HTML Element 更新到 DOM 上
沒什麼好說的...
export default function mount($node, $target) {
$target.appendChild($node); // or replaceWith()
return $node
}
diff() & patch()
資料更新的時候,進行比對,將更新過後的東西送到 HTML 中
參考教學的話,最基本的比較方式是:
-
如果 tag name 不一樣的話,整個 v-node 換成新的
-
如果一樣,才去比對 attributes 跟 children
-
children 也進行 diff
-
最後回傳一個 patch function,call 了之後就只會更新有變化的節點
diff 回傳 patch
export default diff(vOldNode, vNewNode) {
// diff attributes and children
const patchAttrs = diffAttr(vOldNode.attrs, vNewNode.attrs);
const patchChildren = diffChildren(vOldNode.children, vNewNode.children);
// return patch
return ($node) => {
patchAttrs($node);
patchChildren($node);
}
}
tag name 不一樣的話直接替換
export default function diff(vOldNode, vNewNode) {
// tag name 不一樣的話,就可以直接替換
if (vOldNode.tagName !== vNewNode.tagName) {
return ($node) => {
const $newNode = render(vNewNode);
$node.replaceWith($newNode);
return $newNode
}
}
const patchAttrs = diffAttr(vOldNode.attrs, vNewNode.attrs);
const patchChildren = diffChildren(vOldNode.children, vNewNode.children);
return ($node) => {
patchAttrs($node);
patchChildren($node);
}
}
處理 text node 跟 empty 的狀況
export default function diff(vOldNode, vNewNode) {
// 沒有 new node 的話可以直接移除
if (!vNewNode) {
return ($node) => {
$node.remove();
return undefined
}
}
// 其中一個是 text node 的情況
if (typeof vOldNode === 'string' || typeof vNewNode === 'string') {
if (vOldNode !== vNewNode) {
return ($node) => {
const $newNode = render(vNewNode);
$node.replaceWith($newNode);
return $newNode
}
} else {
return ($node) => undefined
}
}
// tag name 不一樣的話,就可以直接替換
if (vOldNode.tagName !== vNewNode.tagName) {
return ($node) => {
const $newNode = render(vNewNode);
$node.replaceWith($newNode);
return $newNode
}
}
const patchAttrs = diffAttr(vOldNode.attrs, vNewNode.attrs);
const patchChildren = diffChildren(vOldNode.children, vNewNode.children);
return ($node) => {
patchAttrs($node);
patchChildren($node);
}
}
diffAttr:
- 把新舊 attributes 轉成 array,新的全部掛上去
- 舊的屬性如果不在新的 attr 裡面,就移除這個屬性
- 使用 set / removeAttributes
function diffAttr(vOldAttrs, vNewAttrs) {
const vOldAttrEntries = Object.entries(vOldAttrs);
const vNewAttrEntries = Object.entries(vNewAttrs);
const patches = [];
vNewAttrEntries.forEach(([key, value]) => {
const patch = ($node) => {
$node.setAttribute(key, value)
return $node
}
patches.push(patch)
})
vOldAttrEntries.forEach(([key, value]) => {
if (!(key in vNewAttrs)) {
const patch = ($node) => {
$node.removeAttribute(key)
return $node
}
patches.push(patch)
}
})
return ($node) => {
patches.forEach((patch) => {
patch($node)
})
}
}
diffChildren:
- 新舊 children diff 一遍得到 patches
- 完全新的直接掛上去
function diffChildren(vOldChildren, vNewChildren) {
const patches = [];
const newNodePatches = [];
const zippedChildren = zip(vOldChildren, vNewChildren);
zippedChildren.forEach(([vOldChild, vNewChild]) => {
if (!vOldChild) {
const vNewChildNode = render(vNewChild);
const patch = ($node) => {
$node.appendChild(vNewChildNode)
return $node
}
newNodePatches.push(patch);
}
const childrenPatch = diff(vOldChild, vNewChild);
patches.push(childrenPatch)
})
return ($parentNode) => {
const zippedPairs = zip(patches, $parentNode.childNodes);
zippedPairs.forEach(([patch, childNode]) => {
patch(childNode)
})
newNodePatches.forEach((patch) => {
patch($parentNode)
})
return $parentNode
}
}
diffChildren:
- 新舊 children diff 一遍得到 patches
- 完全新的直接掛上去
function diffChildren(vOldChildren, vNewChildren) {
const patches = [];
const newNodePatches = [];
const zippedChildren = zip(vOldChildren, vNewChildren);
zippedChildren.forEach(([vOldChild, vNewChild]) => {
if (!vOldChild) {
const vNewChildNode = render(vNewChild);
const patch = ($node) => {
$node.appendChild(vNewChildNode)
return $node
}
newNodePatches.push(patch);
}
const childrenPatch = diff(vOldChild, vNewChild);
patches.push(childrenPatch)
})
return ($parentNode) => {
const zippedPairs = zip(patches, $parentNode.childNodes);
zippedPairs.forEach(([patch, childNode]) => {
patch(childNode)
})
newNodePatches.forEach((patch) => {
patch($parentNode)
})
return $parentNode
}
}
參考:
-
這篇幫我破解了 DOM 效能的迷思
https://blog.techbridge.cc/2019/02/04/vdom-from-scratch/ -
很詳盡的介紹,有點歷史跟很多線索可以深究 react
https://www.accelebrate.com/blog/the-real-benefits-of-the-virtual-dom-in-react-js/ -
第一個參考文章作者參考的影片。看他實作真的很棒,雖然此影片聲音處理不佳。主要照著這個寫的
https://www.youtube.com/watch?v=85gJMUEcnkc&feature=youtu.be
demo source code:
end.
做個簡易 Virtual DOM
By Kai Ting Liu
做個簡易 Virtual DOM
- 286