fix: refactor match change code (#2352)

This commit is contained in:
qinluhe 2023-04-26 13:14:38 +08:00 committed by GitHub
parent eb78f9d36a
commit 9717dfa3c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 326 additions and 176 deletions

View File

@ -1,6 +1,5 @@
import TextBlock from '../TextBlock';
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import { HeadingBlockData } from '@/appflowy_app/interfaces/document';
import { HeadingBlockData, Node } from '@/appflowy_app/interfaces/document';
const fontSize: Record<string, string> = {
1: 'mt-8 text-3xl',

View File

@ -1,6 +1,6 @@
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import { Circle } from '@mui/icons-material';
import NodeComponent from '../Node';
import { Node } from '$app/interfaces/document';
export default function BulletedListBlock({
title,

View File

@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import ColumnBlock from '../ColumnBlock';
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import { Node } from '$app/interfaces/document';
export default function ColumnListBlock({
node,

View File

@ -1,5 +1,5 @@
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import NodeComponent from '../Node';
import { Node } from '$app/interfaces/document';
export default function NumberedListBlock({
title,

View File

@ -3,8 +3,7 @@ import TextBlock from '../TextBlock';
import NumberedListBlock from './NumberedListBlock';
import BulletedListBlock from './BulletedListBlock';
import ColumnListBlock from './ColumnListBlock';
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import { TextDelta } from '@/appflowy_app/interfaces/document';
import { Node, TextDelta } from '@/appflowy_app/interfaces/document';
export default function ListBlock({ node }: { node: Node }) {
const title = useMemo(() => {

View File

@ -2,9 +2,9 @@ import React, { useCallback } from 'react';
import { useNode } from './Node.hooks';
import { withErrorBoundary } from 'react-error-boundary';
import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import TextBlock from '../TextBlock';
import { NodeContext } from '../_shared/SubscribeNode.hooks';
import { Node } from '$app/interfaces/document';
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
const { node, childIds, isSelected, ref } = useNode(id);

View File

@ -5,15 +5,9 @@ import { documentActions } from '$app/stores/reducers/document/slice';
export function useParseTree(documentData: DocumentData) {
const dispatch = useAppDispatch();
const { blocks, meta } = documentData;
useEffect(() => {
dispatch(
documentActions.create({
nodes: blocks,
children: meta.childrenMap,
})
);
dispatch(documentActions.create(documentData));
return () => {
dispatch(documentActions.clear());

View File

@ -1,7 +1,7 @@
import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey';
import { useCallback, useContext } from 'react';
import { Range, Editor, Element, Text, Location } from 'slate';
import { TextDelta } from '$app/interfaces/document';
import { TextDelta, TextSelection } from '$app/interfaces/document';
import { useTextInput } from '../_shared/TextInput.hooks';
import { useAppDispatch } from '@/appflowy_app/stores/store';
import { DocumentControllerContext } from '@/appflowy_app/stores/effects/document/document_controller';
@ -10,7 +10,7 @@ import {
indentNodeThunk,
splitNodeThunk,
} from '@/appflowy_app/stores/reducers/document/async_actions';
import { documentActions, TextSelection } from '@/appflowy_app/stores/reducers/document/slice';
import { documentActions } from '@/appflowy_app/stores/reducers/document/slice';
export function useTextBlock(id: string) {
const { editor, onChange, value } = useTextInput(id);

View File

@ -1,10 +1,10 @@
import { Slate, Editable } from 'slate-react';
import Leaf from './Leaf';
import { useTextBlock } from './TextBlock.hooks';
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import NodeComponent from '../Node';
import HoveringToolbar from '../_shared/HoveringToolbar';
import React, { useEffect } from 'react';
import { Node } from '$app/interfaces/document';
function TextBlock({
node,

View File

@ -1,8 +1,8 @@
import React from 'react';
import { useVirtualizedList } from './VirtualizedList.hooks';
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import DocumentTitle from '../DocumentTitle';
import Overlay from '../Overlay';
import { Node } from '$app/interfaces/document';
export default function VirtualizedList({
childIds,

View File

@ -1,6 +1,6 @@
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import { useAppSelector } from '@/appflowy_app/stores/store';
import { useMemo, createContext } from 'react';
import { Node } from '$app/interfaces/document';
export const NodeContext = createContext<Node | null>(null);
/**

View File

@ -1,6 +1,6 @@
import { useCallback, useContext, useMemo, useRef, useEffect, useState } from 'react';
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
import { TextDelta } from '$app/interfaces/document';
import { TextDelta, TextSelection } from '$app/interfaces/document';
import { NodeContext } from './SubscribeNode.hooks';
import { useAppDispatch, useAppSelector } from '@/appflowy_app/stores/store';
@ -10,7 +10,7 @@ import { withReact, ReactEditor } from 'slate-react';
import * as Y from 'yjs';
import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
import { updateNodeDeltaThunk } from '@/appflowy_app/stores/reducers/document/async_actions/update';
import { documentActions, TextSelection } from '@/appflowy_app/stores/reducers/document/slice';
import { documentActions } from '@/appflowy_app/stores/reducers/document/slice';
import { deltaToSlateValue, getDeltaFromSlateNodes } from '@/appflowy_app/utils/block';
export function useTextInput(id: string) {

View File

@ -0,0 +1,3 @@
export const BLOCK_MAP_NAME = 'blocks';
export const META_NAME = 'meta';
export const CHILDREN_MAP_NAME = 'children_map';

View File

@ -37,13 +37,6 @@ export interface TextDelta {
insert: string;
attributes?: Record<string, string | boolean>;
}
export interface DocumentData {
rootId: string;
blocks: Record<string, NestedBlock>;
meta: {
childrenMap: Record<string, string[]>;
};
}
// eslint-disable-next-line no-shadow
export enum BlockActionType {
@ -60,3 +53,51 @@ export interface DeltaItem {
value?: NestedBlock | string[];
};
}
export type Node = NestedBlock;
export interface SelectionPoint {
path: [number, number];
offset: number;
}
export interface TextSelection {
anchor: SelectionPoint;
focus: SelectionPoint;
}
export interface DocumentData {
rootId: string;
// map of block id to block
nodes: Record<string, Node>;
// map of block id to children block ids
children: Record<string, string[]>;
}
export interface DocumentState {
// map of block id to block
nodes: Record<string, Node>;
// map of block id to children block ids
children: Record<string, string[]>;
// selected block ids
selections: string[];
// map of block id to text selection
textSelections: Record<string, TextSelection>;
}
// eslint-disable-next-line no-shadow
export enum ChangeType {
BlockInsert,
BlockUpdate,
BlockDelete,
ChildrenMapInsert,
ChildrenMapUpdate,
ChildrenMapDelete,
}
export interface BlockPBValue {
id: string;
ty: string;
parent: string;
children: string;
data: string;
}

View File

@ -1,10 +1,20 @@
import { DocumentData, BlockType } from '@/appflowy_app/interfaces/document';
import { DocumentData, Node } from '@/appflowy_app/interfaces/document';
import { createContext } from 'react';
import { DocumentBackendService } from './document_bd_svc';
import { FlowyError, BlockActionPB, DocEventPB, BlockActionTypePB, BlockEventPayloadPB } from '@/services/backend';
import {
FlowyError,
BlockActionPB,
DocEventPB,
BlockActionTypePB,
BlockEventPayloadPB,
BlockPB,
ChildrenPB,
} from '@/services/backend';
import { DocumentObserver } from './document_observer';
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
import * as Y from 'yjs';
import { blockPB2Node } from '@/appflowy_app/utils/block';
import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME } from '@/appflowy_app/constants/block';
import { get } from '@/appflowy_app/utils/tool';
export const DocumentControllerContext = createContext<DocumentController | null>(null);
@ -34,33 +44,18 @@ export class DocumentController {
const document = await this.backendService.open();
if (document.ok) {
const blocks: DocumentData['blocks'] = {};
document.val.blocks.forEach((block) => {
let data = {};
try {
data = JSON.parse(block.data);
} catch {
console.log('json parse error', block.data);
}
blocks[block.id] = {
id: block.id,
type: block.ty as BlockType,
parent: block.parent_id,
children: block.children_id,
data,
};
const nodes: DocumentData['nodes'] = {};
get<Map<string, BlockPB>>(document.val, [BLOCK_MAP_NAME]).forEach((block) => {
nodes[block.id] = blockPB2Node(block);
});
const childrenMap: Record<string, string[]> = {};
document.val.meta.children_map.forEach((child, key) => {
childrenMap[key] = child.children;
const children: Record<string, string[]> = {};
get<Map<string, ChildrenPB>>(document.val, [META_NAME, CHILDREN_MAP_NAME]).forEach((child, key) => {
children[key] = child.children;
});
return {
rootId: document.val.page_id,
blocks,
meta: {
childrenMap,
},
nodes,
children,
};
}
@ -150,8 +145,8 @@ export class DocumentController {
if (!this.onDocChange) return;
const { events, is_remote } = DocEventPB.deserializeBinary(payload);
events.forEach((event) => {
event.event.forEach((_payload) => {
events.forEach((blockEvent) => {
blockEvent.event.forEach((_payload) => {
this.onDocChange?.({
isRemote: is_remote,
data: _payload,

View File

@ -1,7 +1,7 @@
import { BlockType } from '@/appflowy_app/interfaces/document';
import { BlockType, DocumentState } from '@/appflowy_app/interfaces/document';
import { DocumentController } from '$app/stores/effects/document/document_controller';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { documentActions, DocumentState } from '../slice';
import { documentActions } from '../slice';
import { outdentNodeThunk } from './outdent';
import { setCursorAfterThunk } from './set_cursor';

View File

@ -1,6 +1,7 @@
import { DocumentController } from '$app/stores/effects/document/document_controller';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { DocumentState } from '../slice';
import { DocumentState } from '$app/interfaces/document';
export const deleteNodeThunk = createAsyncThunk(
'document/deleteNode',

View File

@ -1,7 +1,6 @@
import { BlockType } from '@/appflowy_app/interfaces/document';
import { BlockType, DocumentState } from '@/appflowy_app/interfaces/document';
import { DocumentController } from '$app/stores/effects/document/document_controller';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { DocumentState } from '../slice';
export const indentNodeThunk = createAsyncThunk(
'document/indentNode',

View File

@ -1,7 +1,6 @@
import { BlockType, NestedBlock } from '@/appflowy_app/interfaces/document';
import { BlockType, DocumentState, NestedBlock } from '@/appflowy_app/interfaces/document';
import { DocumentController } from '$app/stores/effects/document/document_controller';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { DocumentState } from '../slice';
import { generateId } from '@/appflowy_app/utils/block';
export const insertAfterNodeThunk = createAsyncThunk(

View File

@ -1,6 +1,7 @@
import { DocumentController } from '$app/stores/effects/document/document_controller';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { DocumentState } from '../slice';
import { DocumentState } from '$app/interfaces/document';
export const outdentNodeThunk = createAsyncThunk(
'document/outdentNode',

View File

@ -1,5 +1,6 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { documentActions, DocumentState, SelectionPoint, TextSelection } from '../slice';
import { documentActions } from '../slice';
import { DocumentState, SelectionPoint, TextSelection } from '$app/interfaces/document';
export const setCursorBeforeThunk = createAsyncThunk(
'document/setCursorBefore',

View File

@ -1,8 +1,8 @@
import { BlockType, TextDelta } from '@/appflowy_app/interfaces/document';
import { BlockType, DocumentState, TextDelta } from '@/appflowy_app/interfaces/document';
import { DocumentController } from '@/appflowy_app/stores/effects/document/document_controller';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { generateId } from '@/appflowy_app/utils/block';
import { documentActions, DocumentState } from '../slice';
import { documentActions } from '../slice';
import { setCursorBeforeThunk } from './set_cursor';
export const splitNodeThunk = createAsyncThunk(

View File

@ -1,7 +1,7 @@
import { TextDelta, NestedBlock } from '@/appflowy_app/interfaces/document';
import { TextDelta, NestedBlock, DocumentState } from '@/appflowy_app/interfaces/document';
import { DocumentController } from '@/appflowy_app/stores/effects/document/document_controller';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { documentActions, DocumentState } from '../slice';
import { documentActions } from '../slice';
import { debounce } from '$app/utils/tool';
export const updateNodeDeltaThunk = createAsyncThunk(
'document/updateNodeDelta',

View File

@ -1,32 +1,8 @@
import { NestedBlock } from '@/appflowy_app/interfaces/document';
import { blockChangeValue2Node } from '@/appflowy_app/utils/block';
import { Log } from '@/appflowy_app/utils/log';
import { BlockEventPayloadPB, DeltaTypePB } from '@/services/backend';
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { RegionGrid } from './region_grid';
export type Node = NestedBlock;
export interface SelectionPoint {
path: [number, number];
offset: number;
}
export interface TextSelection {
anchor: SelectionPoint;
focus: SelectionPoint;
}
export interface DocumentState {
// map of block id to block
nodes: Record<string, Node>;
// map of block id to children block ids
children: Record<string, string[]>;
// selected block ids
selections: string[];
// map of block id to text selection
textSelections: Record<string, TextSelection>;
}
import { DocumentState, Node, TextSelection } from '@/appflowy_app/interfaces/document';
import { BlockEventPayloadPB } from '@/services/backend';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RegionGrid } from '@/appflowy_app/utils/region_grid';
import { parseValue, matchChange } from '@/appflowy_app/utils/block_change';
const regionGrid = new RegionGrid(50);
@ -158,44 +134,11 @@ export const documentSlice = createSlice({
const { path, id, value, command } = action.payload.data;
const isRemote = action.payload.isRemote;
let valueJson;
try {
valueJson = JSON.parse(value);
} catch {
Log.error('[onDataChange] json parse error', value);
return;
}
const valueJson = parseValue(value);
if (!valueJson) return;
if (command === DeltaTypePB.Inserted || command === DeltaTypePB.Updated) {
// set map key and value ( block map or children map)
if (path[0] === 'blocks') {
const block = blockChangeValue2Node(valueJson);
if (command === DeltaTypePB.Updated && !isRemote) {
// the `data` from local is already updated in local, so we just need to update other fields
const node = state.nodes[block.id];
if (!node || node.parent !== block.parent || node.type !== block.type || node.children !== block.children) {
state.nodes[block.id] = block;
}
} else {
state.nodes[block.id] = block;
}
} else {
state.children[id] = valueJson;
}
} else {
// remove map key ( block map or children map)
if (path[0] === 'blocks') {
if (state.selections.indexOf(id)) {
state.selections.splice(state.selections.indexOf(id), 1);
}
regionGrid.removeBlock(id);
delete state.textSelections[id];
delete state.nodes[id];
} else {
delete state.children[id];
}
}
// match change
matchChange(state, { path, id, value: valueJson, command }, isRemote);
},
},
});

View File

@ -1,8 +1,8 @@
import { BlockPB } from '@/services/backend/models/flowy-document2';
import { nanoid } from 'nanoid';
import { Descendant, Element, Text } from 'slate';
import { TextDelta, BlockType, NestedBlock } from '../interfaces/document';
import { BlockType, TextDelta } from '../interfaces/document';
import { Log } from './log';
export function generateId() {
return nanoid(10);
}
@ -36,29 +36,19 @@ export function getDeltaFromSlateNodes(slateNodes: Descendant[]) {
});
}
export function blockChangeValue2Node(value: {
id: string;
ty: string;
parent: string;
children: string;
data: string;
}): NestedBlock {
const block = {
id: value.id,
type: value.ty as BlockType,
parent: value.parent,
children: value.children,
data: {},
};
if ('data' in value && typeof value.data === 'string') {
try {
Object.assign(block, {
data: JSON.parse(value.data),
});
} catch {
Log.error('valueJson data parse error', block.data);
}
export function blockPB2Node(block: BlockPB) {
let data = {};
try {
data = JSON.parse(block.data);
} catch {
Log.error('[Document Open] json parse error', block.data);
}
return block;
const node = {
id: block.id,
type: block.ty as BlockType,
parent: block.parent_id,
children: block.children_id,
data,
};
return node;
}

View File

@ -0,0 +1,185 @@
import { DeltaTypePB } from '@/services/backend/models/flowy-document2';
import { BlockType, NestedBlock, DocumentState, ChangeType, BlockPBValue } from '../interfaces/document';
import { Log } from './log';
import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME } from '../constants/block';
// This is a list of all the possible changes that can happen to document data
const matchCases = [
{ match: matchBlockInsert, type: ChangeType.BlockInsert, onMatch: onMatchBlockInsert },
{ match: matchBlockUpdate, type: ChangeType.BlockUpdate, onMatch: onMatchBlockUpdate },
{ match: matchBlockDelete, type: ChangeType.BlockDelete, onMatch: onMatchBlockDelete },
{ match: matchChildrenMapInsert, type: ChangeType.ChildrenMapInsert, onMatch: onMatchChildrenInsert },
{ match: matchChildrenMapUpdate, type: ChangeType.ChildrenMapUpdate, onMatch: onMatchChildrenUpdate },
{ match: matchChildrenMapDelete, type: ChangeType.ChildrenMapDelete, onMatch: onMatchChildrenDelete },
];
export function matchChange(
state: DocumentState,
{
command,
path,
id,
value,
}: {
command: DeltaTypePB;
path: string[];
id: string;
value: BlockPBValue & string[];
},
isRemote?: boolean
) {
const matchCase = matchCases.find((item) => item.match(command, path));
if (matchCase) {
matchCase.onMatch(state, id, value, isRemote);
}
}
/**
* @param command DeltaTypePB.Inserted
* @param path [BLOCK_MAP_NAME]
*/
function matchBlockInsert(command: DeltaTypePB, path: string[]) {
if (path.length !== 1) return false;
return command === DeltaTypePB.Inserted && path[0] === BLOCK_MAP_NAME;
}
/**
* @param command DeltaTypePB.Updated
* @param path [BLOCK_MAP_NAME, blockId]
*/
function matchBlockUpdate(command: DeltaTypePB, path: string[]) {
if (path.length !== 2) return false;
return command === DeltaTypePB.Updated && path[0] === BLOCK_MAP_NAME && typeof path[1] === 'string';
}
/**
* @param command DeltaTypePB.Removed
* @param path [BLOCK_MAP_NAME, blockId]
*/
function matchBlockDelete(command: DeltaTypePB, path: string[]) {
if (path.length !== 2) return false;
return command === DeltaTypePB.Removed && path[0] === BLOCK_MAP_NAME && typeof path[1] === 'string';
}
/**
* @param command DeltaTypePB.Inserted
* @param path [META_NAME, CHILDREN_MAP_NAME]
*/
function matchChildrenMapInsert(command: DeltaTypePB, path: string[]) {
if (path.length !== 2) return false;
return command === DeltaTypePB.Inserted && path[0] === META_NAME && path[1] === CHILDREN_MAP_NAME;
}
/**
* @param command DeltaTypePB.Updated
* @param path [META_NAME, CHILDREN_MAP_NAME, id]
*/
function matchChildrenMapUpdate(command: DeltaTypePB, path: string[]) {
if (path.length !== 3) return false;
return (
command === DeltaTypePB.Updated &&
path[0] === META_NAME &&
path[1] === CHILDREN_MAP_NAME &&
typeof path[2] === 'string'
);
}
/**
* @param command DeltaTypePB.Removed
* @param path [META_NAME, CHILDREN_MAP_NAME, id]
*/
function matchChildrenMapDelete(command: DeltaTypePB, path: string[]) {
if (path.length !== 3) return false;
return (
command === DeltaTypePB.Removed &&
path[0] === META_NAME &&
path[1] === CHILDREN_MAP_NAME &&
typeof path[2] === 'string'
);
}
function onMatchBlockInsert(state: DocumentState, blockId: string, blockValue: BlockPBValue, _isRemote?: boolean) {
const block = blockChangeValue2Node(blockValue);
state.nodes[blockId] = block;
}
function onMatchBlockUpdate(state: DocumentState, blockId: string, blockValue: BlockPBValue, isRemote?: boolean) {
const block = blockChangeValue2Node(blockValue);
const node = state.nodes[blockId];
if (!node) return;
// if the change is from remote, we should update all fields
if (isRemote) {
state.nodes[blockId] = block;
return;
}
// if the change is from local, we should update all fields except `data`,
// because we will update `data` field in `updateNodeData` action
const shouldUpdate = node.parent !== block.parent || node.type !== block.type || node.children !== block.children;
if (shouldUpdate) {
state.nodes[blockId] = {
...block,
data: node.data,
};
}
return;
}
function onMatchBlockDelete(state: DocumentState, blockId: string, blockValue: BlockPBValue, _isRemote?: boolean) {
const index = state.selections.indexOf(blockId);
if (index > -1) {
state.selections.splice(index, 1);
}
delete state.textSelections[blockId];
delete state.nodes[blockId];
}
function onMatchChildrenInsert(state: DocumentState, id: string, children: string[], _isRemote?: boolean) {
state.children[id] = children;
}
function onMatchChildrenUpdate(state: DocumentState, id: string, newChildren: string[], _isRemote?: boolean) {
const children = state.children[id];
if (!children) return;
state.children[id] = newChildren;
}
function onMatchChildrenDelete(state: DocumentState, id: string, _children: string[], _isRemote?: boolean) {
delete state.children[id];
}
/**
* convert block change value to node
* @param value
*/
export function blockChangeValue2Node(value: BlockPBValue): NestedBlock {
const block = {
id: value.id,
type: value.ty as BlockType,
parent: value.parent,
children: value.children,
data: {},
};
if ('data' in value && typeof value.data === 'string') {
try {
Object.assign(block, {
data: JSON.parse(value.data),
});
} catch {
Log.error('[onDataChange] valueJson data parse error', block.data);
}
}
return block;
}
export function parseValue(value: string) {
let valueJson;
try {
valueJson = JSON.parse(value);
} catch {
Log.error('[onDataChange] json parse error', value);
return;
}
return valueJson;
}

View File

@ -1,30 +1,30 @@
export function debounce(fn: (...args: any[]) => void, delay: number) {
let timeout: NodeJS.Timeout;
return (...args: any[]) => {
clearTimeout(timeout)
timeout = setTimeout(()=>{
clearTimeout(timeout);
timeout = setTimeout(() => {
// eslint-disable-next-line prefer-spread
fn.apply(undefined, args)
}, delay)
}
fn.apply(undefined, args);
}, delay);
};
}
export function throttle(fn: (...args: any[]) => void, delay: number, immediate = true) {
let timeout: NodeJS.Timeout | null = null
let timeout: NodeJS.Timeout | null = null;
return (...args: any[]) => {
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
timeout = null;
// eslint-disable-next-line prefer-spread
!immediate && fn.apply(undefined, args)
}, delay)
!immediate && fn.apply(undefined, args);
}, delay);
// eslint-disable-next-line prefer-spread
immediate && fn.apply(undefined, args)
immediate && fn.apply(undefined, args);
}
}
};
}
export function get(obj: any, path: string[], defaultValue?: any) {
export function get<T = any>(obj: any, path: string[], defaultValue?: any): T {
let value = obj;
for (const prop of path) {
value = value[prop];
@ -55,7 +55,6 @@ export function isEqual<T>(value1: T, value2: T): boolean {
return value1 === value2;
}
if (Array.isArray(value1)) {
if (!Array.isArray(value2) || value1.length !== value2.length) {
return false;
@ -77,9 +76,9 @@ export function isEqual<T>(value1: T, value2: T): boolean {
return false;
}
for (const key of keys1) {
for (const key of keys1) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// @ts-expect-error
if (!isEqual(value1[key], value2[key])) {
return false;
}