1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-09 00:36:41 +02:00

Upgrade to React v16 lifecycle (#1975)

* Trying to use memoization and upgrade to react v16

* Fix error

* Fix error

* Fix handlers error

* Add annotation

* Remove EventHandlers

* No state

* Remove un-necessary polyfill

* Remove un-necessary polyfill

* Remove un-necessary handlers settings

* Early Return

* Fix Early Return

* Fix onChange

* Do not run onChange stack twice on same change

* Update annotation

* Better sense of resolve++

* Cache value in onChange and didMount

* Remove un-necessary rechack

* Renaming

* Remove change in leaf.js

* Handlers as this.handlers

* do not re-initialize change in onChange

* Re-run onChange stack only when change happens

* Update value when stack changes

* Rename to memoize-one

* queue changes

* Unify interface

* Fix bug

* Add document

* Remove id

* Do not use map

* Fix bug

* Fix eslint

* Fix update when props.value changes

* Add annotation

* Fix stack

* Inline queueChange

* Restore onChange

* restore onChange

* Refactor change and onChange

* Use onChange as the single interface for update

* Do not flushChange if inside event

* Give a warning about synchronous editor.change call

* Change isInChange in editor.change

* refactor resolution and tmp logic, cleanup code
This commit is contained in:
Jinxuan Zhu
2018-08-16 17:59:29 -04:00
committed by Ian Storm Taylor
parent d05e90e546
commit 877dea16bf
7 changed files with 287 additions and 220 deletions

View File

@@ -46,6 +46,7 @@
"lerna": "^2.7.1", "lerna": "^2.7.1",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"matcha": "^0.7.0", "matcha": "^0.7.0",
"memoize-one": "^4.0.0",
"mocha": "^2.5.3", "mocha": "^2.5.3",
"mocha-lcov-reporter": "^1.3.0", "mocha-lcov-reporter": "^1.3.0",
"npm-run-all": "^4.1.2", "npm-run-all": "^4.1.2",

View File

@@ -18,6 +18,7 @@
"is-window": "^1.0.2", "is-window": "^1.0.2",
"keycode": "^2.1.2", "keycode": "^2.1.2",
"lodash": "^4.1.1", "lodash": "^4.1.1",
"memoize-one": "^4.0.0",
"prop-types": "^15.5.8", "prop-types": "^15.5.8",
"react-immutable-proptypes": "^2.1.0", "react-immutable-proptypes": "^2.1.0",
"react-portal": "^3.1.0", "react-portal": "^3.1.0",

View File

@@ -60,23 +60,26 @@ class Content extends React.Component {
} }
/** /**
* Constructor. * Temporary values.
* *
* @param {Object} props * @type {Object}
*/ */
constructor(props) { tmp = {
super(props) isUpdatingSelection: false,
this.tmp = {}
this.tmp.isUpdatingSelection = false
EVENT_HANDLERS.forEach(handler => {
this[handler] = event => {
this.onEvent(handler, event)
}
})
} }
/**
* Create a set of bound event handlers.
*
* @type {Object}
*/
handlers = EVENT_HANDLERS.reduce((obj, handler) => {
obj[handler] = event => this.onEvent(handler, event)
return obj
}, {})
/** /**
* When the editor first mounts in the DOM we need to: * When the editor first mounts in the DOM we need to:
* *
@@ -84,7 +87,7 @@ class Content extends React.Component {
* - Update the selection, in case it starts focused. * - Update the selection, in case it starts focused.
*/ */
componentDidMount = () => { componentDidMount() {
const window = getWindow(this.element) const window = getWindow(this.element)
window.document.addEventListener( window.document.addEventListener(
@@ -124,7 +127,7 @@ class Content extends React.Component {
* On update, update the selection. * On update, update the selection.
*/ */
componentDidUpdate = () => { componentDidUpdate() {
this.updateSelection() this.updateSelection()
} }
@@ -363,7 +366,7 @@ class Content extends React.Component {
*/ */
render() { render() {
const { props } = this const { props, handlers } = this
const { const {
className, className,
readOnly, readOnly,
@@ -386,11 +389,6 @@ class Content extends React.Component {
return this.renderNode(child, isSelected, childrenDecorations[i]) return this.renderNode(child, isSelected, childrenDecorations[i])
}) })
const handlers = EVENT_HANDLERS.reduce((obj, handler) => {
obj[handler] = this[handler]
return obj
}, {})
const style = { const style = {
// Prevent the default outline styles. // Prevent the default outline styles.
outline: 'none', outline: 'none',
@@ -417,21 +415,6 @@ class Content extends React.Component {
contentEditable={readOnly ? null : true} contentEditable={readOnly ? null : true}
suppressContentEditableWarning suppressContentEditableWarning
className={className} className={className}
onBlur={this.onBlur}
onFocus={this.onFocus}
onCompositionEnd={this.onCompositionEnd}
onCompositionStart={this.onCompositionStart}
onCopy={this.onCopy}
onCut={this.onCut}
onDragEnd={this.onDragEnd}
onDragOver={this.onDragOver}
onDragStart={this.onDragStart}
onDrop={this.onDrop}
onInput={this.onInput}
onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
onPaste={this.onPaste}
onSelect={this.onSelect}
autoCorrect={props.autoCorrect ? 'on' : 'off'} autoCorrect={props.autoCorrect ? 'on' : 'off'}
spellCheck={spellCheck} spellCheck={spellCheck}
style={style} style={style}

View File

@@ -5,6 +5,7 @@ import SlateTypes from 'slate-prop-types'
import Types from 'prop-types' import Types from 'prop-types'
import logger from 'slate-dev-logger' import logger from 'slate-dev-logger'
import { Schema, Stack } from 'slate' import { Schema, Stack } from 'slate'
import memoizeOne from 'memoize-one'
import EVENT_HANDLERS from '../constants/event-handlers' import EVENT_HANDLERS from '../constants/event-handlers'
import PLUGINS_PROPS from '../constants/plugin-props' import PLUGINS_PROPS from '../constants/plugin-props'
@@ -66,94 +67,61 @@ class Editor extends React.Component {
} }
/** /**
* Constructor. * Initial state.
* *
* @param {Object} props * @type {Object}
*/ */
constructor(props) { state = {}
super(props)
this.state = {}
this.tmp = {}
this.tmp.updates = 0
this.tmp.resolves = 0
// Resolve the plugins and create a stack and schema from them. /**
const plugins = this.resolvePlugins(props.plugins, props.schema) * Temporary values.
const stack = Stack.create({ plugins }) *
const schema = Schema.create({ plugins }) * @type {Object}
this.state.schema = schema */
this.state.stack = stack
// Run `onChange` on the passed-in value because we need to ensure that it tmp = {
// is normalized, and queue the resulting change. change: null,
const change = props.value.change() isChanging: false,
stack.run('onChange', change, this) operationsSize: null,
this.queueChange(change) plugins: null,
this.state.value = change.value resolves: 0,
updates: 0,
// Create a bound event handler for each event. value: null,
EVENT_HANDLERS.forEach(handler => {
this[handler] = (...args) => {
this.onEvent(handler, ...args)
}
})
} }
/** /**
* When the `props` are updated, create a new `Stack` if necessary and run * Create a set of bound event handlers.
* `onChange` to ensure the value is normalized.
* *
* @param {Object} props * @type {Object}
*/ */
componentWillReceiveProps = props => { handlers = EVENT_HANDLERS.reduce((obj, handler) => {
let { schema, stack } = this obj[handler] = event => this.onEvent(handler, event)
return obj
}, {})
// Increment the updates counter as a baseline. /**
* When the component first mounts, flush any temporary changes, and then,
* focus the editor if `autoFocus` is set.
*/
componentDidMount() {
this.tmp.updates++ this.tmp.updates++
// If the plugins or the schema have changed, we need to re-resolve the const { autoFocus } = this.props
// plugins, since it will result in a new stack and new validations. const { change } = this.tmp
if (
props.plugins != this.props.plugins ||
props.schema != this.props.schema
) {
const plugins = this.resolvePlugins(props.plugins, props.schema)
stack = Stack.create({ plugins })
schema = Schema.create({ plugins })
this.setState({ schema, stack })
// Increment the resolves counter. if (autoFocus) {
this.tmp.resolves++ if (change) {
change.focus()
// If we've resolved a few times already, and it's exactly in line with } else {
// the updates, then warn the user that they may be doing something wrong. this.focus()
if (this.tmp.resolves > 5 && this.tmp.resolves == this.tmp.updates) {
logger.warn(
'A Slate <Editor> is re-resolving `props.plugins` or `props.schema` on each update, which leads to poor performance. This is often due to passing in a new `schema` or `plugins` prop with each render by declaring them inline in your render function. Do not do this!'
)
} }
} }
// Run `onChange` on the passed-in value because we need to ensure that it if (change) {
// is normalized, and queue the resulting change. this.onChange(change)
const change = props.value.change()
stack.run('onChange', change, this)
this.queueChange(change)
this.setState({ value: change.value })
}
/**
* When the component first mounts, flush any temporary changes,
* and then, focus the editor if `autoFocus` is set.
*/
componentDidMount = () => {
this.flushChange()
if (this.props.autoFocus) {
this.focus()
} }
} }
@@ -161,113 +129,24 @@ class Editor extends React.Component {
* When the component updates, flush any temporary change. * When the component updates, flush any temporary change.
*/ */
componentDidUpdate = () => { componentDidUpdate(prevProps) {
this.flushChange() this.tmp.updates++
}
/** const { change, resolves, updates } = this.tmp
* Queue a `change` object, to be able to flush it later. This is required for
* when a change needs to be applied to the value, but because of the React
* lifecycle we can't apply that change immediately. So we cache it here and
* later can call `this.flushChange()` to flush it.
*
* @param {Change} change
*/
queueChange = change => { // If we've resolved a few times already, and it's exactly in line with
if (change.operations.size) { // the updates, then warn the user that they may be doing something wrong.
debug('queueChange', { change }) if (resolves > 5 && resolves === updates) {
this.tmp.change = change logger.warn(
'A Slate <Editor> component is re-resolving `props.plugins` or `props.schema` on each update, which leads to poor performance. This is often due to passing in a new `schema` or `plugins` prop with each render by declaring them inline in your render function. Do not do this!'
)
} }
}
/**
* Flush a temporarily stored `change` object, for when a change needed to be
* made but couldn't because of React's lifecycle.
*/
flushChange = () => {
const { change } = this.tmp
if (change) { if (change) {
debug('flushChange', { change }) this.onChange(change)
delete this.tmp.change
this.props.onChange(change)
} }
} }
/**
* Perform a change on the editor, passing `...args` to `change.call`.
*
* @param {Mixed} ...args
*/
change = (...args) => {
const change = this.value.change().call(...args)
this.onChange(change)
}
/**
* Programmatically blur the editor.
*/
blur = () => {
this.change(c => c.blur())
}
/**
* Programmatically focus the editor.
*/
focus = () => {
this.change(c => c.focus())
}
/**
* Getters for exposing public properties of the editor's state.
*/
get schema() {
return this.state.schema
}
get stack() {
return this.state.stack
}
get value() {
return this.state.value
}
/**
* On event.
*
* @param {String} handler
* @param {Event} event
*/
onEvent = (handler, event) => {
this.change(change => {
this.stack.run(handler, event, change, this)
})
}
/**
* On change.
*
* @param {Change} change
*/
onChange = change => {
debug('onChange', { change })
this.stack.run('onChange', change, this)
const { value } = change
const { onChange } = this.props
if (value == this.value) return
onChange(change)
}
/** /**
* Render the editor. * Render the editor.
* *
@@ -291,7 +170,162 @@ class Editor extends React.Component {
} }
/** /**
* Resolve an array of plugins from `plugins` and `schema` props. * Get the editor's current plugins.
*
* @return {Array}
*/
get plugins() {
const plugins = this.resolvePlugins(this.props.plugins, this.props.schema)
return plugins
}
/**
* Get the editor's current schema.
*
* @return {Schema}
*/
get schema() {
const schema = this.resolveSchema(this.plugins)
return schema
}
/**
* Get the editor's current stack.
*
* @return {Stack}
*/
get stack() {
const stack = this.resolveStack(this.plugins)
return stack
}
/**
* Get the editor's current value.
*
* @return {Value}
*/
get value() {
// If the current `plugins` and `value` are the same as the last seen ones
// that were saved in `tmp`, don't re-resolve because that will trigger
// extra `onChange` runs.
if (
this.plugins === this.tmp.plugins &&
this.props.value === this.tmp.value
) {
return this.tmp.value
}
const value = this.resolveValue(this.plugins, this.props.value)
return value
}
/**
* Perform a change on the editor, passing `...args` to `change.call`.
*
* @param {Mixed} ...args
*/
change = (...args) => {
if (this.tmp.isChanging) {
logger.warn(
"The `editor.change` method was called from within an existing `editor.change` callback. This is not allowed, and often due to calling `editor.change` directly from a plugin's event handler which is unnecessary."
)
return
}
const change = this.value.change()
try {
this.tmp.isChanging = true
change.call(...args)
} catch (error) {
throw error
} finally {
this.tmp.isChanging = false
}
this.onChange(change)
}
/**
* Programmatically blur the editor.
*/
blur = () => {
this.change(c => c.blur())
}
/**
* Programmatically focus the editor.
*/
focus = () => {
this.change(c => c.focus())
}
/**
* On change.
*
* @param {Change} change
*/
onChange = change => {
// If the change doesn't define any operations to apply, abort.
if (change.operations.size === 0) {
return
}
debug('onChange', { change })
change = this.resolveChange(this.plugins, change, change.operations.size)
// Store a reference to the last `value` and `plugins` that were seen by the
// editor, so we can know whether to normalize a new unknown value if one
// is passed in via `this.props`.
this.tmp.value = change.value
this.tmp.plugins = this.plugins
// Remove the temporary `change`, since it's being flushed.
delete this.tmp.change
delete this.tmp.operationsSize
this.props.onChange(change)
}
/**
* On event.
*
* @param {String} handler
* @param {Event} event
*/
onEvent = (handler, event) => {
this.change(change => {
this.stack.run(handler, event, change, this)
})
}
/**
* Resolve a change from the current `plugins`, a potential `change` and its
* current operations `size`.
*
* @param {Array} plugins
* @param {Change} change
* @param {Number} size
*/
resolveChange = memoizeOne((plugins, change, size) => {
const stack = this.resolveStack(plugins)
stack.run('onChange', change, this)
return change
})
/**
* Resolve a set of plugins from potential `plugins` and a `schema`.
* *
* In addition to the plugins provided in props, this will initialize three * In addition to the plugins provided in props, this will initialize three
* other plugins: * other plugins:
@@ -304,19 +338,20 @@ class Editor extends React.Component {
* @return {Array} * @return {Array}
*/ */
resolvePlugins = (plugins, schema) => { resolvePlugins = memoizeOne((plugins = [], schema = {}) => {
debug('resolvePlugins', { plugins, schema })
this.tmp.resolves++
const beforePlugin = BeforePlugin() const beforePlugin = BeforePlugin()
const afterPlugin = AfterPlugin() const afterPlugin = AfterPlugin()
const editorPlugin = { const editorPlugin = { schema }
schema: schema || {},
}
for (const prop of PLUGINS_PROPS) { for (const prop of PLUGINS_PROPS) {
// Skip `onChange` because the editor's `onChange` is special. // Skip `onChange` because the editor's `onChange` is special.
if (prop == 'onChange') continue if (prop == 'onChange') continue
// Skip `schema` because it can't be proxied easily, so it must be // Skip `schema` because it can't be proxied easily, so it must be passed
// passed in as an argument to this function instead. // in as an argument to this function instead.
if (prop == 'schema') continue if (prop == 'schema') continue
// Define a function that will just proxies into `props`. // Define a function that will just proxies into `props`.
@@ -325,12 +360,59 @@ class Editor extends React.Component {
} }
} }
return [beforePlugin, editorPlugin, ...(plugins || []), afterPlugin] return [beforePlugin, editorPlugin, ...plugins, afterPlugin]
} })
/**
* Resolve a schema from the current `plugins`.
*
* @param {Array} plugins
* @return {Schema}
*/
resolveSchema = memoizeOne(plugins => {
debug('resolveSchema', { plugins })
const schema = Schema.create({ plugins })
return schema
})
/**
* Resolve a stack from the current `plugins`.
*
* @param {Array} plugins
* @return {Stack}
*/
resolveStack = memoizeOne(plugins => {
debug('resolveStack', { plugins })
const stack = Stack.create({ plugins })
return stack
})
/**
* Resolve a value from the current `plugins` and a potential `value`.
*
* @param {Array} plugins
* @param {Value} value
* @return {Change}
*/
resolveValue = memoizeOne((plugins, value) => {
debug('resolveValue', { plugins, value })
let change = value.change()
change = this.resolveChange(plugins, change, change.operations.size)
// Store the change and it's operations count so that it can be flushed once
// the component next updates.
this.tmp.change = change
this.tmp.operationsSize = change.operations.size
return change.value
})
} }
/** /**
* Mix in the property types for the event handlers. * Mix in the prop types for the event handlers.
*/ */
for (const prop of EVENT_HANDLERS) { for (const prop of EVENT_HANDLERS) {

View File

@@ -62,7 +62,7 @@ class Node extends React.Component {
* @return {Boolean} * @return {Boolean}
*/ */
shouldComponentUpdate = nextProps => { shouldComponentUpdate(nextProps) {
const { props } = this const { props } = this
const { stack } = props.editor const { stack } = props.editor
const shouldUpdate = stack.find( const shouldUpdate = stack.find(

View File

@@ -7,7 +7,6 @@ import getWindow from 'get-window'
import { Block, Inline, Text } from 'slate' import { Block, Inline, Text } from 'slate'
import Hotkeys from 'slate-hotkeys' import Hotkeys from 'slate-hotkeys'
import EVENT_HANDLERS from '../constants/event-handlers'
import Content from '../components/content' import Content from '../components/content'
import cloneFragment from '../utils/clone-fragment' import cloneFragment from '../utils/clone-fragment'
import findDOMNode from '../utils/find-dom-node' import findDOMNode from '../utils/find-dom-node'
@@ -683,10 +682,7 @@ function AfterPlugin() {
*/ */
function renderEditor(props, editor) { function renderEditor(props, editor) {
const handlers = EVENT_HANDLERS.reduce((obj, handler) => { const { handlers } = editor
obj[handler] = editor[handler]
return obj
}, {})
return ( return (
<Content <Content

View File

@@ -5532,6 +5532,10 @@ mem@^1.1.0:
dependencies: dependencies:
mimic-fn "^1.0.0" mimic-fn "^1.0.0"
memoize-one@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.0.tgz#fc5e2f1427a216676a62ec652cf7398cfad123db"
memory-fs@^0.4.0, memory-fs@~0.4.1: memory-fs@^0.4.0, memory-fs@~0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"