现在主流框架都采用虚拟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
后续对应四个不同的操作进行修改即可。