nervjs
Version:
A react-like framework based on virtual-dom
345 lines (326 loc) • 8.66 kB
JavaScript
import VPatch from './vpatch'
import { isVNode, isVText, isWidget, isStateLess, isHook } from './vnode/types'
import { isFunction, isObject, getPrototype } from '~'
function diff (a, b) {
let patches = {old: a}
walk(a, b, patches, 0)
return patches
}
function walk (a, b, patches, index) {
if (a === b) {
return
}
let apply = patches[index]
let applyClear = false
if (!b) {
if (!isWidget(a)) {
clearState(a, patches, index)
apply = patches[index]
}
apply = appendPatch(apply, new VPatch(VPatch.REMOVE, a, null))
} else if (isVText(b)) {
if (!isVText(a)) {
applyClear = true
apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
} else if (a.text !== b.text) {
apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
}
} else if (isVNode(b)) {
if (!isVNode(a)) {
apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
applyClear = true
} else if (a.tagName === b.tagName && a.key === b.key) {
const propsPatch = diffProps(a.props, b.props)
if (propsPatch) {
apply = appendPatch(apply, new VPatch(VPatch.PROPS, a, propsPatch))
}
apply = diffChildren(a, b, apply, patches, index)
} else {
apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
applyClear = true
}
} else if (isWidget(b)) {
if (!isWidget(a)) {
applyClear = true
}
apply = appendPatch(apply, new VPatch(VPatch.WIDGET, a, b))
} else if (Array.isArray(b)) {
applyClear = true
b.forEach(item => {
walk(null, item, patches, index)
index++
})
} else if (isStateLess(b)) {
applyClear = true
apply = appendPatch(apply, new VPatch(VPatch.STATELESS, a, b))
}
if (apply) {
patches[index] = apply
}
if (applyClear) {
clearState(a, patches, index)
}
}
function diffProps (propsA, propsB) {
let diff = null
for (let key in propsA) {
if (!propsB.hasOwnProperty(key)) {
diff = diff || {}
diff[key] = undefined
}
let aValue = propsA[key]
let bValue = propsB[key]
if (aValue === bValue) {
continue
} else if (isObject(aValue) && isObject(bValue)) {
if (getPrototype(aValue) !== getPrototype(bValue)) {
diff = diff || {}
diff[key] = bValue
} else if (isHook(bValue)) {
diff = diff || {}
diff[key] = bValue
} else {
let objDiff = diffProps(aValue, bValue)
if (objDiff) {
diff = diff || {}
diff[key] = objDiff
}
}
} else {
diff = diff || {}
diff[key] = bValue
}
}
for (let key in propsB) {
if (!propsA.hasOwnProperty(key)) {
diff = diff || {}
diff[key] = propsB[key]
}
}
return diff
}
function diffChildren (a, b, apply, patches, index) {
const aChildren = a.children
const diffSet = diffList(aChildren, b.children, 'key')
let bChildren = diffSet.list
let len = Math.max(aChildren.length, bChildren.length)
for (let i = 0; i < len; i++) {
let leftNode = aChildren[i]
let rightNode = bChildren[i]
index += 1
if (!leftNode) {
if (rightNode) {
apply = appendPatch(apply, new VPatch(VPatch.INSERT, null, rightNode))
}
} else {
walk(leftNode, rightNode, patches, index)
}
if (isVNode(leftNode) && leftNode.count) {
index += leftNode.count
}
}
if (diffSet.moves) {
apply = appendPatch(apply, new VPatch(VPatch.ORDER, a, diffSet.moves))
}
return apply
}
function diffList (oldList, newList, key) {
const newListKeyIndex = mapListKeyIndex(newList, key)
const newListkeyMap = newListKeyIndex.keyMap
const newListFree = newListKeyIndex.free
if (newListFree.length === newList.length) {
return {
list: newList,
moves: null
}
}
const oldListKeyIndex = mapListKeyIndex(oldList, key)
const oldListkeyMap = oldListKeyIndex.keyMap
const oldListFree = oldListKeyIndex.free
if (oldListFree.length === oldList.length) {
return {
list: newList,
moves: null
}
}
let listChange = []
let freeIndex = 0
let deletedItems = 0
listChange = oldList.map((item) => {
const itemKey = item[key]
if (itemKey) {
if (newListkeyMap.hasOwnProperty(itemKey)) {
return newList[newListkeyMap[itemKey]]
}
deletedItems++
return null
}
let itemIndex = newListFree[freeIndex++]
let freeItem = newList[itemIndex]
if (!freeItem) {
deletedItems++
return null
}
return freeItem
})
let lastFreeIndex = freeIndex >= newListFree.length ? newList.length : newListFree[freeIndex]
newList.forEach((newItem, index) => {
const itemKey = newItem[key]
if (itemKey) {
if (!oldListkeyMap.hasOwnProperty(itemKey)) {
listChange.push(newItem)
}
} else if (index >= lastFreeIndex) {
listChange.push(newItem)
}
})
let simulate = listChange.slice(0)
let simulateIndex = 0
let removes = []
let inserts = []
let simulateItem
for (let k = 0; k < newList.length;) {
simulateItem = simulate[simulateIndex]
while (simulateItem === null && simulate.length) {
removes.push(remove(simulate, simulateIndex, null))
simulateItem = simulate[simulateIndex]
}
let newItem = newList[k]
let newItemKey = newItem[key]
let simulateItemKey = simulateItem[key]
if (!simulateItem || simulateItemKey !== newItemKey) {
if (newItem[key]) {
if (simulateItem && simulateItemKey) {
if (newListkeyMap[simulateItemKey] !== k + 1) {
removes.push(remove(simulate, simulateIndex, simulateItemKey))
simulateItem = simulate[simulateIndex]
if (!simulateItem || simulateItemKey !== newItemKey) {
inserts.push({key: newItemKey, to: k})
} else {
simulateIndex++
}
} else {
inserts.push({key: newItemKey, to: k})
}
} else {
inserts.push({key: newItemKey, to: k})
}
k++
} else if (simulateItem && simulateItemKey) {
removes.push(remove(simulate, simulateIndex, simulateItemKey))
}
} else {
simulateIndex++
k++
}
}
while (simulateIndex < simulate.length) {
simulateItem = simulate[simulateIndex]
removes.push(remove(simulate, simulateIndex, simulateItem && simulateItem.key))
}
if (removes.length === deletedItems && !inserts.length) {
return {
list: listChange,
moves: null
}
}
return {
list: listChange,
moves: {
removes,
inserts
}
}
}
function remove (arr, index, key) {
arr.splice(index, 1)
return {
from: index,
key
}
}
function clearState (vnode, patch, index) {
unhook(vnode, patch, index)
destroyWidgets(vnode, patch, index)
}
function unhook (vnode, patch, index) {
if (isVNode(vnode)) {
if (vnode.hooks) {
patch[index] = appendPatch(
patch[index],
new VPatch(
VPatch.PROPS,
vnode,
undefinedKeys(vnode.hooks)
)
)
}
if (vnode.descendantHooks) {
let children = vnode.children
let len = children.length
for (let i = 0; i < len; i++) {
let child = children[i]
index += 1
unhook(child, patch, index)
if (isVNode(child) && child.count) {
index += child.count
}
}
}
} else if (isStateLess(vnode)) {
index += 1
unhook(vnode._renderd, patch, index)
}
}
function destroyWidgets (vnode, patch, index) {
if (isWidget(vnode)) {
if (isFunction(vnode.destroy)) {
patch[index] = appendPatch(patch[index], new VPatch(VPatch.REMOVE, vnode, null))
}
} else if (isVNode(vnode) && vnode.hasWidgets) {
vnode.children.forEach(child => {
index += 1
destroyWidgets(child, patch, index)
if (isVNode(child) && child.count) {
index += child.count
}
})
} else if (isStateLess(vnode)) {
index += 1
destroyWidgets(vnode._renderd, patch, index)
}
}
function mapListKeyIndex (list, key) {
let keyMap = {}
let free = []
list.forEach((item, i) => {
if (item[key]) {
keyMap[item[key]] = i
} else {
free.push(i)
}
})
return {
keyMap,
free
}
}
function undefinedKeys (obj) {
let result = {}
for (let key in obj) {
result[key] = undefined
}
return result
}
function appendPatch (apply, patch) {
if (apply) {
if (Array.isArray(apply)) {
apply.push(patch)
} else {
apply = [apply, patch]
}
return apply
}
return [patch]
}
export default diff