feat(editor): 新增依赖收集器

This commit is contained in:
roymondchen 2023-03-27 19:07:56 +08:00
parent 3b3fbb288d
commit 35f9a59f44
4 changed files with 630 additions and 0 deletions

View File

@ -0,0 +1,328 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2023 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter } from 'events';
import { reactive } from 'vue';
import { MNode } from '@tmagic/schema';
type IsTarget = (key: string | number, value: any) => boolean;
interface TargetOptions {
isTarget: IsTarget;
id: string | number;
type?: string;
name: string;
}
interface Dep {
[key: string | number]: {
name: string;
keys: (string | number)[];
};
}
interface TargetList {
[key: string]: {
[key: string | number]: Target;
};
}
/**
*
*
*/
export class Target extends EventEmitter {
/**
*
*/
public isTarget: IsTarget;
/**
* id
* id
*/
public id: string | number;
/**
*
*/
public name: string;
/**
* type
*/
public type = 'default';
/**
*
* { 'node_id': { name: 'node_name', keys: [ created, mounted ] } }
*/
public deps = reactive<Dep>({});
constructor(options: TargetOptions) {
super();
this.isTarget = options.isTarget;
this.id = options.id;
this.name = options.name;
if (options.type) {
this.type = options.type;
}
}
/**
*
* @param node
* @param key key配置了这个目标的id
*/
public updateDep(node: MNode, key: string | number) {
const dep = this.deps[node.id] || {
name: node.name,
keys: [],
};
if (node.name) {
dep.name = node.name;
}
this.deps[node.id] = dep;
if (dep.keys.indexOf(key) === -1) {
dep.keys.push(key);
}
this.emit('change');
}
/**
*
* @param node
* @param key key需要移除key
* @returns void
*/
public removeDep(node?: MNode, key?: string | number) {
if (!node) {
Object.keys(this.deps).forEach((depKey) => {
delete this.deps[depKey];
});
this.emit('change');
return;
}
const dep = this.deps[node.id];
if (!dep) return;
if (key) {
const index = dep.keys.indexOf(key);
dep.keys.splice(index, 1);
if (dep.keys.length === 0) {
delete this.deps[node.id];
}
} else {
delete this.deps[node.id];
}
this.emit('change');
}
/**
* key是否存在在依赖列表中
* @param node
* @param key key
* @returns boolean
*/
public hasDep(node: MNode, key: string | number) {
const dep = this.deps[node.id];
return Boolean(dep?.keys.find((d) => d === key));
}
public destroy() {
this.removeAllListeners();
}
}
export class Watcher extends EventEmitter {
public targets = reactive<TargetList>({});
/**
* target
* @param type
* @returns Target[]
*/
public getTargets(type = 'default') {
return this.targets[type] || {};
}
/**
*
* @param target Target
*/
public addTarget(target: Target) {
const targets = this.getTargets(target.type) || {};
this.targets[target.type] = targets;
targets[target.id] = target;
this.emit('add-target', target);
}
/**
* id的target
* @param id target id
* @returns Target
*/
public getTarget(id: string | number) {
const allTargets = Object.values(this.targets);
for (const targets of allTargets) {
if (targets[id]) {
return targets[id];
}
}
}
/**
* id的target
* @param id target id
* @returns boolean
*/
public hasTarget(id: string | number) {
const allTargets = Object.values(this.targets);
for (const targets of allTargets) {
if (targets[id]) {
return true;
}
}
return false;
}
/**
* id的target
* @param id target id
*/
public removeTarget(id: string | number) {
const allTargets = Object.values(this.targets);
for (const targets of allTargets) {
if (targets[id]) {
targets[id].destroy();
delete targets[id];
}
}
this.emit('remove-target');
}
/**
* target
* @param type
* @returns void
*/
public removeTargets(type = 'default') {
const targets = this.targets[type];
if (!targets) return;
for (const target of Object.values(targets)) {
target.destroy();
}
delete this.targets[type];
this.emit('remove-target');
}
/**
* target
*/
public clearTargets() {
Object.keys(this.targets).forEach((key) => {
delete this.targets[key];
});
}
/**
*
* @param nodes
* @param deep
*/
public collect(nodes: MNode[], deep = false) {
Object.values(this.targets).forEach((targets) => {
Object.values(targets).forEach((target) => {
nodes.forEach((node) => {
target.removeDep(node);
this.collectItem(node, target, deep);
});
});
});
}
/**
*
* @param nodes
*/
public clear(nodes?: MNode[]) {
Object.values(this.targets).forEach((targets) => {
Object.values(targets).forEach((target) => {
if (nodes) {
nodes.forEach((node) => {
target.removeDep(node);
if (Array.isArray(node.items)) {
this.clear(node.items);
}
});
} else {
target.removeDep();
}
});
});
}
private collectItem(node: MNode, target: Target, deep = false) {
const collectTarget = (config: Record<string | number, any>, prop = '') => {
const doCollect = (key: string, value: any) => {
const keyIsItems = key === 'items';
const fullKey = prop ? `${prop}.${key}` : key;
if (target.isTarget(key, value)) {
target.updateDep(node, fullKey);
} else if (!keyIsItems && Array.isArray(value)) {
value.forEach((item, index) => {
collectTarget(item, `${fullKey}.${index}`);
});
} else if (Object.prototype.toString.call(value) === '[object Object]') {
collectTarget(value, fullKey);
}
if (keyIsItems && deep && Array.isArray(value)) {
value.forEach((child) => {
this.collectItem(child, target, deep);
});
}
};
Object.entries(config).forEach(([key, value]) => {
doCollect(key, value);
});
};
collectTarget(node);
}
}
export type DepService = Watcher;
export default new Watcher();

View File

@ -0,0 +1,21 @@
import { isEmpty } from 'lodash-es';
import { CodeBlockContent, HookType, Id } from '@tmagic/schema';
import { Target } from '../services/dep';
import { HookData } from '../type';
export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent) =>
new Target({
type: 'code-block',
id,
name: codeBlock.name,
isTarget: (key: string | number, value: any) => {
if (value?.hookType === HookType.CODE && !isEmpty(value.hookData)) {
const index = value.hookData.findIndex((item: HookData) => item.codeId === id);
return Boolean(index > -1);
}
return false;
},
});

View File

@ -0,0 +1,227 @@
import { describe, expect, test } from 'vitest';
import depService, { Target, Watcher } from '@editor/services/dep';
describe('Watcher', () => {
test('instance', () => {
const watcher = new Watcher();
expect(watcher).toBeInstanceOf(Watcher);
});
});
describe('depService', () => {
const defaultTarget = new Target({
id: 1,
name: 'test',
isTarget: () => true,
});
const target = new Target({
type: 'target',
id: 2,
name: 'test',
isTarget: () => true,
});
test('default target type', () => {
expect(defaultTarget.type).toBe('default');
expect(target.type).toBe('target');
});
test('addTarget', () => {
depService.addTarget(target);
expect(depService.getTarget(1)).toBeUndefined();
expect(depService.getTarget(2)?.id).toBe(2);
expect(Object.keys(depService.getTargets())).toHaveLength(0);
expect(Object.keys(depService.getTargets('target'))).toHaveLength(1);
});
test('clearTargets', () => {
depService.clearTargets();
depService.addTarget(target);
expect(depService.hasTarget(2)).toBeTruthy();
depService.clearTargets();
expect(depService.hasTarget(2)).toBeFalsy();
});
test('hasTarget', () => {
depService.clearTargets();
depService.addTarget(target);
expect(depService.hasTarget(1)).toBeFalsy();
expect(depService.hasTarget(2)).toBeTruthy();
});
test('removeTarget', () => {
depService.clearTargets();
depService.addTarget(target);
expect(depService.hasTarget(2)).toBeTruthy();
depService.removeTarget(2);
expect(depService.hasTarget(2)).toBeFalsy();
});
test('removeTargets', () => {
depService.clearTargets();
depService.addTarget(defaultTarget);
depService.addTarget(target);
expect(depService.hasTarget(1)).toBeTruthy();
expect(depService.hasTarget(2)).toBeTruthy();
depService.removeTargets('target');
expect(depService.hasTarget(1)).toBeTruthy();
expect(depService.hasTarget(2)).toBeFalsy();
depService.removeTargets('target1');
});
test('collect', () => {
depService.clearTargets();
depService.addTarget(
new Target({
type: 'target',
id: 'collect_1',
name: 'test',
isTarget: (key: string | number, value: any) => key === 'text' && value === 'text1',
}),
);
depService.addTarget(
new Target({
type: 'target',
id: 'collect_2',
name: 'test2',
isTarget: (key: string | number, value: any) => key === 'text1' && value === 'text',
}),
);
depService.collect([
{
id: 'node_1',
name: 'node',
text: 'text1',
text1: 'text',
object: {
text1: 'text',
},
array: [
{
object: {
text1: 'text',
},
},
],
},
]);
const target1 = depService.getTarget('collect_1');
const target2 = depService.getTarget('collect_2');
expect((target1?.deps || {}).node_1.name).toBe('node');
expect((target2?.deps || {}).node_1.name).toBe('node');
expect((target1?.deps || {}).node_1.keys).toHaveLength(1);
expect((target2?.deps || {}).node_1.keys).toHaveLength(3);
depService.collect([
{
id: 'node_1',
name: 'node',
text: 'text',
text1: 'text',
object: {
text1: 'text1',
},
array: [
{
object: {
text1: 'text1',
},
},
],
},
]);
expect((target1?.deps || {}).node_1).toBeUndefined();
expect((target2?.deps || {}).node_1.keys).toHaveLength(1);
depService.collect([
{
id: 'node_1',
name: 'node',
text: 'text',
text1: 'text',
},
]);
expect((target1?.deps || {}).node_1).toBeUndefined();
expect((target2?.deps || {}).node_1.keys[0]).toBe('text1');
depService.clear([
{
id: 'node_1',
name: 'node',
},
]);
expect((target1?.deps || {}).node_1).toBeUndefined();
expect((target2?.deps || {}).node_1).toBeUndefined();
});
test('collect deep', () => {
depService.clearTargets();
depService.addTarget(
new Target({
type: 'target',
id: 'collect_1',
name: 'test',
isTarget: (key: string | number, value: any) => key === 'text' && value === 'text1',
}),
);
depService.collect(
[
{
id: 'node_1',
name: 'node',
text: 'text1',
items: [
{
id: 'node_2',
name: 'node2',
text: 'text1',
},
],
},
],
true,
);
const target1 = depService.getTarget('collect_1');
expect((target1?.deps || {}).node_1.name).toBe('node');
expect((target1?.deps || {}).node_2.name).toBe('node2');
depService.clear([
{
id: 'node_1',
name: 'node',
items: [
{
id: 'node_2',
name: 'node2',
text: 'text1',
},
],
},
]);
expect((target1?.deps || {}).node_1).toBeUndefined();
expect((target1?.deps || {}).node_2).toBeUndefined();
});
});

View File

@ -0,0 +1,54 @@
import { describe, expect, test } from 'vitest';
import * as dep from '@editor/utils/dep';
describe('dep', () => {
test('createCodeBlockTarget', () => {
const target = dep.createCodeBlockTarget('code_5316', {
name: 'code',
content: () => false,
params: [],
});
expect(target.id).toBe('code_5316');
expect(target.name).toBe('code');
expect(target.type).toBe('code-block');
const isTarget = target.isTarget('created', {
hookType: 'code',
hookData: [
{
codeId: 'code_5336',
params: {
studentName: 'lisa',
age: 14,
},
},
{
codeId: 'code_5316',
params: {},
},
],
});
expect(isTarget).toBeTruthy();
const target1 = dep.createCodeBlockTarget('1', {
name: 'code',
content: () => false,
params: [],
});
const isTarget1 = target1.isTarget('created', {
hookType: 'code',
hookData: [
{
codeId: 'code_5316',
params: {},
},
],
});
expect(isTarget1).toBeFalsy();
});
});