diff --git a/compat/src/index.js b/compat/src/index.js index a35478a2a6..b5e7b5ff58 100644 --- a/compat/src/index.js +++ b/compat/src/index.js @@ -92,7 +92,7 @@ function findDOMNode(component) { return component; } - return getChildDom(component._internal); + return getChildDom(component._internal, 0); } /** @@ -120,7 +120,7 @@ const StrictMode = Fragment; * @param {Arg} [arg] Optional arugment that can be passed to the callback * @returns */ -const flushSync = (callback, arg) => callback(arg) +const flushSync = (callback, arg) => callback(arg); export * from 'preact/hooks'; export { diff --git a/hooks/src/index.js b/hooks/src/index.js index 1729a0a630..1797a7e99a 100644 --- a/hooks/src/index.js +++ b/hooks/src/index.js @@ -144,7 +144,7 @@ export function useReducer(reducer, initialState, init) { const nextValue = hookState._reducer(hookState._value[0], action); if (hookState._value[0] !== nextValue) { hookState._value = [nextValue, hookState._value[1]]; - hookState._internal.rerender(hookState._internal); + hookState._internal.render(); } } ]; diff --git a/jsx-runtime/src/index.js b/jsx-runtime/src/index.js index 9de5249d3d..c1754d70f6 100644 --- a/jsx-runtime/src/index.js +++ b/jsx-runtime/src/index.js @@ -28,8 +28,11 @@ function createVNode(type, props, key, __source, __self) { // forwardRef components in the future, but that should happen via // a separate PR. let normalizedProps = {}; + let ref; for (let i in props) { - if (i != 'ref') { + if (i === 'ref') { + ref = props[i]; + } else { normalizedProps[i] = props[i]; } } @@ -38,24 +41,15 @@ function createVNode(type, props, key, __source, __self) { type, props: normalizedProps, key, - ref: props && props.ref, + ref, constructor: undefined, _vnodeId: --vnodeId, __source, __self }; - // If a Component VNode, check for and apply defaultProps. - // Note: `type` is often a String, and can be `undefined` in development. - let defaults, i; - if (typeof type === 'function' && (defaults = type.defaultProps)) { - for (i in defaults) - if (normalizedProps[i] === undefined) { - normalizedProps[i] = defaults[i]; - } - } - if (options.vnode) options.vnode(vnode); + return vnode; } diff --git a/jsx-runtime/test/browser/jsx-runtime.test.js b/jsx-runtime/test/browser/jsx-runtime.test.js index 83dc82afec..aef900a834 100644 --- a/jsx-runtime/test/browser/jsx-runtime.test.js +++ b/jsx-runtime/test/browser/jsx-runtime.test.js @@ -31,7 +31,8 @@ describe('Babel jsx/jsxDEV', () => { expect(vnode.key).to.equal('foo'); }); - it('should apply defaultProps', () => { + // We no longer support defaultProps. + it.skip('should apply defaultProps', () => { class Foo extends Component { render() { return
; @@ -48,7 +49,7 @@ describe('Babel jsx/jsxDEV', () => { }); }); - it('should keep props over defaultProps', () => { + it.skip('should keep props over defaultProps', () => { class Foo extends Component { render() { return
; @@ -81,6 +82,7 @@ describe('Babel jsx/jsxDEV', () => { delete jsxVNode.__source; delete jsxVNode._vnodeId; delete elementVNode._vnodeId; + // expect(jsxVNode.constructor).to.equal(elementVNode.constructor); expect(jsxVNode).to.deep.equal(elementVNode); }); diff --git a/mangle.json b/mangle.json index 14b3d639e2..ebd5ae7c4b 100644 --- a/mangle.json +++ b/mangle.json @@ -34,6 +34,7 @@ "props": { "cname": 6, "props": { + "$flags": "f", "$_vnodeId": "__v", "$_cleanup": "__c", "$_afterPaintQueued": "__a", diff --git a/src/clone-element.js b/src/clone-element.js index 62669d8202..dba60c261b 100644 --- a/src/clone-element.js +++ b/src/clone-element.js @@ -1,4 +1,5 @@ -import { createVNode } from './create-element'; +import { EMPTY_ARR } from './constants'; +import { createElement } from './create-element'; /** * Clones the given VNode, optionally adding attributes/props and replacing its children. @@ -8,33 +9,13 @@ import { createVNode } from './create-element'; * @returns {import('./internal').VNode} */ export function cloneElement(vnode, props, children) { - let normalizedProps = Object.assign({}, vnode.props), - key, - ref, - i; - - for (i in props) { - if (i == 'key') key = props[i]; - else if (i == 'ref') ref = props[i]; - else normalizedProps[i] = props[i]; - } - if (arguments.length > 3) { - children = [children]; - for (i = 3; i < arguments.length; i++) { - children.push(arguments[i]); - } - } - - if (arguments.length > 2) { - normalizedProps.children = children; + children = EMPTY_ARR.slice.call(arguments, 2); } - return createVNode( + return createElement( vnode.type, - normalizedProps, - key || vnode.key, - ref || vnode.ref, - 0 + Object.assign({ key: vnode.key, ref: vnode.ref }, vnode.props, props), + children ); } diff --git a/src/component.js b/src/component.js index 1b63e02608..9dc37a059a 100644 --- a/src/component.js +++ b/src/component.js @@ -1,4 +1,4 @@ -import { addCommitCallback, commitRoot } from './diff/commit'; +import { commitRoot } from './diff/commit'; import options from './options'; import { createVNode, Fragment } from './create-element'; import { patch } from './diff/patch'; @@ -47,35 +47,39 @@ Component.prototype.setState = function(update, callback) { update = update(Object.assign({}, s), this.props); } - if (update) { - Object.assign(s, update); - } + Object.assign(s, update); // Skip update if updater function returned null if (update == null) return; - if (this._internal) { - if (callback) addCommitCallback(this._internal, callback.bind(this)); - this._internal.rerender(this._internal); - } + // The 0 flag value here prevents FORCE_UPDATE from being set + renderComponentInstance.call(this, callback, 0); }; /** * Immediately perform a synchronous re-render of the component + * @param {() => void} [callback] A function to call after re-rendering completes + * @this {import('./internal').Component} + */ +Component.prototype.forceUpdate = renderComponentInstance; + +/** + * Immediately perform a synchronous re-render of the component. + * This method is the implementation of forceUpdate() for class components. + * @param {() => void} [callback] A function to call after rendering completes + * @param {number} [flags = FORCE_UPDATE] Flags to set. Defaults to FORCE_UPDATE. * @this {import('./internal').Component} - * @param {() => void} [callback] A function to be called after component is - * re-rendered */ -Component.prototype.forceUpdate = function(callback) { +export function renderComponentInstance(callback, flags) { if (this._internal) { // Set render mode so that we can differentiate where the render request - // is coming from. We need this because forceUpdate should never call - // shouldComponentUpdate - this._internal.flags |= FORCE_UPDATE; - if (callback) addCommitCallback(this._internal, callback.bind(this)); - this._internal.rerender(this._internal); + // is coming from (eg: forceUpdate should never call shouldComponentUpdate). + this._internal.flags |= flags == null ? FORCE_UPDATE : flags; + this._internal.render(callback); + // Note: the above is equivalent to invoking enqueueRender: + // enqueueRender.call(this._internal, callback); } -}; +} /** * Accepts `props` and `state`, and returns a new Virtual DOM tree to build. @@ -90,36 +94,36 @@ Component.prototype.forceUpdate = function(callback) { Component.prototype.render = Fragment; /** - * @param {import('./internal').Component} internal The internal to rerender + * Render an Internal that has been marked + * @param {import('./internal').Internal} internal The internal to rerender */ -function rerender(internal) { - if (~internal.flags & MODE_UNMOUNTING && internal.flags & DIRTY_BIT) { - let parentDom = getParentDom(internal); - let startDom = - (internal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) === - (MODE_HYDRATE | MODE_SUSPENDED) - ? internal._dom - : getDomSibling(internal, 0); - - const vnode = createVNode( - internal.type, - internal.props, - internal.key, // @TODO we shouldn't need to actually pass these - internal.ref, // since the mode flag should bypass key/ref handling - 0 - ); - - const commitQueue = []; - patch(parentDom, vnode, internal, commitQueue, startDom); - commitRoot(commitQueue, internal); - } -} +const renderQueuedInternal = internal => { + const commitQueue = []; + + const vnode = createVNode(internal.type, internal.props); + + // Don't render unmounting/unmounted trees: + if (internal.flags & MODE_UNMOUNTING) return; + + // Don't render trees already rendered in this pass: + if (!(internal.flags & DIRTY_BIT)) return; + + let parentDom = getParentDom(internal); + let startDom = + (internal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) === + (MODE_HYDRATE | MODE_SUSPENDED) + ? internal._dom + : getDomSibling(internal, 0); + + patch(parentDom, vnode, internal, commitQueue, startDom); + commitRoot(commitQueue, internal); +}; /** - * The render queue - * @type {Array} + * A queue of Internals to be rendered in the next batch. + * @type {Array} */ -let rerenderQueue = []; +let renderQueue = []; /* * The value of `Component.debounce` must asynchronously invoke the passed in callback. It is @@ -136,28 +140,35 @@ const defer = Promise.prototype.then.bind(Promise.resolve()); /** * Enqueue a rerender of a component - * @param {import('./internal').Component} internal The internal to rerender + * @this {import('./internal').Internal} internal The internal to rerender */ -export function enqueueRender(internal) { +export function enqueueRender(callback) { + let internal = this; + if (callback) { + if (internal._commitCallbacks == null) { + internal._commitCallbacks = []; + } + internal._commitCallbacks.push(callback); + } if ( (!(internal.flags & DIRTY_BIT) && (internal.flags |= DIRTY_BIT) && - rerenderQueue.push(internal) && - !process._rerenderCount++) || + renderQueue.push(internal) && + !processRenderQueue._rerenderCount++) || prevDebounce !== options.debounceRendering ) { prevDebounce = options.debounceRendering; - (prevDebounce || defer)(process); + (prevDebounce || defer)(processRenderQueue); } } /** Flush the render queue by rerendering all queued components */ -function process() { - while ((len = process._rerenderCount = rerenderQueue.length)) { - rerenderQueue.sort((a, b) => a._depth - b._depth); +function processRenderQueue() { + while ((len = processRenderQueue._rerenderCount = renderQueue.length)) { + renderQueue.sort((a, b) => a._depth - b._depth); while (len--) { - rerender(rerenderQueue.shift()); + renderQueuedInternal(renderQueue.shift()); } } } -let len = (process._rerenderCount = 0); +let len = (processRenderQueue._rerenderCount = 0); diff --git a/src/create-context.js b/src/create-context.js index 8fe6f29bb6..b10ed89392 100644 --- a/src/create-context.js +++ b/src/create-context.js @@ -1,5 +1,3 @@ -import { enqueueRender } from './component'; - let nextContextId = 0; const providers = new Set(); @@ -9,7 +7,7 @@ export const unsubscribeFromContext = internal => { if (providers.delete(internal)) return; // ... otherwise, unsubscribe from any contexts: providers.forEach(p => { - p._component._subs.delete(internal); + p._subs.delete(internal); }); }; @@ -21,9 +19,6 @@ export const createContext = (defaultValue, contextId) => { _defaultValue: defaultValue, /** @type {import('./internal').FunctionComponent} */ Consumer(props, contextValue) { - // return props.children( - // context[contextId] ? context[contextId].props.value : defaultValue - // ); return props.children(contextValue); }, /** @type {import('./internal').FunctionComponent} */ @@ -34,11 +29,11 @@ export const createContext = (defaultValue, contextId) => { ctx = {}; ctx[contextId] = this; this.getChildContext = () => ctx; - providers.add(this._internal); + providers.add(this); } // re-render subscribers in response to value change else if (props.value !== this._prev) { - this._subs.forEach(enqueueRender); + this._subs.forEach(i => i.render()); } this._prev = props.value; diff --git a/src/create-element.js b/src/create-element.js index d5955d0aa1..270292ffc9 100644 --- a/src/create-element.js +++ b/src/create-element.js @@ -12,53 +12,45 @@ let vnodeId = 0; * @returns {import('./internal').VNode} */ export function createElement(type, props, children) { - let normalizedProps = {}, - key, - ref, - i; - for (i in props) { - if (i == 'key') key = props[i]; - else if (i == 'ref') ref = props[i]; - else normalizedProps[i] = props[i]; + /** @type {import('./internal').VNode['props']} */ + let normalizedProps = {}; + let key; + let ref; + + if (props != null) { + for (let i in props) { + if (i === 'key') { + key = props[i]; + } else if (i === 'ref') { + ref = props[i]; + } else { + normalizedProps[i] = props[i]; + } + } } if (arguments.length > 3) { children = [children]; // https://github.com/preactjs/preact/issues/1916 - for (i = 3; i < arguments.length; i++) { + for (let i = 3; i < arguments.length; i++) { children.push(arguments[i]); } } - - if (arguments.length > 2) { + if (children !== undefined) { normalizedProps.children = children; } - return createVNode(type, normalizedProps, key, ref, 0); + return createVNode(type, normalizedProps, key, ref); } -/** - * Create a VNode (used internally by Preact) - * @param {import('./internal').VNode["type"]} type The node name or Component - * Constructor for this virtual node - * @param {object | string | number | null} props The properties of this virtual node. - * If this virtual node represents a text node, this is the text of the node (string or number). - * @param {string | number | null} key The key for this virtual node, used when - * diffing it against its children - * @param {import('./internal').VNode["ref"]} ref The ref property that will - * receive a reference to its created child - * @returns {import('./internal').VNode} - */ -export function createVNode(type, props, key, ref, original) { - // V8 seems to be better at detecting type shapes if the object is allocated from the same call site - // Do not inline into createElement and coerceToVNode! +export function createVNode(type, props, key, ref) { const vnode = { type, props, key, ref, constructor: undefined, - _vnodeId: original || ++vnodeId + _vnodeId: ++vnodeId }; if (options.vnode != null) options.vnode(vnode); @@ -77,7 +69,7 @@ export function normalizeToVNode(childVNode) { if (typeof childVNode === 'object') { return Array.isArray(childVNode) - ? createVNode(Fragment, { children: childVNode }, null, null, 0) + ? createElement(Fragment, null, childVNode) : childVNode; } diff --git a/src/create-root.js b/src/create-root.js index bdf3b0506c..7f37f0befc 100644 --- a/src/create-root.js +++ b/src/create-root.js @@ -12,12 +12,12 @@ import { patch } from './diff/patch'; import { createInternal } from './tree'; /** - * - * @param {import('./internal').PreactElement} parentDom The DOM element to + * @param {import('./internal').PreactElement} parentDom A parent DOM element to render within. + * @param {import('./internal').Internal} [rootInternal] (do not use) + * @returns {import('./internal').PreactRoot} */ -export function createRoot(parentDom) { - let rootInternal, - commitQueue, +export function createRoot(parentDom, rootInternal) { + let commitQueue, firstChild, flags = 0; diff --git a/src/diff/children.js b/src/diff/children.js index 22c570080f..57ae3dcc4b 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -149,7 +149,7 @@ export function diffChildren( parentInternal.flags & TYPE_COMPONENT && startDom != null && ((oldChildren[i].flags & TYPE_DOM && oldChildren[i]._dom == startDom) || - getChildDom(oldChildren[i]) == startDom) + getChildDom(oldChildren[i], 0) == startDom) ) { // If the startDom points to a dom node that is about to be unmounted, // then get the next sibling of that vnode and set startDom to it diff --git a/src/diff/commit.js b/src/diff/commit.js index 7ca1d5f3d2..10773a7e0a 100644 --- a/src/diff/commit.js +++ b/src/diff/commit.js @@ -1,17 +1,5 @@ import options from '../options'; -/** - * @param {import('../internal').Internal} internal - * @param {() => void} callback - */ -export function addCommitCallback(internal, callback) { - if (internal._commitCallbacks == null) { - internal._commitCallbacks = []; - } - - internal._commitCallbacks.push(callback); -} - /** * @param {import('../internal').CommitQueue} commitQueue List of components * which have callbacks to invoke in commitRoot @@ -26,7 +14,7 @@ export function commitRoot(commitQueue, rootInternal) { commitQueue = internal._commitCallbacks.length; // @ts-ignore See above ts-ignore comment while (commitQueue--) { - internal._commitCallbacks.shift()(); + internal._commitCallbacks.shift().call(internal._component); } } catch (e) { options._catchError(e, internal); diff --git a/src/diff/component.js b/src/diff/component.js index 55ac14f201..e275fabe1a 100644 --- a/src/diff/component.js +++ b/src/diff/component.js @@ -1,6 +1,6 @@ import options from '../options'; import { DIRTY_BIT, FORCE_UPDATE, SKIP_CHILDREN } from '../constants'; -import { addCommitCallback } from './commit'; +import { renderComponentInstance } from '../component'; export function renderFunctionComponent( newVNode, @@ -24,7 +24,7 @@ export function renderFunctionComponent( internal._component = c = { props: newProps, context: componentContext, - forceUpdate: internal.rerender.bind(null, internal) + forceUpdate: renderComponentInstance }; internal.flags |= DIRTY_BIT; @@ -112,7 +112,10 @@ export function renderClassComponent( // If the component was constructed, queue up componentDidMount so the // first time this internal commits (regardless of suspense or not) it // will be called - addCommitCallback(internal, c.componentDidMount.bind(c)); + if (internal._commitCallbacks == null) { + internal._commitCallbacks = []; + } + internal._commitCallbacks.push(c.componentDidMount); } } else { if ( @@ -166,7 +169,10 @@ export function renderClassComponent( // Only schedule componentDidUpdate if the component successfully rendered if (c.componentDidUpdate != null) { - addCommitCallback(internal, () => { + if (internal._commitCallbacks == null) { + internal._commitCallbacks = []; + } + internal._commitCallbacks.push(() => { c.componentDidUpdate(oldProps, oldState, snapshot); }); } diff --git a/src/diff/patch.js b/src/diff/patch.js index 013f7ca5ce..820c398734 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -77,7 +77,7 @@ export function patch(parentDom, newVNode, internal, commitQueue, startDom) { parentDom = newVNode.props._parentDom; if (parentDom !== prevParentDom) { - startDom = getChildDom(internal) || startDom; + startDom = getChildDom(internal, 0) || startDom; // The `startDom` variable might point to a node from another // tree from a previous render diff --git a/src/internal.d.ts b/src/internal.d.ts index fb53243a32..bfaf579d38 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -87,7 +87,9 @@ export interface ComponentClass

extends preact.ComponentClass

{ export type ComponentType

= ComponentClass

| FunctionComponent

; export interface PreactElement extends HTMLElement { + /** The root Internal of a tree, if one exists (minified to `__k`) */ _children?: Internal | null; + /** Event listeners to support event delegation */ _listeners?: Record void>; @@ -135,7 +137,7 @@ export interface Internal

{ /** Polymorphic property to store extensions like hooks on */ data: object; /** The function that triggers in-place re-renders for an internal */ - rerender: () => void; + render: (callback?: () => void) => void; /** children Internal nodes */ _children: Internal[]; @@ -172,3 +174,8 @@ export interface PreactContext extends preact.Context { _id: string; _defaultValue: any; } + +export interface PreactRoot { + render(VNode: vnode): void; + hydrate(VNode: vnode): void; +} diff --git a/src/render.js b/src/render.js index af50960452..b389feb427 100644 --- a/src/render.js +++ b/src/render.js @@ -7,12 +7,7 @@ import { createRoot } from './create-root'; * render into */ export function render(vnode, parentDom) { - let root = parentDom && parentDom._root; - if (!root) { - root = createRoot(parentDom); - } - root.render(vnode); - parentDom._root = root; + createRoot(parentDom, parentDom && parentDom._children).render(vnode); } /** @@ -22,10 +17,5 @@ export function render(vnode, parentDom) { * update */ export function hydrate(vnode, parentDom) { - let root = parentDom && parentDom._root; - if (!root) { - root = createRoot(parentDom); - } - root.hydrate(vnode); - parentDom._root = root; + createRoot(parentDom, parentDom && parentDom._children).hydrate(vnode); } diff --git a/src/tree.js b/src/tree.js index a302d1d139..b5fda834c5 100644 --- a/src/tree.js +++ b/src/tree.js @@ -85,7 +85,7 @@ export function createInternal(vnode, parentInternal) { key, ref, data: typeof type == 'function' ? {} : null, - rerender: enqueueRender, + render: enqueueRender, flags, _children: null, _parent: parentInternal, @@ -93,6 +93,7 @@ export function createInternal(vnode, parentInternal) { _dom: null, _component: null, _context: null, + _commitCallbacks: null, _depth: parentInternal ? parentInternal._depth + 1 : 0 }; @@ -138,23 +139,23 @@ export function getDomSibling(internal, childIndex) { /** * @param {import('./internal').Internal} internal - * @param {number} [i] + * @param {number} index The offset within children to search from * @returns {import('./internal').PreactElement} */ -export function getChildDom(internal, i) { +export function getChildDom(internal, index) { if (internal._children == null) { return null; } - for (i = i || 0; i < internal._children.length; i++) { - let child = internal._children[i]; + for (; index < internal._children.length; index++) { + let child = internal._children[index]; if (child != null) { if (child.flags & TYPE_DOM) { return child._dom; } if (shouldSearchComponent(child)) { - let childDom = getChildDom(child); + let childDom = getChildDom(child, 0); if (childDom) { return childDom; } diff --git a/test/browser/cloneElement.test.js b/test/browser/cloneElement.test.js index 80c44c86a6..f624a3ea86 100644 --- a/test/browser/cloneElement.test.js +++ b/test/browser/cloneElement.test.js @@ -38,7 +38,7 @@ describe('cloneElement', () => { expect(clone.props.children).to.deep.equal(['world', '!']); }); - it('should override children if null is provided as an argument', () => { + it('should override children if undefined is provided as an argument', () => { function Foo() {} const instance = foo; const clone = cloneElement(instance, { children: 'bar' }, null); @@ -46,7 +46,7 @@ describe('cloneElement', () => { expect(clone.prototype).to.equal(instance.prototype); expect(clone.type).to.equal(instance.type); expect(clone.props).not.to.equal(instance.props); - expect(clone.props.children).to.be.null; + expect(clone.props.children).to.equal(null); }); it('should override key if specified', () => {