1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-12 10:14:02 +02:00

Optimize isText, isElement, isNodeList and isEditor (#5859)

* Remove the `isPlainObject` check from `isText` and `isElement` for performance

* Optimise `isElement`, `isNodeList` and `isText` further

* Update changeset

* Fix changeset

* Refactor object check into `isObject`
This commit is contained in:
Joe Anderson
2025-05-05 16:31:56 +01:00
committed by GitHub
parent d39bead80a
commit 72532fd2d7
20 changed files with 112 additions and 86 deletions

View File

@@ -0,0 +1,8 @@
---
'slate': patch
'slate-history': patch
'slate-hyperscript': patch
'slate-react': patch
---
Optimize `isElement`, `isText`, `isNodeList` and `isEditor` by removing dependency on `is-plain-object` and by performing shallow checks by default. To perform a full check, including all descendants, pass the `{ deep: true }` option to `isElement`, `isNodeList` or `isEditor`.

View File

@@ -13,9 +13,6 @@
"files": [
"dist/"
],
"dependencies": {
"is-plain-object": "^5.0.0"
},
"devDependencies": {
"@babel/runtime": "^7.23.2",
"lodash": "^4.17.21",

View File

@@ -1,5 +1,4 @@
import { isPlainObject } from 'is-plain-object'
import { Operation, Range } from 'slate'
import { Operation, Range, isObject } from 'slate'
interface Batch {
operations: Operation[]
@@ -24,7 +23,7 @@ export const History = {
isHistory(value: any): value is History {
return (
isPlainObject(value) &&
isObject(value) &&
Array.isArray(value.redos) &&
Array.isArray(value.undos) &&
(value.redos.length === 0 ||

View File

@@ -13,9 +13,6 @@
"files": [
"dist/"
],
"dependencies": {
"is-plain-object": "^5.0.0"
},
"devDependencies": {
"@babel/runtime": "^7.23.2",
"slate": "^0.114.0",

View File

@@ -1,5 +1,4 @@
import { isPlainObject } from 'is-plain-object'
import { Element, createEditor as makeEditor } from 'slate'
import { Element, createEditor as makeEditor, isObject } from 'slate'
import {
createAnchor,
createCursor,
@@ -86,7 +85,7 @@ const createFactory = <T extends HyperscriptCreators>(creators: T) => {
attributes = {}
}
if (!isPlainObject(attributes)) {
if (!isObject(attributes)) {
children = [attributes].concat(children)
attributes = {}
}

View File

@@ -17,7 +17,6 @@
"@juggle/resize-observer": "^3.4.0",
"direction": "^1.0.4",
"is-hotkey": "^0.2.0",
"is-plain-object": "^5.0.0",
"lodash": "^4.17.21",
"scroll-into-view-if-needed": "^3.1.0",
"tiny-invariant": "1.3.1"

View File

@@ -15,7 +15,6 @@
],
"dependencies": {
"immer": "^10.0.3",
"is-plain-object": "^5.0.0",
"tiny-warning": "^1.0.3"
},
"devDependencies": {

View File

@@ -1,20 +1,14 @@
import { Editor, EditorInterface } from '../interfaces/editor'
import { isPlainObject } from 'is-plain-object'
import { Range } from '../interfaces/range'
import { Node } from '../interfaces/node'
import { Operation } from '../interfaces/operation'
const IS_EDITOR_CACHE = new WeakMap<object, boolean>()
import { isObject } from '../utils'
export const isEditor: EditorInterface['isEditor'] = (
value: any
value: any,
{ deep = false } = {}
): value is Editor => {
const cachedIsEditor = IS_EDITOR_CACHE.get(value)
if (cachedIsEditor !== undefined) {
return cachedIsEditor
}
if (!isPlainObject(value)) {
if (!isObject(value)) {
return false
}
@@ -35,10 +29,10 @@ export const isEditor: EditorInterface['isEditor'] = (
typeof value.onChange === 'function' &&
typeof value.removeMark === 'function' &&
typeof value.getDirtyPaths === 'function' &&
(value.marks === null || isPlainObject(value.marks)) &&
(value.marks === null || isObject(value.marks)) &&
(value.selection === null || Range.isRange(value.selection)) &&
Node.isNodeList(value.children) &&
(!deep || Node.isNodeList(value.children)) &&
Operation.isOperationList(value.operations)
IS_EDITOR_CACHE.set(value, isEditor)
return isEditor
}

View File

@@ -6,3 +6,4 @@ export * from './transforms-node'
export * from './transforms-selection'
export * from './transforms-text'
export * from './types'
export * from './utils/is-object'

View File

@@ -213,6 +213,10 @@ export interface EditorFragmentDeletionOptions {
direction?: TextDirection
}
export interface EditorIsEditorOptions {
deep?: boolean
}
export interface EditorLeafOptions {
depth?: number
edge?: LeafEdge
@@ -469,7 +473,7 @@ export interface EditorInterface {
/**
* Check if a value is an `Editor` object.
*/
isEditor: (value: any) => value is Editor
isEditor: (value: any, options?: EditorIsEditorOptions) => value is Editor
/**
* Check if a value is a read-only `Element` object.

View File

@@ -1,5 +1,12 @@
import { isPlainObject } from 'is-plain-object'
import { Ancestor, Descendant, Editor, ExtendedType, Node, Path } from '..'
import {
Ancestor,
Descendant,
Editor,
ExtendedType,
Node,
Path,
isObject,
} from '..'
/**
* `Element` objects are a type of node in a Slate document that contain other
@@ -13,21 +20,31 @@ export interface BaseElement {
export type Element = ExtendedType<'Element', BaseElement>
export interface ElementIsElementOptions {
deep?: boolean
}
export interface ElementInterface {
/**
* Check if a value implements the 'Ancestor' interface.
*/
isAncestor: (value: any) => value is Ancestor
isAncestor: (
value: any,
options?: ElementIsElementOptions
) => value is Ancestor
/**
* Check if a value implements the `Element` interface.
*/
isElement: (value: any) => value is Element
isElement: (value: any, options?: ElementIsElementOptions) => value is Element
/**
* Check if a value is an array of `Element` objects.
*/
isElementList: (value: any) => value is Element[]
isElementList: (
value: any,
options?: ElementIsElementOptions
) => value is Element[]
/**
* Check if a set of props is a partial of Element.
@@ -56,24 +73,42 @@ export interface ElementInterface {
/**
* Shared the function with isElementType utility
*/
const isElement = (value: any): value is Element => {
return (
isPlainObject(value) &&
Node.isNodeList(value.children) &&
!Editor.isEditor(value)
)
const isElement = (
value: any,
{ deep = false }: ElementIsElementOptions = {}
): value is Element => {
if (!isObject(value)) return false
// PERF: No need to use the full Editor.isEditor here
const isEditor = typeof value.apply === 'function'
if (isEditor) return false
const isChildrenValid = deep
? Node.isNodeList(value.children)
: Array.isArray(value.children)
return isChildrenValid
}
// eslint-disable-next-line no-redeclare
export const Element: ElementInterface = {
isAncestor(value: any): value is Ancestor {
return isPlainObject(value) && Node.isNodeList(value.children)
isAncestor(
value: any,
{ deep = false }: ElementIsElementOptions = {}
): value is Ancestor {
return isObject(value) && Node.isNodeList(value.children, { deep })
},
isElement,
isElementList(value: any): value is Element[] {
return Array.isArray(value) && value.every(val => Element.isElement(val))
isElementList(
value: any,
{ deep = false }: ElementIsElementOptions = {}
): value is Element[] {
return (
Array.isArray(value) &&
value.every(val => Element.isElement(val, { deep }))
)
},
isElementProps(props: any): props is Partial<Element> {

View File

@@ -32,6 +32,10 @@ export interface NodeElementsOptions {
pass?: (node: NodeEntry) => boolean
}
export interface NodeIsNodeOptions {
deep?: boolean
}
export interface NodeLevelsOptions {
reverse?: boolean
}
@@ -144,12 +148,12 @@ export interface NodeInterface {
/**
* Check if a value implements the `Node` interface.
*/
isNode: (value: any) => value is Node
isNode: (value: any, options?: NodeIsNodeOptions) => value is Node
/**
* Check if a value is a list of `Node` objects.
*/
isNodeList: (value: any) => value is Node[]
isNodeList: (value: any, options?: NodeIsNodeOptions) => value is Node[]
/**
* Get the last node entry in a root node from a path.
@@ -211,8 +215,6 @@ export interface NodeInterface {
) => Generator<NodeEntry<Text>, void, undefined>
}
const IS_NODE_LIST_CACHE = new WeakMap<any[], boolean>()
// eslint-disable-next-line no-redeclare
export const Node: NodeInterface = {
ancestor(root: Node, path: Path): Ancestor {
@@ -437,23 +439,21 @@ export const Node: NodeInterface = {
return true
},
isNode(value: any): value is Node {
isNode(value: any, { deep = false }: NodeIsNodeOptions = {}): value is Node {
return (
Text.isText(value) || Element.isElement(value) || Editor.isEditor(value)
Text.isText(value) ||
Element.isElement(value, { deep }) ||
Editor.isEditor(value, { deep })
)
},
isNodeList(value: any): value is Node[] {
if (!Array.isArray(value)) {
return false
}
const cachedResult = IS_NODE_LIST_CACHE.get(value)
if (cachedResult !== undefined) {
return cachedResult
}
const isNodeList = value.every(val => Node.isNode(val))
IS_NODE_LIST_CACHE.set(value, isNodeList)
return isNodeList
isNodeList(
value: any,
{ deep = false }: NodeIsNodeOptions = {}
): value is Node[] {
return (
Array.isArray(value) && value.every(val => Node.isNode(val, { deep }))
)
},
last(root: Node, path: Path): NodeEntry {

View File

@@ -1,5 +1,4 @@
import { ExtendedType, Node, Path, Range } from '..'
import { isPlainObject } from 'is-plain-object'
import { ExtendedType, Node, Path, Range, isObject } from '..'
export type BaseInsertNodeOperation = {
type: 'insert_node'
@@ -178,7 +177,7 @@ export const Operation: OperationInterface = {
},
isOperation(value: any): value is Operation {
if (!isPlainObject(value)) {
if (!isObject(value)) {
return false
}
@@ -195,7 +194,7 @@ export const Operation: OperationInterface = {
return (
typeof value.position === 'number' &&
Path.isPath(value.path) &&
isPlainObject(value.properties)
isObject(value.properties)
)
case 'move_node':
return Path.isPath(value.path) && Path.isPath(value.newPath)
@@ -210,21 +209,20 @@ export const Operation: OperationInterface = {
case 'set_node':
return (
Path.isPath(value.path) &&
isPlainObject(value.properties) &&
isPlainObject(value.newProperties)
isObject(value.properties) &&
isObject(value.newProperties)
)
case 'set_selection':
return (
(value.properties === null && Range.isRange(value.newProperties)) ||
(value.newProperties === null && Range.isRange(value.properties)) ||
(isPlainObject(value.properties) &&
isPlainObject(value.newProperties))
(isObject(value.properties) && isObject(value.newProperties))
)
case 'split_node':
return (
Path.isPath(value.path) &&
typeof value.position === 'number' &&
isPlainObject(value.properties)
isObject(value.properties)
)
default:
return false

View File

@@ -1,6 +1,5 @@
import { isPlainObject } from 'is-plain-object'
import { produce } from 'immer'
import { ExtendedType, Operation, Path } from '..'
import { ExtendedType, Operation, Path, isObject } from '..'
import { TextDirection } from '../types/types'
/**
@@ -89,7 +88,7 @@ export const Point: PointInterface = {
isPoint(value: any): value is Point {
return (
isPlainObject(value) &&
isObject(value) &&
typeof value.offset === 'number' &&
Path.isPath(value.path)
)

View File

@@ -1,6 +1,5 @@
import { produce } from 'immer'
import { isPlainObject } from 'is-plain-object'
import { ExtendedType, Operation, Path, Point, PointEntry } from '..'
import { ExtendedType, Operation, Path, Point, PointEntry, isObject } from '..'
import { RangeDirection } from '../types/types'
/**
@@ -200,7 +199,7 @@ export const Range: RangeInterface = {
isRange(value: any): value is Range {
return (
isPlainObject(value) &&
isObject(value) &&
Point.isPoint(value.anchor) &&
Point.isPoint(value.focus)
)

View File

@@ -1,5 +1,4 @@
import { isPlainObject } from 'is-plain-object'
import { Path, Range } from '..'
import { Range, isObject } from '..'
import { ExtendedType } from '../types/custom-types'
import { isDeepEqual } from '../utils/deep-equal'
@@ -93,7 +92,7 @@ export const Text: TextInterface = {
},
isText(value: any): value is Text {
return isPlainObject(value) && typeof value.text === 'string'
return isObject(value) && typeof value.text === 'string'
},
isTextList(value: any): value is Text[] {

View File

@@ -1,4 +1,4 @@
import { isPlainObject } from 'is-plain-object'
import { isObject } from './is-object'
/*
Custom deep equal comparison for Slate nodes.
@@ -17,13 +17,13 @@ export const isDeepEqual = (
for (const key in node) {
const a = node[key]
const b = another[key]
if (isPlainObject(a) && isPlainObject(b)) {
if (!isDeepEqual(a, b)) return false
} else if (Array.isArray(a) && Array.isArray(b)) {
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false
}
} else if (isObject(a) && isObject(b)) {
if (!isDeepEqual(a, b)) return false
} else if (a !== b) {
return false
}

View File

@@ -1,5 +1,6 @@
export * from './deep-equal'
export * from './get-default-insert-location'
export * from './is-object'
export * from './match-path'
export * from './string'
export * from './types'

View File

@@ -0,0 +1,2 @@
export const isObject = (value: any) =>
typeof value === 'object' && value !== null

View File

@@ -13302,7 +13302,6 @@ __metadata:
resolution: "slate-history@workspace:packages/slate-history"
dependencies:
"@babel/runtime": "npm:^7.23.2"
is-plain-object: "npm:^5.0.0"
lodash: "npm:^4.17.21"
slate: "npm:^0.114.0"
slate-hyperscript: "npm:^0.100.0"
@@ -13317,7 +13316,6 @@ __metadata:
resolution: "slate-hyperscript@workspace:packages/slate-hyperscript"
dependencies:
"@babel/runtime": "npm:^7.23.2"
is-plain-object: "npm:^5.0.0"
slate: "npm:^0.114.0"
source-map-loader: "npm:^4.0.1"
peerDependencies:
@@ -13427,7 +13425,6 @@ __metadata:
"@types/resize-observer-browser": "npm:^0.1.8"
direction: "npm:^1.0.4"
is-hotkey: "npm:^0.2.0"
is-plain-object: "npm:^5.0.0"
lodash: "npm:^4.17.21"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
@@ -13451,7 +13448,6 @@ __metadata:
dependencies:
"@babel/runtime": "npm:^7.23.2"
immer: "npm:^10.0.3"
is-plain-object: "npm:^5.0.0"
lodash: "npm:^4.17.21"
slate-hyperscript: "npm:^0.100.0"
source-map-loader: "npm:^4.0.1"