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": [ "files": [
"dist/" "dist/"
], ],
"dependencies": {
"is-plain-object": "^5.0.0"
},
"devDependencies": { "devDependencies": {
"@babel/runtime": "^7.23.2", "@babel/runtime": "^7.23.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import { isPlainObject } from 'is-plain-object' import { Range, isObject } from '..'
import { Path, Range } from '..'
import { ExtendedType } from '../types/custom-types' import { ExtendedType } from '../types/custom-types'
import { isDeepEqual } from '../utils/deep-equal' import { isDeepEqual } from '../utils/deep-equal'
@@ -93,7 +92,7 @@ export const Text: TextInterface = {
}, },
isText(value: any): value is Text { 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[] { 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. Custom deep equal comparison for Slate nodes.
@@ -17,13 +17,13 @@ export const isDeepEqual = (
for (const key in node) { for (const key in node) {
const a = node[key] const a = node[key]
const b = another[key] const b = another[key]
if (isPlainObject(a) && isPlainObject(b)) { if (Array.isArray(a) && Array.isArray(b)) {
if (!isDeepEqual(a, b)) return false
} else if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) { for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false if (a[i] !== b[i]) return false
} }
} else if (isObject(a) && isObject(b)) {
if (!isDeepEqual(a, b)) return false
} else if (a !== b) { } else if (a !== b) {
return false return false
} }

View File

@@ -1,5 +1,6 @@
export * from './deep-equal' export * from './deep-equal'
export * from './get-default-insert-location' export * from './get-default-insert-location'
export * from './is-object'
export * from './match-path' export * from './match-path'
export * from './string' export * from './string'
export * from './types' 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" resolution: "slate-history@workspace:packages/slate-history"
dependencies: dependencies:
"@babel/runtime": "npm:^7.23.2" "@babel/runtime": "npm:^7.23.2"
is-plain-object: "npm:^5.0.0"
lodash: "npm:^4.17.21" lodash: "npm:^4.17.21"
slate: "npm:^0.114.0" slate: "npm:^0.114.0"
slate-hyperscript: "npm:^0.100.0" slate-hyperscript: "npm:^0.100.0"
@@ -13317,7 +13316,6 @@ __metadata:
resolution: "slate-hyperscript@workspace:packages/slate-hyperscript" resolution: "slate-hyperscript@workspace:packages/slate-hyperscript"
dependencies: dependencies:
"@babel/runtime": "npm:^7.23.2" "@babel/runtime": "npm:^7.23.2"
is-plain-object: "npm:^5.0.0"
slate: "npm:^0.114.0" slate: "npm:^0.114.0"
source-map-loader: "npm:^4.0.1" source-map-loader: "npm:^4.0.1"
peerDependencies: peerDependencies:
@@ -13427,7 +13425,6 @@ __metadata:
"@types/resize-observer-browser": "npm:^0.1.8" "@types/resize-observer-browser": "npm:^0.1.8"
direction: "npm:^1.0.4" direction: "npm:^1.0.4"
is-hotkey: "npm:^0.2.0" is-hotkey: "npm:^0.2.0"
is-plain-object: "npm:^5.0.0"
lodash: "npm:^4.17.21" lodash: "npm:^4.17.21"
react: "npm:^18.2.0" react: "npm:^18.2.0"
react-dom: "npm:^18.2.0" react-dom: "npm:^18.2.0"
@@ -13451,7 +13448,6 @@ __metadata:
dependencies: dependencies:
"@babel/runtime": "npm:^7.23.2" "@babel/runtime": "npm:^7.23.2"
immer: "npm:^10.0.3" immer: "npm:^10.0.3"
is-plain-object: "npm:^5.0.0"
lodash: "npm:^4.17.21" lodash: "npm:^4.17.21"
slate-hyperscript: "npm:^0.100.0" slate-hyperscript: "npm:^0.100.0"
source-map-loader: "npm:^4.0.1" source-map-loader: "npm:^4.0.1"