以下是 TS 实现
type PropValue = string | number | boolean;
interface Props {
[key: string]: PropValue
}
interface VNode {
tagName: string,
props: Props,
children: Array<VNode | string>,
key?: string | number
}
interface Patch {
type: string,
node?: VNode,
content?: string,
props?: Array<{ key: string, value: PropValue }>,
moves?: Array<{ index: number, type: string, from?: number, to?: number }>
}
function diff(oldTree: VNode, newTree: VNode): Patch[] {
let index = 0;
let patches: Patch[] = [];
walk(oldTree, newTree, index, patches);
return patches;
}
function walk(oldNode: VNode, newNode: VNode, index: number, patches: Patch[]) {
let current: Patch[] = [];
if (newNode == null) {
// Node has been removed
} else if (typeof oldNode === 'string' && typeof newNode === 'string') {
// Text content has changed
if (newNode !== oldNode) {
current.push({ type: 'TEXT', content: newNode });
}
} else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
// Node is the same, but properties may have changed
let props = diffProps(oldNode.props, newNode.props);
if (props.length > 0) {
current.push({ type: 'PROPS', props: props });
}
diffChildren(oldNode.children, newNode.children, index, patches, current);
} else {
// Node is different, need to replace old node with new node
current.push({ type: 'REPLACE', node: newNode });
}
if (current.length > 0) {
patches[index] = current;
}
}
function diffProps(oldProps: Props, newProps: Props): Array<{ key: string, value: PropValue }> {
let props: Array<{ key: string, value: PropValue }> = [];
for (let key in oldProps) {
if (oldProps.hasOwnProperty(key) && !newProps.hasOwnProperty(key)) {
props.push({ key: key, value: undefined });
}
}
for (let key in newProps) {
if (newProps.hasOwnProperty(key)) {
let oldValue = oldProps[key];
let newValue = newProps[key];
if (oldValue !== newValue) {
props.push({ key: key, value: newValue });
}
}
}
return props;
}
function diffChildren(oldChildren: Array<VNode | string>, newChildren: Array<VNode | string>, index: number, patches: Patch[], current: Patch[]) {
let diffs = listDiff(oldChildren, newChildren, 'key');
newChildren = diffs.children;
if (diffs.moves.length > 0) {
let reorderPatch = { type: 'REORDER', moves: diffs.moves };
current.push(reorderPatch);
}
let leftNode = null;
let currentNodeIndex = index;
oldChildren.forEach(function(oldChild, i) {
let newChild = newChildren[i];
currentNodeIndex = (leftNode && isVNode(leftNode)) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1;
walk(oldChild, newChild, currentNodeIndex, patches);
leftNode = oldChild;
});
}
function isVNode(node: VNode | string): node is VNode {
return typeof node !== 'string';
}
function listDiff(oldList: Array<VNode | string>, newList: Array<VNode | string>, key: string): { moves: Array<{ index: number, type: string, from?: number, to?: number }>, children: Array<VNode | string> } {
let oldMap = makeKeyIndexAndFree(oldList, key);
let newMap = makeKeyIndexAndFree(newList, key);
let newFreeList = newMap.freeList;
let moves = [];
let children = [];
let i = 0;
let freeIndex = 0;
while (i < oldList.length) {
let item = oldList[i];
let itemIndex = getItemIndex(item, newMap, key);
if (itemIndex != null) {
children.push(newList[itemIndex]);
i++;
freeIndex = itemIndex;
} else {
let freeItem = newFreeList[freeIndex++];
children.push(freeItem || null);
moves.push({ index: i, type: 'REMOVE' });
i++;
}
}
let lastIndex = 0;
newList.forEach(function(item, i) {
let itemIndex = getItemIndex(item, oldMap, key);
if (itemIndex == null) {
moves.push({ index: i, type: 'INSERT' });
children.splice(i, 0, null);
} else {
if (itemIndex < lastIndex) {
moves.push({ index: i, type: 'MOVE', from: itemIndex, to: lastIndex });
}
lastIndex = Math.max(lastIndex, itemIndex);
}
});
return {
moves: moves,
children: children
};
}
function makeKeyIndexAndFree(list: Array<VNode | string>, key: string): { keyIndex: { [key: string]: number }, freeList: Array<VNode | string> } {
let keyIndex = {};
let freeList = [];
for (let i = 0; i < list.length; i++) {
let item = list[i];
let itemKey = isVNode(item) ? item[key] : undefined;
if (itemKey != null) {
keyIndex[itemKey] = i;
} else {
freeList.push(item);
}
}
return {
keyIndex: keyIndex,
freeList: freeList
};
}
function getItemIndex(item: VNode | string, map: { keyIndex: { [key: string]: number }, freeList: Array<VNode | string> }, key: string): number {
let itemKey = isVNode(item) ? item[key] : undefined;
if (itemKey != null) {
return map.keyIndex[itemKey];
} else {
return null;
}
}
以下是通过 JS 实现
function diff(oldTree, newTree) {
var index = 0;
var patches = {};
walk(oldTree, newTree, index, patches);
return patches;
}
function walk(oldNode, newNode, index, patches) {
var current = [];
if (newNode == null) {
// Node has been removed
} else if (typeof oldNode === 'string' && typeof newNode === 'string') {
// Text content has changed
if (newNode !== oldNode) {
current.push({ type: 'TEXT', content: newNode });
}
} else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
// Node is the same, but properties may have changed
var props = diffProps(oldNode.props, newNode.props);
if (props.length > 0) {
current.push({ type: 'PROPS', props: props });
}
diffChildren(oldNode.children, newNode.children, index, patches, current);
} else {
// Node is different, need to replace old node with new node
current.push({ type: 'REPLACE', node: newNode });
}
if (current.length > 0) {
patches[index] = current;
}
}
function diffProps(oldProps, newProps) {
var props = [];
for (var key in oldProps) {
if (oldProps.hasOwnProperty(key) && !newProps.hasOwnProperty(key)) {
props.push({ key: key, value: undefined });
}
}
for (var key in newProps) {
if (newProps.hasOwnProperty(key)) {
var oldValue = oldProps[key];
var newValue = newProps[key];
if (oldValue !== newValue) {
props.push({ key: key, value: newValue });
}
}
}
return props;
}
function diffChildren(oldChildren, newChildren, index, patches, current) {
var diffs = listDiff(oldChildren, newChildren, 'key');
newChildren = diffs.children;
if (diffs.moves.length > 0) {
var reorderPatch = { type: 'REORDER', moves: diffs.moves };
current.push(reorderPatch);
}
var leftNode = null;
var currentNodeIndex = index;
oldChildren.forEach(function(oldChild, i) {
var newChild = newChildren[i];
currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1;
walk(oldChild, newChild, currentNodeIndex, patches);
leftNode = oldChild;
});
}
function listDiff(oldList, newList, key) {
var oldMap = makeKeyIndexAndFree(oldList, key);
var newMap = makeKeyIndexAndFree(newList, key);
var newFreeList = newMap.freeList;
var moves = [];
var children = [];
var i = 0;
var freeIndex = 0;
while (i < oldList.length) {
var item = oldList[i];
var itemIndex = getItemIndex(item, newMap, key);
if (itemIndex != null) {
children.push(newList[itemIndex]);
i++;
freeIndex = itemIndex;
} else {
var freeItem = newFreeList[freeIndex++];
children.push(freeItem || null);
moves.push({ index: i, type: 'REMOVE' });
i++;
}
}
var lastIndex = 0;
newList.forEach(function(item, i) {
var itemIndex = getItemIndex(item, oldMap, key);
if (itemIndex == null) {
moves.push({ index: i, type: 'INSERT' });
children.splice(i, 0, null);
} else {
if (itemIndex < lastIndex) {
moves.push({ index: i, type: 'MOVE', from: itemIndex, to: lastIndex });
}
lastIndex = Math.max(lastIndex, itemIndex);
}
});
return {
moves: moves,
children: children
};
}
function makeKeyIndexAndFree(list, key) {
var keyIndex = {};
var freeList = [];
for (var i = 0; i < list.length; i++) {
var item = list[i];
var itemKey = item[key];
if (itemKey != null) {
keyIndex[itemKey] = i;
} else {
freeList.push(item);
}
}
return {
keyIndex: keyIndex,
freeList: freeList
};
}
function getItemIndex(item, map, key) {
var itemKey = item[key];
if (itemKey != null) {
return map.keyIndex[itemKey];
} else {
return null;
}
}