現在主流框架都採用虛擬dom,爲什麼?
一個真實dom的生成代價很昂貴,不過js的運行速度很快,這樣我們通過對js的虛擬dom樹操作,通過Diff算法來對真實dom做出相應的操作,效率事倍功半。
首先看看一個虛擬dom的結構,其實就是一個js的對象,裏面包含了各種需要實現的屬性:
const ul = {
tagName: 'ul',
props: {
id: 'list'
},
children: [
{
tagName: 'li',
props: {
class: 'item'
},
children: [{
tagName: 'button',
children: ['這裏是個按鈕']
}]
},{
tagName: 'li',
children: ['li信息']
}
]
}
Virtual DOM生成真實dom:
class Element {
constructor (vdom) {
this.vitrual = vdom
const { props, children } = vdom
this.key = props ? props.key : void 666
// 這邊比較重要,DFS的標記,爲後面leftNode標記算法使用
let count = 0
children.forEach((child, i) => {
if (child instanceof Element) {
count += child.count
}
count ++
})
this.count = count
}
render () {
const { tagName, props, children } = this.vitrual
el = document.createElement(tagName)
const fregment = children ? document.createDocumentFragment() : null
children && children.map(elem => {
const child = (elem instanceof Element) ?
elem.render() : document.createTextNode(elem)
fregment.appendChild(child)
})
fregment && el.appendChild(fregment)
return el
}
}
// 遍歷virtual dom, DFS遍歷
function createTree (vdom) {
const { children } = vdom
vdom.children = children && children.map(v =>
(v instanceof Object) ? createTree(v) : v
)
return new Element(vdom)
}
diff算法:先找變化的地方,通過標記,改變真是DOM;
- 先找diff
參考react Virtual DOM的Diff思路:分爲4種;
- REPLACE 替換節點
- PROPS 修改屬性
- REORDER children的重排
- TEXT 文本內容的替換
首先通過DFS方式遍歷標記找到diff
import _ from './utils'
import listDiff from './diff-list'
var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3
function diff (oldTree, newTree) {
const index = 0 // 從0開始標記
const patches = {} // 存放所有的變更
dfsWalk(oldTree, newTree, index, patches)
return patches
}
// DFS計算diff
function dfsWalk(oldNode, newNode, index, patches) {
const currentPatch = []
if (!newNode) {}
else if (_.isString(oldNode) && _.isString(newNode)) {
if (oldNode !== newNode) {
currentPatch.push({
type: TEXT,
content: newNode
})
}
} else if (oldNode.tagName === newNode.tagName &&
oldNode.key === newNode.key) {
const propPatches = diffProps(oldNode, newNode)
propPatches && currentPatch.push({
type: PROPS,
props: propPatches
})
// 這邊不管ignore
diffChildren(
oldNode.children,
newNode.children,
index,
patches,
currentPatch
)
} else {
currentPatch.push({
type: REPLACE,
node: newNode
})
}
}
// 比較list
function diffChildren (oldChildren, newChildren, index, patches, currentPatch) {
const diffs = listDiff(oldChildren, newChildren, 'key')
newChildren = diffs.children
if (diffs.moves.length) {
currentPatch.push({ type: REORDER, moves: diffs.moves })
}
let leftNode = null
let currentNodeIndex = index
oldChildren.forEach((child, i) => {
const newChild = newChildren[i]
// 這邊要聯繫上下文,count在一開始建立virtual時
// 這邊比較重要了
currentNodeIndex = (leftNode && leftNode.count)
? currentNodeIndex + leftNode.count + 1
: currentNodeIndex + 1
dfsWalk(child, newChild, currentNodeIndex, patches)
leftNode = child
})
}
// 比較props
function diffProps (oldNode, newNode) {
const propsPatches = {}
const oldProps = oldNode.props
const newProps = newNode.props
for (const [key, val] of Object.entries(oldProps)) {
if (val !== newProps[key]) {
propsPatches[key] = val
}
}
for (const key of Object.keys(newProps)) {
if (oldProps.hasOwnProperty(key)) {
propsPatches[key] = newProps[key]
}
}
const { length } = Object.keys(propsPatches)
if (!length) return null
return propsPatches
}
export default diff
上面找出對於的diff存入patches中,在應用修改對應的diff
講講list-diff2的對比:
通過把新老的children進行對比,通過key值對比,不存在key的項存入free數組然後對應覆蓋,其他將被移除的key元素,用null代替,標記爲待移除(remove)存入moves數組,新增的key元素(待插入)加入moves數組,返回一個對象,所以在列表遍歷時候框架會提示需要傳入key,最大的原因就是爲了Virtual DOM的diff操作,這邊是一個example:
這邊第二個元素被移除用null代替,並在moves列表新增改元素的刪除操作;
type: 0 刪除 1 新增插入
然後children爲何oldChildren對應長度的列表,繼續走正常的diff操作。
找出了diff後,那就應用diff進行對真實dom進行修改:
import _ from './utils'
var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3
function patch(node, patches) {
// DFS累計標記,通過對象存儲,指向同一個地址
let walker = { index: 0 }
dfsWalker(node, walker, patches)
}
function dfsWalker (node, walker, patches) {
const currentPatches = patches[walker.index]
const childNodes = node.childNodes
childNodes.length && childNodes.forEach((v, i) => {
walker.index++
dfsWalker(v, walker, patches)
})
if (currentPatches) applyPatches(node, currentPatches)
}
function applyPatches (node, currentPatches) {
currentPatches.forEach(currentPatch => {
switch(currentPatch.type) {
case REPLACE:
setReplace(node, currentPatch)
break
case REORDER:
setReorder(node, currentPatch.moves)
break
case PROPS:
setProps(node, currentPatch.props)
break
case TEXT:
node.textContent = currentPatch.content
}
})
}
function setProps (node, props) {
for (const key of Object.keys(props)) {
!props[key] && node.removeAttribute(key)
props[key] && _.setProps(node, key, props[key])
}
}
function setReplace (node, currentPatch) {
const newNode = (typeof currentPatch.node === 'string') ?
document.createTextNode(currentPatch.node) :
currentPatch.node.render()
node.parentNode.replaceChild(newNode, node)
}
function setReorder (node, moves) {
const map = {}
const staticNodes = Array.from(node.childNodes)
staticNodes.forEach(child => {
if (child.nodeType === 1) {
const key = child.getAttribute('key')
if (key) {
map[key] = child
}
}
})
moves.forEach((move, i) => {
const { index } = move
if (move.type === 0) { // remove
// 此處判斷元素是否因爲insert操作已經被刪除
if (staticNodes[index] === node.childNodes[index]) {
node.removeChild(node.childNodes[index])
}
} else { // insert type === 1
const newNode = map[move.item.key] ? map[move.item.key].childNodes(true)
: (typeof move.item === 'object')
? move.item.render()
: document.createTextNode(move.item)
staticNodes.splice(index, 0, newNode)
node.insertBefore(newNode, node.childNodes[index] || null)
}
})
}
export default patch
後續對應四個不同的操作進行修改即可。