rewrite mutation observer handler with lazy child list calculation
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rrweb",
|
"name": "rrweb",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "record and replay the web",
|
"description": "record and replay the web",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/module.js",
|
"module": "dist/module.js",
|
||||||
@@ -40,6 +40,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mitt": "^1.1.3",
|
"mitt": "^1.1.3",
|
||||||
"rrweb-snapshot": "^0.5.2"
|
"rrweb-snapshot": "0.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default [
|
|||||||
file: './dist/record/module.js',
|
file: './dist/record/module.js',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'record1',
|
name: 'record',
|
||||||
format: 'iife',
|
format: 'iife',
|
||||||
file: './dist/record/browser.js',
|
file: './dist/record/browser.js',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import {
|
|||||||
} from '../utils';
|
} from '../utils';
|
||||||
import {
|
import {
|
||||||
mutationCallBack,
|
mutationCallBack,
|
||||||
textMutation,
|
|
||||||
attributeMutation,
|
|
||||||
removedNodeMutation,
|
removedNodeMutation,
|
||||||
addedNodeMutation,
|
addedNodeMutation,
|
||||||
observerParam,
|
observerParam,
|
||||||
@@ -24,14 +22,40 @@ import {
|
|||||||
inputValue,
|
inputValue,
|
||||||
inputCallback,
|
inputCallback,
|
||||||
hookResetter,
|
hookResetter,
|
||||||
|
textCursor,
|
||||||
|
attributeCursor,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutation observer will merge several mutations into an array and pass
|
||||||
|
* it to the callback function, this may make tracing added nodes hard.
|
||||||
|
* For example, if we append an element el_1 into body, and then append
|
||||||
|
* another element el_2 into el_1, these two mutations may be passed to the
|
||||||
|
* callback function together when the two operations were done.
|
||||||
|
* Generally we need trace child nodes of newly added node, but in this
|
||||||
|
* case if we count el_2 as el_1's child node in the first mutation record,
|
||||||
|
* then we will count el_2 again in the secoond mutation record which was
|
||||||
|
* duplicated.
|
||||||
|
* To avoid of duplicate counting added nodes, we will use a Set to store
|
||||||
|
* added nodes and its child nodes during iterate mutation records. Then
|
||||||
|
* collect added nodes from the Set which will has no duplicate copy. But
|
||||||
|
* this also cause newly added node will not be serialized with id ASAP,
|
||||||
|
* which means all the id related calculation should be lazy too.
|
||||||
|
* @param cb mutationCallBack
|
||||||
|
*/
|
||||||
function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
||||||
const observer = new MutationObserver(mutations => {
|
const observer = new MutationObserver(mutations => {
|
||||||
const texts: textMutation[] = [];
|
const texts: textCursor[] = [];
|
||||||
const attributes: attributeMutation[] = [];
|
const attributes: attributeCursor[] = [];
|
||||||
const removes: removedNodeMutation[] = [];
|
const removes: removedNodeMutation[] = [];
|
||||||
const adds: addedNodeMutation[] = [];
|
const adds: addedNodeMutation[] = [];
|
||||||
|
const dropped: Node[] = [];
|
||||||
|
|
||||||
|
const addsSet = new Set<Node>();
|
||||||
|
const genAdds = (n: Node) => {
|
||||||
|
addsSet.add(n);
|
||||||
|
n.childNodes.forEach(childN => genAdds(childN));
|
||||||
|
};
|
||||||
mutations.forEach(mutation => {
|
mutations.forEach(mutation => {
|
||||||
const {
|
const {
|
||||||
type,
|
type,
|
||||||
@@ -40,17 +64,14 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
|||||||
addedNodes,
|
addedNodes,
|
||||||
removedNodes,
|
removedNodes,
|
||||||
attributeName,
|
attributeName,
|
||||||
nextSibling,
|
|
||||||
previousSibling,
|
|
||||||
} = mutation;
|
} = mutation;
|
||||||
const id = mirror.getId(target as INode);
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'characterData': {
|
case 'characterData': {
|
||||||
const value = target.textContent;
|
const value = target.textContent;
|
||||||
if (value !== oldValue) {
|
if (value !== oldValue) {
|
||||||
texts.push({
|
texts.push({
|
||||||
id,
|
|
||||||
value,
|
value,
|
||||||
|
node: target,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -60,12 +81,12 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
|||||||
if (value === oldValue) {
|
if (value === oldValue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let item: attributeMutation | undefined = attributes.find(
|
let item: attributeCursor | undefined = attributes.find(
|
||||||
a => a.id === id,
|
a => a.node === target,
|
||||||
);
|
);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
item = {
|
item = {
|
||||||
id,
|
node: target,
|
||||||
attributes: {},
|
attributes: {},
|
||||||
};
|
};
|
||||||
attributes.push(item);
|
attributes.push(item);
|
||||||
@@ -74,23 +95,25 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
|||||||
item.attributes[attributeName!] = value;
|
item.attributes[attributeName!] = value;
|
||||||
}
|
}
|
||||||
case 'childList': {
|
case 'childList': {
|
||||||
addedNodes.forEach(n => {
|
addedNodes.forEach(n => genAdds(n));
|
||||||
adds.push({
|
|
||||||
parentId: id,
|
|
||||||
previousId: !previousSibling
|
|
||||||
? previousSibling
|
|
||||||
: mirror.getId(previousSibling as INode),
|
|
||||||
nextId: !nextSibling
|
|
||||||
? nextSibling
|
|
||||||
: mirror.getId(nextSibling as INode),
|
|
||||||
node: serializeNodeWithId(n, document, mirror.map)!,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
removedNodes.forEach(n => {
|
removedNodes.forEach(n => {
|
||||||
removes.push({
|
// removed node has not been serialized yet, just remove it from the Set
|
||||||
parentId: id,
|
if (addsSet.has(n)) {
|
||||||
id: mirror.getId(n as INode),
|
addsSet.delete(n);
|
||||||
});
|
dropped.push(n);
|
||||||
|
} else if (addsSet.has(target) && !mirror.getId(n as INode)) {
|
||||||
|
/**
|
||||||
|
* If target was newly added and removed child node was
|
||||||
|
* not serialized, it means the child node has been removed
|
||||||
|
* before callback fired, so we can ignore it.
|
||||||
|
* TODO: verify this
|
||||||
|
*/
|
||||||
|
} else {
|
||||||
|
removes.push({
|
||||||
|
parentId: mirror.getId(target as INode),
|
||||||
|
id: mirror.getId(n as INode),
|
||||||
|
});
|
||||||
|
}
|
||||||
mirror.removeNodeFromMap(n as INode);
|
mirror.removeNodeFromMap(n as INode);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@@ -99,10 +122,40 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Array.from(addsSet).forEach(n => {
|
||||||
|
if (n.parentNode && dropped.indexOf(n.parentNode) < 0) {
|
||||||
|
adds.push({
|
||||||
|
parentId: mirror.getId(n.parentNode as INode),
|
||||||
|
previousId: !n.previousSibling
|
||||||
|
? n.previousSibling
|
||||||
|
: mirror.getId(n.previousSibling as INode),
|
||||||
|
nextId: !n.nextSibling
|
||||||
|
? n.nextSibling
|
||||||
|
: mirror.getId(n.nextSibling as INode),
|
||||||
|
node: serializeNodeWithId(n, document, mirror.map, true)!,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dropped.push(n);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
cb({
|
cb({
|
||||||
texts,
|
texts: texts.map(text => ({
|
||||||
attributes,
|
id: mirror.getId(text.node as INode),
|
||||||
removes,
|
value: text.value,
|
||||||
|
})),
|
||||||
|
attributes: attributes.map(attribute => ({
|
||||||
|
id: mirror.getId(attribute.node as INode),
|
||||||
|
attributes: attribute.attributes,
|
||||||
|
})),
|
||||||
|
removes: removes.map(remove => {
|
||||||
|
if (remove.parentNode) {
|
||||||
|
remove.parentId = mirror.getId(remove.parentNode as INode);
|
||||||
|
delete remove.parentNode;
|
||||||
|
}
|
||||||
|
return remove;
|
||||||
|
}),
|
||||||
adds,
|
adds,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ export class Replayer {
|
|||||||
mutation.node,
|
mutation.node,
|
||||||
this.iframe.contentDocument!,
|
this.iframe.contentDocument!,
|
||||||
mirror.map,
|
mirror.map,
|
||||||
|
true,
|
||||||
) as Node;
|
) as Node;
|
||||||
const parent = (mirror.getNode(mutation.parentId) as Node) as Element;
|
const parent = (mirror.getNode(mutation.parentId) as Node) as Element;
|
||||||
if (mutation.nextId) {
|
if (mutation.nextId) {
|
||||||
@@ -213,8 +214,13 @@ export class Replayer {
|
|||||||
});
|
});
|
||||||
d.removes.forEach(mutation => {
|
d.removes.forEach(mutation => {
|
||||||
const target = (mirror.getNode(mutation.id) as Node) as Element;
|
const target = (mirror.getNode(mutation.id) as Node) as Element;
|
||||||
const parent = (mirror.getNode(mutation.parentId) as Node) as Element;
|
const parent = (mirror.getNode(
|
||||||
parent.removeChild(target);
|
mutation.parentId!,
|
||||||
|
) as Node) as Element;
|
||||||
|
// target may be removed with its parents before
|
||||||
|
if (parent) {
|
||||||
|
parent.removeChild(target);
|
||||||
|
}
|
||||||
delete mirror.map[mutation.id];
|
delete mirror.map[mutation.id];
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|||||||
13
src/types.ts
13
src/types.ts
@@ -110,11 +110,21 @@ export type observerParam = {
|
|||||||
inputCb: inputCallback;
|
inputCb: inputCallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type textCursor = {
|
||||||
|
node: Node;
|
||||||
|
value: string | null;
|
||||||
|
};
|
||||||
export type textMutation = {
|
export type textMutation = {
|
||||||
id: number;
|
id: number;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type attributeCursor = {
|
||||||
|
node: Node;
|
||||||
|
attributes: {
|
||||||
|
[key: string]: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
export type attributeMutation = {
|
export type attributeMutation = {
|
||||||
id: number;
|
id: number;
|
||||||
attributes: {
|
attributes: {
|
||||||
@@ -123,7 +133,8 @@ export type attributeMutation = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type removedNodeMutation = {
|
export type removedNodeMutation = {
|
||||||
parentId: number;
|
parentId?: number;
|
||||||
|
parentNode?: Node;
|
||||||
id: number;
|
id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
listenerHandler,
|
listenerHandler,
|
||||||
hookResetter,
|
hookResetter,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { INode } from 'rrweb-snapshot';
|
||||||
|
|
||||||
export function on(
|
export function on(
|
||||||
type: string,
|
type: string,
|
||||||
@@ -26,6 +27,11 @@ export const mirror: Mirror = {
|
|||||||
removeNodeFromMap(n) {
|
removeNodeFromMap(n) {
|
||||||
const id = n.__sn && n.__sn.id;
|
const id = n.__sn && n.__sn.id;
|
||||||
delete mirror.map[id];
|
delete mirror.map[id];
|
||||||
|
if (n.childNodes) {
|
||||||
|
n.childNodes.forEach(child =>
|
||||||
|
mirror.removeNodeFromMap((child as Node) as INode),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user