From 9e341b6384079444182f5d15346df96ce1dc79d4 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 21 Dec 2022 11:26:11 -0800 Subject: [PATCH 01/71] WIP: Convert internals to use linked list for children --- compat/src/index.js | 6 +-- compat/src/render.js | 4 +- src/create-root.js | 2 +- src/diff/children.js | 10 ++-- src/diff/mount.js | 26 +++++++--- src/internal.d.ts | 12 +++-- src/tree.js | 97 ++++++++++++++++++++++++------------- test/browser/render.test.js | 2 +- 8 files changed, 99 insertions(+), 60 deletions(-) diff --git a/compat/src/index.js b/compat/src/index.js index 587dcd6c4e..0f965815c9 100644 --- a/compat/src/index.js +++ b/compat/src/index.js @@ -72,7 +72,7 @@ function cloneElement(element) { * @returns {boolean} */ function unmountComponentAtNode(container) { - if (container._children) { + if (container._child) { preactRender(null, container); return true; } @@ -112,11 +112,11 @@ const StrictMode = Fragment; /** * In React, `flushSync` flushes the entire tree and forces a rerender. It's - * implmented here as a no-op. + * implemented here as a no-op. * @template Arg * @template Result * @param {(arg: Arg) => Result} callback function that runs before the flush - * @param {Arg} [arg] Optional arugment that can be passed to the callback + * @param {Arg} [arg] Optional argument that can be passed to the callback * @returns */ const flushSync = (callback, arg) => callback(arg); diff --git a/compat/src/render.js b/compat/src/render.js index a59411e331..12cff033a2 100644 --- a/compat/src/render.js +++ b/compat/src/render.js @@ -56,14 +56,14 @@ Component.prototype.isReactComponent = {}; export function render(vnode, parent, callback) { // React destroys any existing DOM nodes, see #1727 // ...but only on the first render, see #1828 - if (parent._children == null) { + if (parent._child == null) { parent.textContent = ''; } preactRender(vnode, parent); if (typeof callback == 'function') callback(); - const internal = parent._children._children[0]; + const internal = parent._child._child; return internal ? internal._component : null; } diff --git a/src/create-root.js b/src/create-root.js index 3d2de048a9..a3b38e605f 100644 --- a/src/create-root.js +++ b/src/create-root.js @@ -35,7 +35,7 @@ export function createRoot(parentDom) { rootInternal = createInternal(vnode); // Store the VDOM tree root on the DOM element in a (minified) property: - parentDom._children = rootInternal; + parentDom._child = rootInternal; // Calling createRoot().render() on an Element with existing children triggers mutative hydrate mode: if (firstChild) { diff --git a/src/diff/children.js b/src/diff/children.js index 091a6765f8..5682985007 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -20,11 +20,11 @@ import { createInternal, getDomSibling } from '../tree'; * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered */ export function patchChildren(internal, children, parentDom) { - let oldChildren = - (internal._children && internal._children.slice()) || EMPTY_ARR; + // let oldChildren = + // (internal._children && internal._children.slice()) || EMPTY_ARR; - let oldChildrenLength = oldChildren.length; - let remainingOldChildren = oldChildrenLength; + // let oldChildrenLength = oldChildren.length; + // let remainingOldChildren = oldChildrenLength; let skew = 0; let i; @@ -44,7 +44,7 @@ export function patchChildren(internal, children, parentDom) { // Terser removes the `continue` here and wraps the loop body // in a `if (childVNode) { ... } condition if (childVNode == null) { - newChildren[i] = null; + // newChildren[i] = null; continue; } diff --git a/src/diff/mount.js b/src/diff/mount.js index 34e081dd7f..fceef8d00e 100644 --- a/src/diff/mount.js +++ b/src/diff/mount.js @@ -48,7 +48,8 @@ export function mount(internal, newVNode, parentDom, startDom) { } const renderResult = mountComponent(internal, startDom); - if (renderResult === startDom) { + // if (renderResult === startDom) { + if (renderResult === null) { nextDomSibling = startDom; } else { nextDomSibling = mountChildren( @@ -224,10 +225,13 @@ function mountElement(internal, dom) { * @param {import('../internal').PreactNode} startDom */ export function mountChildren(internal, children, parentDom, startDom) { - let internalChildren = (internal._children = []), - i, - childVNode, + // let internalChildren = (internal._children = []), + let i, + /** @type {import('../internal').Internal} */ + prevChildInternal, + /** @type {import('../internal').Internal} */ childInternal, + childVNode, newDom, mountedNextChild; @@ -237,12 +241,15 @@ export function mountChildren(internal, children, parentDom, startDom) { // Terser removes the `continue` here and wraps the loop body // in a `if (childVNode) { ... } condition if (childVNode == null) { - internalChildren[i] = null; + // @TODO - account for holes? + // internalChildren[i] = null; continue; } childInternal = createInternal(childVNode, internal); - internalChildren[i] = childInternal; + // internalChildren[i] = childInternal; + if (prevChildInternal) prevChildInternal._next = childInternal; + else internal._child = childInternal; // Morph the old element into the new one, but don't append it to the dom yet mountedNextChild = mount(childInternal, childVNode, parentDom, startDom); @@ -268,6 +275,8 @@ export function mountChildren(internal, children, parentDom, startDom) { childInternal ); } + + prevChildInternal = childInternal; } // Remove children that are not part of any vnode. @@ -292,7 +301,7 @@ export function mountChildren(internal, children, parentDom, startDom) { /** * @param {import('../internal').Internal} internal The component's backing Internal node * @param {import('../internal').PreactNode} startDom the preceding node - * @returns {import('../internal').PreactNode} the component's children + * @returns {import('../internal').ComponentChild[]} the component's children */ function mountComponent(internal, startDom) { /** @type {import('../internal').Component} */ @@ -393,7 +402,8 @@ function mountComponent(internal, startDom) { } if (renderResult == null) { - return startDom; + // return startDom; + return null; } if (typeof renderResult == 'object') { diff --git a/src/internal.d.ts b/src/internal.d.ts index 379a28fdaa..3ec800b525 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -38,7 +38,7 @@ export interface Options extends preact.Options { _render?(internal: Internal): void; /** Attach a hook that is invoked before a hook's state is queried. */ _hook?(component: Component, index: number, type: HookType): void; - /** Bypass effect execution. Currenty only used in devtools for hooks inspection */ + /** Bypass effect execution. Currently only used in devtools for hooks inspection */ _skipEffects?: boolean; /** Attach a hook that is invoked after an error is caught in a component but before calling lifecycle hooks */ _catchError?(error: any, internal: Internal): void; @@ -93,7 +93,7 @@ export type Root = { }; export interface PreactElement extends HTMLElement { - _children?: Internal | null; + _child?: Internal | null; _root?: Root | null; /** Event listeners to support event delegation */ _listeners?: Record void>; @@ -145,10 +145,12 @@ export interface Internal

{ /** The function that triggers in-place re-renders for an internal */ rerender: (internal: Internal) => void; - /** children Internal nodes */ - _children: Internal[]; - /** next sibling Internal node */ + /** parent Internal node */ _parent: Internal; + /** first child Internal node */ + _child: Internal | null; + /** next sibling Internal node */ + _next: Internal | null; /** most recent vnode ID */ _vnodeId: number; /** diff --git a/src/tree.js b/src/tree.js index cd0ec2e36b..dda7e022dc 100644 --- a/src/tree.js +++ b/src/tree.js @@ -93,8 +93,9 @@ export function createInternal(vnode, parentInternal) { : null, rerender: enqueueRender, flags, - _children: null, _parent: parentInternal, + _child: null, + _next: null, _vnodeId: vnodeId, _component: null, _context: null, @@ -112,20 +113,43 @@ const shouldSearchComponent = internal => internal.props._parentDom == getParentDom(internal._parent)); /** + * Get the next DOM Internal after a given index within a parent Internal. + * If `childIndex` is `null`, finds the next DOM Internal sibling of the given Internal. * @param {import('./internal').Internal} internal - * @param {number | null} [childIndex] + * @param {never} [childIndex] todo - replace parent+index with child internal reference * @returns {import('./internal').PreactNode} */ export function getDomSibling(internal, childIndex) { + // basically looking for the next pointer that can be used to perform an insertBefore: + // @TODO inline the null case, since it's only used in patch. if (childIndex == null) { // Use childIndex==null as a signal to resume the search from the vnode's sibling - return getDomSibling( - internal._parent, - internal._parent._children.indexOf(internal) + 1 - ); + const next = internal._next; + return next && (getChildDom(next) || getDomSibling(next)); + + // return next && (getChildDom(next) || getDomSibling(next)); + // let sibling = internal; + // while (sibling = sibling._next) { + // let domChildInternal = getChildDom(sibling); + // if (domChildInternal) return domChildInternal; + // } + + // const parent = internal._parent; + // let child = parent._child; + // while (child) { + // if (child === internal) { + // return getDomSibling(child._next); + // } + // child = child._next; + // } + + // return getDomSibling( + // internal._parent, + // internal._parent._children.indexOf(internal) + 1 + // ); } - let childDom = getChildDom(internal, childIndex); + let childDom = getChildDom(internal._child); if (childDom) { return childDom; } @@ -142,29 +166,32 @@ export function getDomSibling(internal, childIndex) { } /** - * @param {import('./internal').Internal} internal - * @param {number} offset + * Get the root DOM element for a given subtree. + * Returns the nearest DOM element within a given Internal's subtree. + * If the provided Internal _is_ a DOM Internal, its DOM will be returned. + * @param {import('./internal').Internal} internal The internal to begin the search * @returns {import('./internal').PreactElement} */ -export function getChildDom(internal, offset) { - if (internal._children == null) { - return null; - } +export function getChildDom(internal) { + while (internal) { + // this is a DOM internal + if (internal.flags & TYPE_DOM) { + // @ts-ignore this is an Element Internal, .data is a PreactElement. + return internal.data; + } - for (; offset < internal._children.length; offset++) { - let child = internal._children[offset]; - if (child != null) { - if (child.flags & TYPE_DOM) { - return child.data; - } - - if (shouldSearchComponent(child)) { - let childDom = getChildDom(child, 0); - if (childDom) { - return childDom; - } - } + // This is a Component internal (but might be a root/portal). + // Find its first DOM child, unless it's a portal: + // @todo - this is an inlined version of shouldSearchComponent without the type=component guard. + if ( + !(internal.flags & TYPE_ROOT) || + internal.props._parentDom == getParentDom(internal._parent) + ) { + const childDom = getChildDom(internal._child); + if (childDom) return childDom; } + + internal = internal._next; } return null; @@ -184,23 +211,23 @@ export function getParentContext(internal) { } /** + * Get the parent DOM for an Internal. If the Internal is a Root, returns its DOM root. * @param {import('./internal').Internal} internal * @returns {import('./internal').PreactElement} */ export function getParentDom(internal) { - let parent = internal; - // if this is a Root internal, return its parent DOM: - if (parent.flags & TYPE_ROOT) { - return parent.props._parentDom; + if (internal.flags & TYPE_ROOT) { + return internal.props._parentDom; } // walk up the tree to find the nearest DOM or Root Internal: - while ((parent = parent._parent)) { - if (parent.flags & TYPE_ROOT) { - return parent.props._parentDom; - } else if (parent.flags & TYPE_ELEMENT) { - return parent.data; + while ((internal = internal._parent)) { + if (internal.flags & TYPE_ELEMENT) { + return internal.data; + } + if (internal.flags & TYPE_ROOT) { + return internal.props._parentDom; } } } diff --git a/test/browser/render.test.js b/test/browser/render.test.js index 9159410654..79d4e18594 100644 --- a/test/browser/render.test.js +++ b/test/browser/render.test.js @@ -26,7 +26,7 @@ function getAttributes(node) { const isIE11 = /Trident\//.test(navigator.userAgent); -describe('render()', () => { +describe.only('render()', () => { let scratch, rerender; let resetAppendChild; From dda7076078c5ce1592f7604c4a6ee89849796a7a Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 2 Jan 2023 12:03:22 -0800 Subject: [PATCH 02/71] Implement basic patchChildren using linked list and skewed old head. - Removed vnode arg to mount - Fixed up mounting with linked list - Use _index property on internal to track null holes in children Co-authored-by: Jovi De Croock Co-authored-by: Jason Miller Co-authored-by: Marvin Hagemeister --- package.json | 38 +++++------ src/create-root.js | 2 +- src/diff/children.js | 130 ++++++++++++++++++++++++++++++++++-- src/diff/mount.js | 64 ++++++++---------- src/diff/patch.js | 7 +- src/internal.d.ts | 2 + src/render.js | 4 +- src/tree.js | 1 + test/browser/keys.test.js | 26 ++++++++ test/browser/render.test.js | 2 +- 10 files changed, 209 insertions(+), 67 deletions(-) diff --git a/package.json b/package.json index 0488234160..25ffa353bb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "preact", "amdName": "preact", - "version": "11.0.0-beta", + "version": "11.0.0-alpha", "private": false, "description": "Fast 3kb React-compatible Virtual DOM library.", "main": "dist/preact.js", @@ -21,65 +21,65 @@ "Samsung>=8.2", "not dead" ], - "sideEffects": [ - "./compat/**/*", - "./debug/**/*", - "./devtools/**/*", - "./hooks/**/*", - "./test-utils/**/*" - ], + "sideEffects": [ + "./compat/**/*", + "./debug/**/*", + "./devtools/**/*", + "./hooks/**/*", + "./test-utils/**/*" + ], "exports": { ".": { - "types": "./src/index.d.ts", + "types": "./src/index.d.ts", "module": "./dist/preact.mjs", "import": "./dist/preact.mjs", "require": "./dist/preact.js", "umd": "./dist/preact.umd.js" }, "./compat": { - "types": "./compat/src/index.d.ts", + "types": "./compat/src/index.d.ts", "module": "./compat/dist/compat.mjs", "import": "./compat/dist/compat.mjs", "require": "./compat/dist/compat.js", "umd": "./compat/dist/compat.umd.js" }, "./debug": { - "types": "./debug/src/index.d.ts", + "types": "./debug/src/index.d.ts", "module": "./debug/dist/debug.mjs", "import": "./debug/dist/debug.mjs", "require": "./debug/dist/debug.js", "umd": "./debug/dist/debug.umd.js" }, "./devtools": { - "types": "./devtools/src/index.d.ts", + "types": "./devtools/src/index.d.ts", "module": "./devtools/dist/devtools.mjs", "import": "./devtools/dist/devtools.mjs", "require": "./devtools/dist/devtools.js", "umd": "./devtools/dist/devtools.umd.js" }, "./hooks": { - "types": "./hooks/src/index.d.ts", + "types": "./hooks/src/index.d.ts", "module": "./hooks/dist/hooks.mjs", "import": "./hooks/dist/hooks.mjs", "require": "./hooks/dist/hooks.js", "umd": "./hooks/dist/hooks.umd.js" }, "./test-utils": { - "types": "./test-utils/src/index.d.ts", + "types": "./test-utils/src/index.d.ts", "module": "./test-utils/dist/testUtils.mjs", "import": "./test-utils/dist/testUtils.mjs", "require": "./test-utils/dist/testUtils.js", "umd": "./test-utils/dist/testUtils.umd.js" }, "./jsx-runtime": { - "types": "./jsx-runtime/src/index.d.ts", + "types": "./jsx-runtime/src/index.d.ts", "module": "./jsx-runtime/dist/jsxRuntime.mjs", "import": "./jsx-runtime/dist/jsxRuntime.mjs", "require": "./jsx-runtime/dist/jsxRuntime.js", "umd": "./jsx-runtime/dist/jsxRuntime.umd.js" }, "./jsx-dev-runtime": { - "types": "./jsx-runtime/src/index.d.ts", + "types": "./jsx-runtime/src/index.d.ts", "module": "./jsx-runtime/dist/jsxRuntime.mjs", "import": "./jsx-runtime/dist/jsxRuntime.mjs", "require": "./jsx-runtime/dist/jsxRuntime.js", @@ -91,12 +91,12 @@ "require": "./server/dist/server.js", "umd": "./server/dist/server.umd.js" }, - "./compat/client": { + "./compat/client": { "import": "./compat/client.mjs", "require": "./compat/client.js" }, "./compat/server": { - "browser": "./compat/server.browser.js", + "browser": "./compat/server.browser.js", "module": "./compat/server.mjs", "import": "./compat/server.mjs", "require": "./compat/server.js" @@ -219,7 +219,7 @@ "compat/server.js", "compat/server.browser.js", "compat/server.mjs", - "compat/client.js", + "compat/client.js", "compat/client.mjs", "compat/test-utils.js", "compat/jsx-runtime.js", diff --git a/src/create-root.js b/src/create-root.js index a3b38e605f..c40327efbe 100644 --- a/src/create-root.js +++ b/src/create-root.js @@ -49,7 +49,7 @@ export function createRoot(parentDom) { rootInternal._context = {}; - mount(rootInternal, vnode, parentDom, firstChild); + mount(rootInternal, parentDom, firstChild); } // Flush all queued effects diff --git a/src/diff/children.js b/src/diff/children.js index 5682985007..ab5f488c9d 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -2,23 +2,139 @@ import { applyRef } from './refs'; import { normalizeToVNode } from '../create-element'; import { TYPE_COMPONENT, + TYPE_TEXT, MODE_HYDRATE, MODE_SUSPENDED, EMPTY_ARR, TYPE_DOM, - UNDEFINED + UNDEFINED, + TYPE_ELEMENT } from '../constants'; import { mount } from './mount'; import { patch } from './patch'; import { unmount } from './unmount'; import { createInternal, getDomSibling } from '../tree'; +/** + * Scenarios: + * + * 1. Unchanged: no ordering changes, walk new+old children and update Internals in-place + * 2. All removed: walk old child Internals and unmount + * 3. All added: walk over new child vnodes and create Internals, assign `.next`, mount + */ + +/** @typedef {import('../internal').Internal} Internal */ +/** @typedef {import('../internal').VNode} VNode */ + /** * Update an internal with new children. - * @param {import('../internal').Internal} internal The internal whose children should be patched + * @param {Internal} parentInternal The internal whose children should be patched * @param {import('../internal').ComponentChild[]} children The new children, represented as VNodes * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered */ +export function patchChildren(parentInternal, children, parentDom) { + /** @type {Internal} */ + let newTail; + let oldHead = parentInternal._child; + let skewedOldHead = oldHead; + + for (let index = 0; index < children.length; index++) { + const vnode = children[index]; + + // holes get accounted for in the index property: + if (vnode == null || vnode === true || vnode === false) continue; + + let type = null; + let typeFlag = 0; + let key; + let normalizedVNode = /** @type {VNode | string} */ (vnode); + + // text VNodes (strings, numbers, bigints, etc): + if (typeof vnode !== 'object') { + typeFlag = TYPE_TEXT; + normalizedVNode += ''; + } else { + type = vnode.type; + typeFlag = typeof type === 'function' ? TYPE_COMPONENT : TYPE_ELEMENT; + key = vnode.key; + } + + /** @type {Internal?} */ + let internal; + + // seek forward through the unsorted Internals list, starting at the skewed head. + // if we reach the end of the list, wrap around until we hit the skewed head again. + let match = skewedOldHead; + + /** @type {Internal?} */ + while (match) { + const flags = match.flags; + const next = match._next; + + if ((flags & typeFlag) !== 0 && match.type === type && match.key == key) { + internal = match; + + // if we are at the old head, bump it forward and reset skew: + if (match === oldHead) oldHead = skewedOldHead = next; + // otherwise just bump the skewed head: + else skewedOldHead = next || oldHead; + + break; + } + + // we've visited all candidates, bail out (no match): + if (next === skewedOldHead) break; + // advance forward one or wrap around: + match = next || oldHead; + } + + // no match, create a new Internal: + if (!internal) { + internal = createInternal(normalizedVNode, parentInternal); + } + + // move into place in new list + if (newTail) newTail._next = internal; + else parentInternal._child = internal; + newTail = internal; + } + + let childInternal = parentInternal._child; + // walk over the now sorted Internal children and insert/mount/update + for (let index = 0; index < children.length; index++) { + const vnode = children[index]; + + // account for holes by incrementing the index: + if (vnode == null || vnode === true || vnode === false) continue; + + let prevIndex = childInternal._index; + childInternal._index = index; + if (prevIndex === -1) { + // insert + mount(childInternal, parentDom, getDomSibling(childInternal)); + } else { + // update (or move+update) + patch(childInternal, vnode, parentDom); + if (prevIndex !== index) { + // move + insertComponentDom( + childInternal, + getDomSibling(childInternal), + parentDom + ); + } + } + + childInternal = childInternal._next; + } + + // walk over the unused children and unmount: + while (oldHead) { + unmount(oldHead, parentInternal, 0); + oldHead = oldHead._next; + } +} +/* export function patchChildren(internal, children, parentDom) { // let oldChildren = // (internal._children && internal._children.slice()) || EMPTY_ARR; @@ -26,16 +142,17 @@ export function patchChildren(internal, children, parentDom) { // let oldChildrenLength = oldChildren.length; // let remainingOldChildren = oldChildrenLength; + let skew = 0; let i; - /** @type {import('../internal').Internal} */ + /** @type {import('../internal').Internal} *\/ let childInternal; - /** @type {import('../internal').ComponentChild} */ + /** @type {import('../internal').ComponentChild} *\/ let childVNode; - /** @type {import('../internal').Internal[]} */ + /** @type {import('../internal').Internal[]} *\/ const newChildren = []; for (i = 0; i < children.length; i++) { @@ -170,6 +287,7 @@ export function patchChildren(internal, children, parentDom) { } } } +*/ /** * @param {import('../internal').VNode | string} childVNode @@ -178,6 +296,7 @@ export function patchChildren(internal, children, parentDom) { * @param {number} remainingOldChildren * @returns {number} */ +/* function findMatchingIndex( childVNode, oldChildren, @@ -225,6 +344,7 @@ function findMatchingIndex( return match; } +*/ /** * @param {import('../internal').Internal} internal diff --git a/src/diff/mount.js b/src/diff/mount.js index fceef8d00e..34571c7f40 100644 --- a/src/diff/mount.js +++ b/src/diff/mount.js @@ -22,13 +22,12 @@ import { commitQueue } from './commit'; /** * Diff two virtual nodes and apply proper changes to the DOM * @param {import('../internal').Internal} internal The Internal node to mount - * @param {import('../internal').VNode | string} newVNode The new virtual node * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered * @param {import('../internal').PreactNode} startDom * @returns {import('../internal').PreactNode | null} pointer to the next DOM node to be hydrated (or null) */ -export function mount(internal, newVNode, parentDom, startDom) { - if (options._diff) options._diff(internal, newVNode); +export function mount(internal, parentDom, startDom) { + if (options._diff) options._diff(internal, null); /** @type {import('../internal').PreactNode} */ let nextDomSibling, prevStartDom; @@ -40,9 +39,9 @@ export function mount(internal, newVNode, parentDom, startDom) { // top. if ( internal.flags & TYPE_ROOT && - newVNode.props._parentDom !== parentDom + internal.props._parentDom !== parentDom ) { - parentDom = newVNode.props._parentDom; + parentDom = internal.props._parentDom; prevStartDom = startDom; startDom = null; } @@ -219,44 +218,41 @@ function mountElement(internal, dom) { /** * Mount all children of an Internal - * @param {import('../internal').Internal} internal The parent Internal of the given children + * @param {import('../internal').Internal} parentInternal The parent Internal of the given children * @param {import('../internal').ComponentChild[]} children * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered * @param {import('../internal').PreactNode} startDom */ -export function mountChildren(internal, children, parentDom, startDom) { - // let internalChildren = (internal._children = []), +export function mountChildren(parentInternal, children, parentDom, startDom) { let i, /** @type {import('../internal').Internal} */ - prevChildInternal, + prevInternal, /** @type {import('../internal').Internal} */ - childInternal, - childVNode, + internal, + vnode, newDom, mountedNextChild; for (i = 0; i < children.length; i++) { - childVNode = normalizeToVNode(children[i]); - - // Terser removes the `continue` here and wraps the loop body - // in a `if (childVNode) { ... } condition - if (childVNode == null) { - // @TODO - account for holes? - // internalChildren[i] = null; - continue; - } + vnode = children[i]; + + // account for holes by incrementing the index: + if (vnode == null || vnode === true || vnode === false) continue; + + const normalizedVNode = typeof vnode === 'object' ? vnode : String(vnode); - childInternal = createInternal(childVNode, internal); - // internalChildren[i] = childInternal; - if (prevChildInternal) prevChildInternal._next = childInternal; - else internal._child = childInternal; + internal = createInternal(normalizedVNode, parentInternal); + internal._index = i; + + if (prevInternal) prevInternal._next = internal; + else parentInternal._child = internal; // Morph the old element into the new one, but don't append it to the dom yet - mountedNextChild = mount(childInternal, childVNode, parentDom, startDom); + mountedNextChild = mount(internal, parentDom, startDom); - newDom = childInternal.data; + newDom = internal.data; - if (childInternal.flags & TYPE_COMPONENT || newDom == startDom) { + if (internal.flags & TYPE_COMPONENT || newDom == startDom) { // If the child is a Fragment-like or if it is DOM VNode and its _dom // property matches the dom we are diffing (i.e. startDom), just // continue with the mountedNextChild @@ -268,21 +264,17 @@ export function mountChildren(internal, children, parentDom, startDom) { parentDom.insertBefore(newDom, startDom); } - if (childInternal.ref) { - applyRef( - childInternal.ref, - childInternal._component || newDom, - childInternal - ); + if (internal.ref) { + applyRef(internal.ref, internal._component || newDom, internal); } - prevChildInternal = childInternal; + prevInternal = internal; } // Remove children that are not part of any vnode. if ( - internal.flags & (MODE_HYDRATE | MODE_MUTATIVE_HYDRATE) && - internal.flags & TYPE_ELEMENT + parentInternal.flags & (MODE_HYDRATE | MODE_MUTATIVE_HYDRATE) && + parentInternal.flags & TYPE_ELEMENT ) { // TODO: Would it be simpler to just clear the pre-existing DOM in top-level // render if render is called with no oldVNode & existing children & no diff --git a/src/diff/patch.js b/src/diff/patch.js index 0dcec97e5c..975b4a4023 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -27,7 +27,7 @@ import { commitQueue } from './commit'; /** * Diff two virtual nodes and apply proper changes to the DOM * @param {import('../internal').Internal} internal The Internal node to patch - * @param {import('../internal').VNode | string} vnode The new virtual node + * @param {import('../internal').ComponentChild} vnode The new virtual node * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered */ export function patch(internal, vnode, parentDom) { @@ -90,7 +90,7 @@ export function patch(internal, vnode, parentDom) { if (vnode._vnodeId !== internal._vnodeId) { internal.flags &= ~DIRTY_BIT; } - } else if (internal._children == null) { + } else if (internal._child == null) { let siblingDom = (internal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) === (MODE_HYDRATE | MODE_SUSPENDED) @@ -180,7 +180,8 @@ function patchElement(internal, vnode) { if (!oldHtml || (value !== oldHtml.__html && value !== dom.innerHTML)) { dom.innerHTML = value; } - internal._children = null; + // @TODO - unmount?? + internal._child = null; } else { if (oldHtml) dom.innerHTML = ''; patchChildren( diff --git a/src/internal.d.ts b/src/internal.d.ts index 3ec800b525..e632610ecd 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -151,6 +151,8 @@ export interface Internal

{ _child: Internal | null; /** next sibling Internal node */ _next: Internal | null; + /** offset within parent's children, accounting for holes */ + _index: number; /** most recent vnode ID */ _vnodeId: number; /** diff --git a/src/render.js b/src/render.js index af50960452..21323efcdd 100644 --- a/src/render.js +++ b/src/render.js @@ -11,8 +11,8 @@ export function render(vnode, parentDom) { if (!root) { root = createRoot(parentDom); } - root.render(vnode); parentDom._root = root; + root.render(vnode); } /** @@ -26,6 +26,6 @@ export function hydrate(vnode, parentDom) { if (!root) { root = createRoot(parentDom); } - root.hydrate(vnode); parentDom._root = root; + root.hydrate(vnode); } diff --git a/src/tree.js b/src/tree.js index dda7e022dc..89be5d8c50 100644 --- a/src/tree.js +++ b/src/tree.js @@ -96,6 +96,7 @@ export function createInternal(vnode, parentInternal) { _parent: parentInternal, _child: null, _next: null, + _index: -1, _vnodeId: vnodeId, _component: null, _context: null, diff --git a/test/browser/keys.test.js b/test/browser/keys.test.js index 34339d1bc6..e41c2d3d66 100644 --- a/test/browser/keys.test.js +++ b/test/browser/keys.test.js @@ -92,6 +92,32 @@ describe('keys', () => { expect(Foo.args[0][0]).to.deep.equal({}); }); + it.only('should update in-place keyed DOM nodes', () => { + render( +

    +
  • a
  • +
  • b
  • +
  • c
  • +
, + scratch + ); + expect(scratch.innerHTML).to.equal( + '
  • a
  • b
  • c
' + ); + + render( +
    +
  • x
  • +
  • y
  • +
  • z
  • +
, + scratch + ); + expect(scratch.innerHTML).to.equal( + '
  • x
  • y
  • z
' + ); + }); + // See preactjs/preact-compat#21 it('should remove orphaned keyed nodes', () => { render( diff --git a/test/browser/render.test.js b/test/browser/render.test.js index 79d4e18594..9159410654 100644 --- a/test/browser/render.test.js +++ b/test/browser/render.test.js @@ -26,7 +26,7 @@ function getAttributes(node) { const isIE11 = /Trident\//.test(navigator.userAgent); -describe.only('render()', () => { +describe('render()', () => { let scratch, rerender; let resetAppendChild; From 5e6cb225c8869ad7d0648e83c03797a28450506b Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 2 Jan 2023 15:06:07 -0800 Subject: [PATCH 03/71] WIP: Switch insertion loop to loop backwards through internal children After running into problems with the 'should move multiple keyed children to the beginning' test with the previous commit, we are switching our insertion loop to iterate backwards through the internal children to do insertions. In summary: First loop: - find matching internals (using if _prev pointer is set to determine if an internal has been used) - setup _prev pointers to be correct Second loop: - setup _next pointers to be correct - insert or mount dom children (detect if a node was moved by comparing if internal._prev._next == internal) There are still bugs though but above is the general idea we are going for Co-authored-by: Jason Miller Co-authored-by: Marvin Hagemeister --- src/diff/children.js | 176 +++++++++++++++++++++++++++++++------- src/diff/mount.js | 1 - src/diff/patch.js | 9 +- src/internal.d.ts | 4 +- src/tree.js | 2 +- test/browser/keys.test.js | 40 ++++++++- 6 files changed, 189 insertions(+), 43 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index ab5f488c9d..4216fe4051 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -13,7 +13,7 @@ import { import { mount } from './mount'; import { patch } from './patch'; import { unmount } from './unmount'; -import { createInternal, getDomSibling } from '../tree'; +import { createInternal, getChildDom, getDomSibling } from '../tree'; /** * Scenarios: @@ -34,9 +34,10 @@ import { createInternal, getDomSibling } from '../tree'; */ export function patchChildren(parentInternal, children, parentDom) { /** @type {Internal} */ - let newTail; + let prevInternal; + // let newTail; let oldHead = parentInternal._child; - let skewedOldHead = oldHead; + // let skewedOldHead = oldHead; for (let index = 0; index < children.length; index++) { const vnode = children[index]; @@ -62,44 +63,122 @@ export function patchChildren(parentInternal, children, parentDom) { /** @type {Internal?} */ let internal; + /* // seek forward through the unsorted Internals list, starting at the skewed head. // if we reach the end of the list, wrap around until we hit the skewed head again. let match = skewedOldHead; + /** @type {Internal?} *\/ + let prevMatch; - /** @type {Internal?} */ while (match) { const flags = match.flags; - const next = match._next; + // next Internal (wrapping around to start): + let next = match._next || oldHead; + if (next === match) next = undefined; if ((flags & typeFlag) !== 0 && match.type === type && match.key == key) { internal = match; + // update ptr from prev item to remove the match, but don't create a circular tail: + if (prevMatch) prevMatch._next = match._next; + else skewedOldHead = next; + // if we are at the old head, bump it forward and reset skew: - if (match === oldHead) oldHead = skewedOldHead = next; + // if (match === oldHead) oldHead = skewedOldHead = next; + if (match === oldHead) oldHead = match._next; // otherwise just bump the skewed head: - else skewedOldHead = next || oldHead; + // else skewedOldHead = next; break; } // we've visited all candidates, bail out (no match): if (next === skewedOldHead) break; + // advance forward one or wrap around: - match = next || oldHead; + prevMatch = match; + match = next; + } + */ + + // seek forward through the Internals list, starting at the head (either first, or first unused). + // only match unused items, which are internals where _prev === undefined. + // note: _prev=null for the first matched internal, and should be considered "used". + let match = oldHead; + while (match) { + const flags = match.flags; + const isUnused = match !== prevInternal && match._prev == null; + if ( + isUnused && + (flags & typeFlag) !== 0 && + match.type === type && + match.key == key + ) { + internal = match; + // if the match was the first unused item, bump the start ptr forward: + if (match === oldHead) oldHead = oldHead._next; + break; + } + match = match._next; } // no match, create a new Internal: if (!internal) { internal = createInternal(normalizedVNode, parentInternal); + } else { + // patch(internal, vnode, parentDom); } // move into place in new list - if (newTail) newTail._next = internal; - else parentInternal._child = internal; - newTail = internal; + // if (newTail) newTail._next = internal; + // else parentInternal._child = internal; + // newTail = internal; + internal._prev = prevInternal || parentInternal; + prevInternal = internal; + } + + // next, we walk backwards over the newly-assigned _prev properties, + // visiting each Internal to set its _next ptr and perform insert/mount/update. + let internal = prevInternal; + /** @type {Internal} */ + let nextInternal; + + let index = children.length; + while (internal) { + let next = internal._next; + + // set this internal's next ptr to the previous loop entry + internal._next = nextInternal; + nextInternal = internal; + + index--; + + if (!next) { + mount(internal, parentDom, getDomSibling(internal)); + insert(internal, parentDom); + } else { + const vnode = children[index]; + patch(internal, vnode, parentDom); + // If the previous Internal doesn't point back to us, it means we were moved. + // if (next._prev !== internal) { + if (internal._prev._next !== internal) { + // move + insert(internal, parentDom, getDomSibling(internal)); + } + } + + // for now, we're only using double-links internally to this function: + prevInternal = internal._prev; + if (prevInternal === parentInternal) prevInternal = undefined; + internal._prev = null; + internal = prevInternal; } + parentInternal._child = internal; + + /* let childInternal = parentInternal._child; + let skew = 0; // walk over the now sorted Internal children and insert/mount/update for (let index = 0; index < children.length; index++) { const vnode = children[index]; @@ -107,31 +186,66 @@ export function patchChildren(parentInternal, children, parentDom) { // account for holes by incrementing the index: if (vnode == null || vnode === true || vnode === false) continue; - let prevIndex = childInternal._index; + let prevIndex = childInternal._index + skew; + childInternal._index = index; if (prevIndex === -1) { + console.log('mounting <' + childInternal.type + '> at index ' + index); // insert mount(childInternal, parentDom, getDomSibling(childInternal)); + // insert(childInternal, getDomSibling(childInternal), parentDom); + insert(childInternal, parentDom); + skew++; } else { // update (or move+update) patch(childInternal, vnode, parentDom); + // if (prevIndex > index) { + // skew--; + // } + + let nextDomSibling; + let siblingSkew = skew; + let sibling = childInternal._next; + let siblingIndex = index; + while (sibling) { + let prevSiblingIndex = sibling._index + siblingSkew; + siblingIndex++; + // if this item is in-place: + if (prevSiblingIndex === siblingIndex) { + nextDomSibling = getChildDom(sibling); + break; + } + sibling = sibling._next; + } + // while (sibling && (sibling._index + siblingSkew) !== ++siblingIndex) { + // siblingSkew++; + // } + + // if (prevIndex < index) { if (prevIndex !== index) { - // move - insertComponentDom( - childInternal, - getDomSibling(childInternal), - parentDom + skew++; + // skew = prevIndex - index; + console.log( + '<' + childInternal.type + '> index changed from ', + prevIndex, + 'to', + index, + childInternal.data.textContent ); + // move + insert(childInternal, parentDom, nextDomSibling); } } childInternal = childInternal._next; } + */ // walk over the unused children and unmount: while (oldHead) { + const next = oldHead._next; unmount(oldHead, parentInternal, 0); - oldHead = oldHead._next; + oldHead = next; } } /* @@ -348,25 +462,23 @@ function findMatchingIndex( /** * @param {import('../internal').Internal} internal - * @param {import('../internal').PreactNode} nextSibling * @param {import('../internal').PreactNode} parentDom + * @param {import('../internal').PreactNode} [nextSibling] */ -export function insertComponentDom(internal, nextSibling, parentDom) { - if (internal._children == null) { - return; +export function insert(internal, parentDom, nextSibling) { + if (nextSibling === undefined) { + nextSibling = getDomSibling(internal); } - for (let i = 0; i < internal._children.length; i++) { - let childInternal = internal._children[i]; - if (childInternal) { - childInternal._parent = internal; - - if (childInternal.flags & TYPE_COMPONENT) { - insertComponentDom(childInternal, nextSibling, parentDom); - } else if (childInternal.data != nextSibling) { - parentDom.insertBefore(childInternal.data, nextSibling); - } + if (internal.flags & TYPE_COMPONENT) { + let child = internal._child; + while (child) { + insert(child, parentDom, nextSibling); + child = child._next; } + } else if (internal.data != nextSibling) { + // @ts-ignore .data is a Node + parentDom.insertBefore(internal.data, nextSibling); } } diff --git a/src/diff/mount.js b/src/diff/mount.js index 34571c7f40..67d1169c6d 100644 --- a/src/diff/mount.js +++ b/src/diff/mount.js @@ -242,7 +242,6 @@ export function mountChildren(parentInternal, children, parentDom, startDom) { const normalizedVNode = typeof vnode === 'object' ? vnode : String(vnode); internal = createInternal(normalizedVNode, parentInternal); - internal._index = i; if (prevInternal) prevInternal._next = internal; else parentInternal._child = internal; diff --git a/src/diff/patch.js b/src/diff/patch.js index 975b4a4023..df372b944b 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -1,4 +1,4 @@ -import { patchChildren, insertComponentDom } from './children'; +import { patchChildren, insert } from './children'; import { setProperty } from './props'; import options from '../options'; import { @@ -56,10 +56,13 @@ export function patch(internal, vnode, parentDom) { if (flags & TYPE_ROOT) { parentDom = vnode.props._parentDom; - if (internal.props._parentDom !== vnode.props._parentDom) { + if (internal.props._parentDom !== parentDom) { + // let nextSibling = + // parentDom == prevParentDom ? getDomSibling(internal) : null; + // insert(internal, nextSibling, parentDom); let nextSibling = parentDom == prevParentDom ? getDomSibling(internal) : null; - insertComponentDom(internal, nextSibling, parentDom); + insert(internal, parentDom, nextSibling); } } diff --git a/src/internal.d.ts b/src/internal.d.ts index e632610ecd..df8cd45465 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -151,8 +151,8 @@ export interface Internal

{ _child: Internal | null; /** next sibling Internal node */ _next: Internal | null; - /** offset within parent's children, accounting for holes */ - _index: number; + /** previous sibling Internal */ + _prev: Internal | null; /** most recent vnode ID */ _vnodeId: number; /** diff --git a/src/tree.js b/src/tree.js index 89be5d8c50..f05bceafbe 100644 --- a/src/tree.js +++ b/src/tree.js @@ -96,7 +96,7 @@ export function createInternal(vnode, parentInternal) { _parent: parentInternal, _child: null, _next: null, - _index: -1, + _prev: null, _vnodeId: vnodeId, _component: null, _context: null, diff --git a/test/browser/keys.test.js b/test/browser/keys.test.js index e41c2d3d66..8eda86515a 100644 --- a/test/browser/keys.test.js +++ b/test/browser/keys.test.js @@ -92,7 +92,7 @@ describe('keys', () => { expect(Foo.args[0][0]).to.deep.equal({}); }); - it.only('should update in-place keyed DOM nodes', () => { + it('should update in-place keyed DOM nodes', () => { render(

  • a
  • @@ -189,7 +189,7 @@ describe('keys', () => { ); }); - it('should append new keyed elements', () => { + it.only('should append new keyed elements', () => { const values = ['a', 'b']; render(, scratch); @@ -280,9 +280,41 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('abcd'); expect(getLog()).to.deep.equal([ - '
  • z.remove()', + '
  • x.remove()', '
  • y.remove()', - '
  • x.remove()' + '
  • z.remove()' + ]); + }); + + it('should move keyed children to the beginning', () => { + const values = ['b', 'c', 'd', 'a']; + + render(, scratch); + expect(scratch.textContent).to.equal('bcda'); + + move(values, values.length - 1, 0); + clearLog(); + + render(, scratch); + expect(scratch.textContent).to.equal('abcd'); + expect(getLog()).to.deep.equal(['
      bcda.insertBefore(
    1. a,
    2. b)']); + }); + + it('should move multiple keyed children to the beginning', () => { + const values = ['c', 'd', 'e', 'a', 'b']; + + render(, scratch); + expect(scratch.textContent).to.equal('cdeab'); + + move(values, values.length - 1, 0); + move(values, values.length - 1, 0); + clearLog(); + + render(, scratch); + expect(scratch.textContent).to.equal('abcde'); + expect(getLog()).to.deep.equal([ + '
        cdeab.insertBefore(
      1. a,
      2. c)', + '
          acdeb.insertBefore(
        1. b,
        2. c)' ]); }); From 27d57d07eab299e6d222670042264c200b6ec567 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 2 Jan 2023 17:54:03 -0800 Subject: [PATCH 04/71] WIP: Unmount before inserting and fix up insertion loop Unmount before inserting means we can use _prev == null to detect internals that need to be unmounted and removes unmounted nodes from the internal children linked list when doing inserts (so all _prev and _next pointers point to internals that will be included) Use `.data` to detect if a component has been mounted or not. Fix up next pointers when mounting or moving internals Co-authored-by: Jason Miller --- src/diff/children.js | 71 ++++++++++++++++++++++++++++++++------- src/internal.d.ts | 2 +- test/browser/keys.test.js | 28 +++++++-------- 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 4216fe4051..221fd1c114 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -107,7 +107,7 @@ export function patchChildren(parentInternal, children, parentDom) { let match = oldHead; while (match) { const flags = match.flags; - const isUnused = match !== prevInternal && match._prev == null; + const isUnused = match._prev == null; if ( isUnused && (flags & typeFlag) !== 0 && @@ -125,7 +125,9 @@ export function patchChildren(parentInternal, children, parentDom) { // no match, create a new Internal: if (!internal) { internal = createInternal(normalizedVNode, parentInternal); + console.log('creating new', internal.type); } else { + console.log('updating', internal.type); // patch(internal, vnode, parentDom); } @@ -137,11 +139,26 @@ export function patchChildren(parentInternal, children, parentDom) { prevInternal = internal; } + // walk over the unused children and unmount: + let lastMatchedInternal; + oldHead = parentInternal._child; + while (oldHead) { + const next = oldHead._next; + if (oldHead._prev == null) { + if (lastMatchedInternal) lastMatchedInternal._next = next; + // else parentInternal._child = next; + unmount(oldHead, parentInternal, 0); + } else { + lastMatchedInternal = oldHead; + } + oldHead = next; + } + // next, we walk backwards over the newly-assigned _prev properties, // visiting each Internal to set its _next ptr and perform insert/mount/update. let internal = prevInternal; /** @type {Internal} */ - let nextInternal; + let nextInternal = null; let index = children.length; while (internal) { @@ -153,23 +170,51 @@ export function patchChildren(parentInternal, children, parentDom) { index--; - if (!next) { + prevInternal = internal._prev; + if (prevInternal === parentInternal) prevInternal = undefined; + + // if (next === null) { + if (internal.data == null) { + console.log('mount', internal.type); mount(internal, parentDom, getDomSibling(internal)); insert(internal, parentDom); + prevInternal._next = internal; + // prevInternal._next = null; } else { const vnode = children[index]; patch(internal, vnode, parentDom); // If the previous Internal doesn't point back to us, it means we were moved. - // if (next._prev !== internal) { - if (internal._prev._next !== internal) { + // if (prevInternal._next !== internal) { + if (internal._next !== next && internal._next) { // move + console.log('move', internal.type, internal.data.textContent); + console.log( + ' > expected _next:', + internal._next && internal._next.type, + internal._next && internal._next.props + ); + console.log(' > _next:', next && next.type, next && next.props); insert(internal, parentDom, getDomSibling(internal)); + // we moved this node, so unset its previous sibling's next pointer + // note: this is like doing a splice() out of oldChildren + internal._prev._next = next; // or set to null? + // prevInternal._next = internal; + // internal._prev._next = internal; + } else { + console.log('update', internal.type, internal.data.textContent); + console.log( + ' > expected _next:', + internal._next && internal._next.type, + internal._next && internal._next.props + ); + console.log(' > _next:', next && next.type, next && next.props); } } + // if (prevInternal) prevInternal._next = internal; + // else parentInternal._child = internal; + // for now, we're only using double-links internally to this function: - prevInternal = internal._prev; - if (prevInternal === parentInternal) prevInternal = undefined; internal._prev = null; internal = prevInternal; } @@ -241,12 +286,12 @@ export function patchChildren(parentInternal, children, parentDom) { } */ - // walk over the unused children and unmount: - while (oldHead) { - const next = oldHead._next; - unmount(oldHead, parentInternal, 0); - oldHead = next; - } + // // walk over the unused children and unmount: + // while (oldHead) { + // const next = oldHead._next; + // unmount(oldHead, parentInternal, 0); + // oldHead = next; + // } } /* export function patchChildren(internal, children, parentDom) { diff --git a/src/internal.d.ts b/src/internal.d.ts index df8cd45465..b580ac7754 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -151,7 +151,7 @@ export interface Internal

          { _child: Internal | null; /** next sibling Internal node */ _next: Internal | null; - /** previous sibling Internal */ + /** temporarily holds previous sibling Internal while diffing. Is purposefully cleared after diffing due to how the diffing algorithm works */ _prev: Internal | null; /** most recent vnode ID */ _vnodeId: number; diff --git a/test/browser/keys.test.js b/test/browser/keys.test.js index 8eda86515a..2f0bead37a 100644 --- a/test/browser/keys.test.js +++ b/test/browser/keys.test.js @@ -6,7 +6,7 @@ import { div } from '../_util/dom'; /** @jsx createElement */ -describe('keys', () => { +describe.only('keys', () => { /** @type {HTMLDivElement} */ let scratch; @@ -92,7 +92,7 @@ describe('keys', () => { expect(Foo.args[0][0]).to.deep.equal({}); }); - it('should update in-place keyed DOM nodes', () => { + it.only('should update in-place keyed DOM nodes', () => { render(

          • a
          • @@ -119,7 +119,7 @@ describe('keys', () => { }); // See preactjs/preact-compat#21 - it('should remove orphaned keyed nodes', () => { + it.only('should remove orphaned keyed nodes', () => { render(
            1
            @@ -206,7 +206,7 @@ describe('keys', () => { ]); }); - it('should remove keyed elements from the end', () => { + it.only('should remove keyed elements from the end', () => { const values = ['a', 'b', 'c', 'd']; render(, scratch); @@ -237,7 +237,7 @@ describe('keys', () => { ]); }); - it('should remove keyed elements from the beginning', () => { + it.only('should remove keyed elements from the beginning', () => { const values = ['z', 'a', 'b', 'c']; render(, scratch); @@ -251,7 +251,7 @@ describe('keys', () => { expect(getLog()).to.deep.equal(['
          • z.remove()']); }); - it('should insert new keyed children in the middle', () => { + it.only('should insert new keyed children in the middle', () => { const values = ['a', 'c']; render(, scratch); @@ -268,7 +268,7 @@ describe('keys', () => { ]); }); - it('should remove keyed children from the middle', () => { + it.only('should remove keyed children from the middle', () => { const values = ['a', 'b', 'x', 'y', 'z', 'c', 'd']; render(, scratch); @@ -286,7 +286,7 @@ describe('keys', () => { ]); }); - it('should move keyed children to the beginning', () => { + it.only('should move keyed children to the beginning', () => { const values = ['b', 'c', 'd', 'a']; render(, scratch); @@ -300,7 +300,7 @@ describe('keys', () => { expect(getLog()).to.deep.equal(['
              bcda.insertBefore(
            1. a,
            2. b)']); }); - it('should move multiple keyed children to the beginning', () => { + it.only('should move multiple keyed children to the beginning', () => { const values = ['c', 'd', 'e', 'a', 'b']; render(, scratch); @@ -313,12 +313,12 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('abcde'); expect(getLog()).to.deep.equal([ - '
                cdeab.insertBefore(
              1. a,
              2. c)', - '
                  acdeb.insertBefore(
                1. b,
                2. c)' + '
                    cdeab.insertBefore(
                  1. b,
                  2. c)', + '
                      bcdea.insertBefore(
                    1. a,
                    2. b)' ]); }); - it('should swap keyed children efficiently', () => { + it.only('should swap keyed children efficiently', () => { render(, scratch); expect(scratch.textContent).to.equal('ab'); @@ -327,10 +327,10 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('ba'); - expect(getLog()).to.deep.equal(['
                        ab.insertBefore(
                      1. a, Null)']); + expect(getLog()).to.deep.equal(['
                          ab.insertBefore(
                        1. b,
                        2. a)']); }); - it('should swap existing keyed children in the middle of a list efficiently', () => { + it.only('should swap existing keyed children in the middle of a list efficiently', () => { const values = ['a', 'b', 'c', 'd']; render(, scratch); From 31a9967383324ffaef7141c12f656683af95bbeb Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 19 Jan 2023 00:04:42 -0800 Subject: [PATCH 05/71] Add two more keys test --- test/browser/keys.test.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/browser/keys.test.js b/test/browser/keys.test.js index 2f0bead37a..9bd13d9472 100644 --- a/test/browser/keys.test.js +++ b/test/browser/keys.test.js @@ -388,6 +388,35 @@ describe.only('keys', () => { ); }); + it.only('should move keyed children to the beginning on longer list', () => { + // Preact v10 worst case + const values = ['a', 'b', 'c', 'd', 'e', 'f']; + + render(, scratch); + expect(scratch.textContent).to.equal('abcdef'); + + move(values, 4, 1); + clearLog(); + + render(, scratch); + expect(scratch.textContent).to.equal('aebcdf'); + expect(getLog()).to.deep.equal(['
                            abcdef.insertBefore(
                          1. e,
                          2. b)']); + }); + + it.only('should move keyed children to the end on longer list', () => { + const values = ['a', 'b', 'c', 'd', 'e', 'f']; + + render(, scratch); + expect(scratch.textContent).to.equal('abcdef'); + + move(values, 1, values.length - 2); + clearLog(); + + render(, scratch); + expect(scratch.textContent).to.equal('acdebf'); + expect(getLog()).to.deep.equal(['
                              abcdef.insertBefore(
                            1. b,
                            2. f)']); + }); + it('should reverse keyed children effectively', () => { const values = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; From 8d138c911bb1555d10f72beeb5f55caeac0306aa Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 19 Jan 2023 00:06:12 -0800 Subject: [PATCH 06/71] Simplify keys test and focus on only test we currently care about - Comment out and skip tests we want to currently ignore - Just focus on number of ops for reverse list atm --- test/browser/keys.test.js | 54 +++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/test/browser/keys.test.js b/test/browser/keys.test.js index 9bd13d9472..629c03c289 100644 --- a/test/browser/keys.test.js +++ b/test/browser/keys.test.js @@ -92,7 +92,7 @@ describe.only('keys', () => { expect(Foo.args[0][0]).to.deep.equal({}); }); - it.only('should update in-place keyed DOM nodes', () => { + it('should update in-place keyed DOM nodes', () => { render(
                              • a
                              • @@ -119,7 +119,7 @@ describe.only('keys', () => { }); // See preactjs/preact-compat#21 - it.only('should remove orphaned keyed nodes', () => { + it('should remove orphaned keyed nodes', () => { render(
                                1
                                @@ -143,7 +143,7 @@ describe.only('keys', () => { ); }); - it('should remove keyed nodes (#232)', () => { + it.skip('should remove keyed nodes (#232)', () => { class App extends Component { componentDidMount() { setTimeout(() => this.setState({ opened: true, loading: true }), 10); @@ -189,7 +189,7 @@ describe.only('keys', () => { ); }); - it.only('should append new keyed elements', () => { + it('should append new keyed elements', () => { const values = ['a', 'b']; render(, scratch); @@ -206,7 +206,7 @@ describe.only('keys', () => { ]); }); - it.only('should remove keyed elements from the end', () => { + it('should remove keyed elements from the end', () => { const values = ['a', 'b', 'c', 'd']; render(, scratch); @@ -237,7 +237,7 @@ describe.only('keys', () => { ]); }); - it.only('should remove keyed elements from the beginning', () => { + it('should remove keyed elements from the beginning', () => { const values = ['z', 'a', 'b', 'c']; render(, scratch); @@ -251,7 +251,7 @@ describe.only('keys', () => { expect(getLog()).to.deep.equal(['
                              • z.remove()']); }); - it.only('should insert new keyed children in the middle', () => { + it('should insert new keyed children in the middle', () => { const values = ['a', 'c']; render(, scratch); @@ -268,7 +268,7 @@ describe.only('keys', () => { ]); }); - it.only('should remove keyed children from the middle', () => { + it('should remove keyed children from the middle', () => { const values = ['a', 'b', 'x', 'y', 'z', 'c', 'd']; render(, scratch); @@ -286,7 +286,7 @@ describe.only('keys', () => { ]); }); - it.only('should move keyed children to the beginning', () => { + it('should move keyed children to the beginning', () => { const values = ['b', 'c', 'd', 'a']; render(, scratch); @@ -300,7 +300,7 @@ describe.only('keys', () => { expect(getLog()).to.deep.equal(['
                                  bcda.insertBefore(
                                1. a,
                                2. b)']); }); - it.only('should move multiple keyed children to the beginning', () => { + it('should move multiple keyed children to the beginning', () => { const values = ['c', 'd', 'e', 'a', 'b']; render(, scratch); @@ -318,7 +318,7 @@ describe.only('keys', () => { ]); }); - it.only('should swap keyed children efficiently', () => { + it('should swap keyed children efficiently', () => { render(, scratch); expect(scratch.textContent).to.equal('ab'); @@ -330,7 +330,7 @@ describe.only('keys', () => { expect(getLog()).to.deep.equal(['
                                    ab.insertBefore(
                                  1. b,
                                  2. a)']); }); - it.only('should swap existing keyed children in the middle of a list efficiently', () => { + it('should swap existing keyed children in the middle of a list efficiently', () => { const values = ['a', 'b', 'c', 'd']; render(, scratch); @@ -388,7 +388,7 @@ describe.only('keys', () => { ); }); - it.only('should move keyed children to the beginning on longer list', () => { + it('should move keyed children to the beginning on longer list', () => { // Preact v10 worst case const values = ['a', 'b', 'c', 'd', 'e', 'f']; @@ -403,7 +403,7 @@ describe.only('keys', () => { expect(getLog()).to.deep.equal(['
                                      abcdef.insertBefore(
                                    1. e,
                                    2. b)']); }); - it.only('should move keyed children to the end on longer list', () => { + it('should move keyed children to the end on longer list', () => { const values = ['a', 'b', 'c', 'd', 'e', 'f']; render(, scratch); @@ -429,19 +429,22 @@ describe.only('keys', () => { render(, scratch); expect(scratch.textContent).to.equal(values.join('')); - expect(getLog()).to.deep.equal([ - '
                                        abcdefghij.insertBefore(
                                      1. j,
                                      2. a)', - '
                                          jabcdefghi.insertBefore(
                                        1. i,
                                        2. a)', - '
                                            jiabcdefgh.insertBefore(
                                          1. h,
                                          2. a)', - '
                                              jihabcdefg.insertBefore(
                                            1. g,
                                            2. a)', - '
                                                jihgabcdef.insertBefore(
                                              1. f,
                                              2. a)', - '
                                                  jihgfabcde.insertBefore(
                                                1. e,
                                                2. a)', - '
                                                    jihgfeabcd.insertBefore(
                                                  1. d,
                                                  2. a)', - '
                                                      jihgfedabc.insertBefore(
                                                    1. c,
                                                    2. a)', - '
                                                        jihgfedcab.insertBefore(
                                                      1. a, Null)' - ]); + expect(getLog()).to.have.lengthOf(9); + // expect(getLog()).to.deep.equal([ + // '
                                                          abcdefghij.insertBefore(
                                                        1. j,
                                                        2. a)', + // '
                                                            jabcdefghi.insertBefore(
                                                          1. i,
                                                          2. a)', + // '
                                                              jiabcdefgh.insertBefore(
                                                            1. h,
                                                            2. a)', + // '
                                                                jihabcdefg.insertBefore(
                                                              1. g,
                                                              2. a)', + // '
                                                                  jihgabcdef.insertBefore(
                                                                1. f,
                                                                2. a)', + // '
                                                                    jihgfabcde.insertBefore(
                                                                  1. e,
                                                                  2. a)', + // '
                                                                      jihgfeabcd.insertBefore(
                                                                    1. d,
                                                                    2. a)', + // '
                                                                        jihgfedabc.insertBefore(
                                                                      1. c,
                                                                      2. a)', + // '
                                                                          jihgfedcab.insertBefore(
                                                                        1. a, Null)' + // ]); }); + // eslint-disable-next-line jest/no-commented-out-tests + /* it("should not preserve state when a component's keys are different", () => { const Stateful = createStateful('Stateful'); @@ -841,4 +844,5 @@ describe.only('keys', () => { done(); }); }); + */ }); From 02e90464f3b30d070daaee2bed7696665a454819 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 19 Jan 2023 00:08:54 -0800 Subject: [PATCH 07/71] Fix null ref bug when mounting children at start --- src/diff/children.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diff/children.js b/src/diff/children.js index 221fd1c114..65f02ca756 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -178,7 +178,7 @@ export function patchChildren(parentInternal, children, parentDom) { console.log('mount', internal.type); mount(internal, parentDom, getDomSibling(internal)); insert(internal, parentDom); - prevInternal._next = internal; + if (prevInternal) prevInternal._next = internal; // prevInternal._next = null; } else { const vnode = children[index]; From 07b9f1b8701790159f85ee201621a9c0a66ffc4e Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 19 Jan 2023 00:27:48 -0800 Subject: [PATCH 08/71] Fix setting parent._child --- src/diff/children.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 65f02ca756..bf75d1b11b 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -214,13 +214,13 @@ export function patchChildren(parentInternal, children, parentDom) { // if (prevInternal) prevInternal._next = internal; // else parentInternal._child = internal; + if (!prevInternal) parentInternal._child = internal; + // for now, we're only using double-links internally to this function: internal._prev = null; internal = prevInternal; } - parentInternal._child = internal; - /* let childInternal = parentInternal._child; let skew = 0; From aa884e2b1f6017e5db4c897adcfe51aa39aefd50 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 19 Jan 2023 00:29:48 -0800 Subject: [PATCH 09/71] Remove some commented code --- src/diff/children.js | 114 ------------------------------------------- 1 file changed, 114 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index bf75d1b11b..8e63839b2e 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -63,44 +63,6 @@ export function patchChildren(parentInternal, children, parentDom) { /** @type {Internal?} */ let internal; - /* - // seek forward through the unsorted Internals list, starting at the skewed head. - // if we reach the end of the list, wrap around until we hit the skewed head again. - let match = skewedOldHead; - /** @type {Internal?} *\/ - let prevMatch; - - while (match) { - const flags = match.flags; - // next Internal (wrapping around to start): - let next = match._next || oldHead; - if (next === match) next = undefined; - - if ((flags & typeFlag) !== 0 && match.type === type && match.key == key) { - internal = match; - - // update ptr from prev item to remove the match, but don't create a circular tail: - if (prevMatch) prevMatch._next = match._next; - else skewedOldHead = next; - - // if we are at the old head, bump it forward and reset skew: - // if (match === oldHead) oldHead = skewedOldHead = next; - if (match === oldHead) oldHead = match._next; - // otherwise just bump the skewed head: - // else skewedOldHead = next; - - break; - } - - // we've visited all candidates, bail out (no match): - if (next === skewedOldHead) break; - - // advance forward one or wrap around: - prevMatch = match; - match = next; - } - */ - // seek forward through the Internals list, starting at the head (either first, or first unused). // only match unused items, which are internals where _prev === undefined. // note: _prev=null for the first matched internal, and should be considered "used". @@ -131,10 +93,6 @@ export function patchChildren(parentInternal, children, parentDom) { // patch(internal, vnode, parentDom); } - // move into place in new list - // if (newTail) newTail._next = internal; - // else parentInternal._child = internal; - // newTail = internal; internal._prev = prevInternal || parentInternal; prevInternal = internal; } @@ -220,78 +178,6 @@ export function patchChildren(parentInternal, children, parentDom) { internal._prev = null; internal = prevInternal; } - - /* - let childInternal = parentInternal._child; - let skew = 0; - // walk over the now sorted Internal children and insert/mount/update - for (let index = 0; index < children.length; index++) { - const vnode = children[index]; - - // account for holes by incrementing the index: - if (vnode == null || vnode === true || vnode === false) continue; - - let prevIndex = childInternal._index + skew; - - childInternal._index = index; - if (prevIndex === -1) { - console.log('mounting <' + childInternal.type + '> at index ' + index); - // insert - mount(childInternal, parentDom, getDomSibling(childInternal)); - // insert(childInternal, getDomSibling(childInternal), parentDom); - insert(childInternal, parentDom); - skew++; - } else { - // update (or move+update) - patch(childInternal, vnode, parentDom); - // if (prevIndex > index) { - // skew--; - // } - - let nextDomSibling; - let siblingSkew = skew; - let sibling = childInternal._next; - let siblingIndex = index; - while (sibling) { - let prevSiblingIndex = sibling._index + siblingSkew; - siblingIndex++; - // if this item is in-place: - if (prevSiblingIndex === siblingIndex) { - nextDomSibling = getChildDom(sibling); - break; - } - sibling = sibling._next; - } - // while (sibling && (sibling._index + siblingSkew) !== ++siblingIndex) { - // siblingSkew++; - // } - - // if (prevIndex < index) { - if (prevIndex !== index) { - skew++; - // skew = prevIndex - index; - console.log( - '<' + childInternal.type + '> index changed from ', - prevIndex, - 'to', - index, - childInternal.data.textContent - ); - // move - insert(childInternal, parentDom, nextDomSibling); - } - } - - childInternal = childInternal._next; - } - */ - - // // walk over the unused children and unmount: - // while (oldHead) { - // const next = oldHead._next; - // unmount(oldHead, parentInternal, 0); - // oldHead = next; - // } } /* export function patchChildren(internal, children, parentDom) { From e80c37a315d5f797d33b92c3955c5a164a2e1e57 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 19 Jan 2023 00:50:14 -0800 Subject: [PATCH 10/71] Use a modified LIS algorithm to determine which internals to move Here I'm using a variant of the longest increasing subsequence algorithm to determine which nodes to move. There are two key differences in my algorithm: 1. I am looping over the nodes in reverse order, so my algorithm is actually a longest decreasing subsequence (LDS) instead of longest increasing subsequence (LIS). 2. I don't use a binary search to insert new nodes into the "wip" LDS array. --- src/diff/children.js | 66 ++++++++++++++++++++++++++++++--------- src/diff/mount.js | 1 + src/internal.d.ts | 2 ++ src/tree.js | 2 ++ test/browser/keys.test.js | 2 +- 5 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 8e63839b2e..01c34875d8 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -15,6 +15,9 @@ import { patch } from './patch'; import { unmount } from './unmount'; import { createInternal, getChildDom, getDomSibling } from '../tree'; +// TODO: Use a flag? +const LDSMarker = -2; + /** * Scenarios: * @@ -39,6 +42,7 @@ export function patchChildren(parentInternal, children, parentDom) { let oldHead = parentInternal._child; // let skewedOldHead = oldHead; + // Step 1. Find matches and set up _prev pointers for (let index = 0; index < children.length; index++) { const vnode = children[index]; @@ -97,7 +101,7 @@ export function patchChildren(parentInternal, children, parentDom) { prevInternal = internal; } - // walk over the unused children and unmount: + // Step 2. Walk over the unused children and unmount: let lastMatchedInternal; oldHead = parentInternal._child; while (oldHead) { @@ -112,9 +116,49 @@ export function patchChildren(parentInternal, children, parentDom) { oldHead = next; } + // Step 3. Find the longest decreasing subsequence + let internal = prevInternal; + /** @type {Internal[]} */ + const wipLDS = [internal]; + + while ((internal = internal._prev) !== parentInternal) { + // Skip over newly mounted internals. They will be mounted in place. + if (internal._index === -1) continue; + + let ldsTail = wipLDS[wipLDS.length - 1]; + if (ldsTail._index > internal._index) { + internal._prevLDS = ldsTail; + wipLDS.push(internal); + } else { + // Search for position in wipLIS where node should go. It should replace + // the first node where node > wip[i] (though keep in mind, we are + // iterating over the list backwards). Example: + // ``` + // wipLIS = [4,3,1], node = 2. + // Node should replace 1: [4,3,2] + // ``` + let i = wipLDS.length; + // TODO: Binary search? + while (--i >= 0 && wipLDS[i]._index < internal._index) {} + + wipLDS[i + 1] = internal; + let prevLDS = i < 0 ? null : wipLDS[i]; + internal._prevLDS = prevLDS; + } + } + + // Step 4. Mark internals in longest decreasing subsequence + /** @type {Internal | null} */ + let ldsNode = wipLDS[wipLDS.length - 1]; + while (ldsNode) { + // Mark node as being in the longest increasing subsequence (_index = -2) + ldsNode._index = LDSMarker; + ldsNode = ldsNode._prevLDS; + } + // next, we walk backwards over the newly-assigned _prev properties, // visiting each Internal to set its _next ptr and perform insert/mount/update. - let internal = prevInternal; + internal = prevInternal; /** @type {Internal} */ let nextInternal = null; @@ -131,19 +175,17 @@ export function patchChildren(parentInternal, children, parentDom) { prevInternal = internal._prev; if (prevInternal === parentInternal) prevInternal = undefined; - // if (next === null) { - if (internal.data == null) { + if (internal._index === -1) { console.log('mount', internal.type); mount(internal, parentDom, getDomSibling(internal)); insert(internal, parentDom); - if (prevInternal) prevInternal._next = internal; + // if (prevInternal) prevInternal._next = internal; // prevInternal._next = null; } else { const vnode = children[index]; patch(internal, vnode, parentDom); - // If the previous Internal doesn't point back to us, it means we were moved. - // if (prevInternal._next !== internal) { - if (internal._next !== next && internal._next) { + + if (internal._index !== LDSMarker) { // move console.log('move', internal.type, internal.data.textContent); console.log( @@ -153,11 +195,6 @@ export function patchChildren(parentInternal, children, parentDom) { ); console.log(' > _next:', next && next.type, next && next.props); insert(internal, parentDom, getDomSibling(internal)); - // we moved this node, so unset its previous sibling's next pointer - // note: this is like doing a splice() out of oldChildren - internal._prev._next = next; // or set to null? - // prevInternal._next = internal; - // internal._prev._next = internal; } else { console.log('update', internal.type, internal.data.textContent); console.log( @@ -175,7 +212,8 @@ export function patchChildren(parentInternal, children, parentDom) { if (!prevInternal) parentInternal._child = internal; // for now, we're only using double-links internally to this function: - internal._prev = null; + internal._prev = internal._prevLDS = null; + internal._index = index; internal = prevInternal; } } diff --git a/src/diff/mount.js b/src/diff/mount.js index 67d1169c6d..34571c7f40 100644 --- a/src/diff/mount.js +++ b/src/diff/mount.js @@ -242,6 +242,7 @@ export function mountChildren(parentInternal, children, parentDom, startDom) { const normalizedVNode = typeof vnode === 'object' ? vnode : String(vnode); internal = createInternal(normalizedVNode, parentInternal); + internal._index = i; if (prevInternal) prevInternal._next = internal; else parentInternal._child = internal; diff --git a/src/internal.d.ts b/src/internal.d.ts index b580ac7754..48fdb33cd1 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -153,6 +153,8 @@ export interface Internal

                                                                          { _next: Internal | null; /** temporarily holds previous sibling Internal while diffing. Is purposefully cleared after diffing due to how the diffing algorithm works */ _prev: Internal | null; + _index: number; + _prevLDS: Internal | null; /** most recent vnode ID */ _vnodeId: number; /** diff --git a/src/tree.js b/src/tree.js index f05bceafbe..bf2c072563 100644 --- a/src/tree.js +++ b/src/tree.js @@ -97,6 +97,8 @@ export function createInternal(vnode, parentInternal) { _child: null, _next: null, _prev: null, + _prevLDS: null, + _index: -1, _vnodeId: vnodeId, _component: null, _context: null, diff --git a/test/browser/keys.test.js b/test/browser/keys.test.js index 629c03c289..27da6dd65f 100644 --- a/test/browser/keys.test.js +++ b/test/browser/keys.test.js @@ -327,7 +327,7 @@ describe.only('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('ba'); - expect(getLog()).to.deep.equal(['

                                                                            ab.insertBefore(
                                                                          1. b,
                                                                          2. a)']); + expect(getLog()).to.deep.equal(['
                                                                              ab.insertBefore(
                                                                            1. a, Null)']); }); it('should swap existing keyed children in the middle of a list efficiently', () => { From c7072477c50de042bd3d922f64fb64f3561b03b2 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 19 Jan 2023 01:06:04 -0800 Subject: [PATCH 11/71] Simplify insertion loop a bit --- src/diff/children.js | 47 ++++++++++++-------------------------------- 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 01c34875d8..c06e2d8555 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -36,6 +36,8 @@ const LDSMarker = -2; * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered */ export function patchChildren(parentInternal, children, parentDom) { + /** @type {Internal | undefined} */ + let internal; /** @type {Internal} */ let prevInternal; // let newTail; @@ -117,7 +119,9 @@ export function patchChildren(parentInternal, children, parentDom) { } // Step 3. Find the longest decreasing subsequence - let internal = prevInternal; + // TODO: Ideally only run this if something has moved + // TODO: Explore trying to do this without an array, maybe next pointers? Or maybe reuse the array + internal = prevInternal; /** @type {Internal[]} */ const wipLDS = [internal]; @@ -156,16 +160,14 @@ export function patchChildren(parentInternal, children, parentDom) { ldsNode = ldsNode._prevLDS; } - // next, we walk backwards over the newly-assigned _prev properties, - // visiting each Internal to set its _next ptr and perform insert/mount/update. - internal = prevInternal; + // Step 5. Walk backwards over the newly-assigned _prev properties, visiting + // each Internal to set its _next ptr and perform insert/mount/update. /** @type {Internal} */ let nextInternal = null; let index = children.length; + internal = prevInternal; while (internal) { - let next = internal._next; - // set this internal's next ptr to the previous loop entry internal._next = nextInternal; nextInternal = internal; @@ -176,38 +178,15 @@ export function patchChildren(parentInternal, children, parentDom) { if (prevInternal === parentInternal) prevInternal = undefined; if (internal._index === -1) { - console.log('mount', internal.type); mount(internal, parentDom, getDomSibling(internal)); - insert(internal, parentDom); - // if (prevInternal) prevInternal._next = internal; - // prevInternal._next = null; } else { - const vnode = children[index]; - patch(internal, vnode, parentDom); - - if (internal._index !== LDSMarker) { - // move - console.log('move', internal.type, internal.data.textContent); - console.log( - ' > expected _next:', - internal._next && internal._next.type, - internal._next && internal._next.props - ); - console.log(' > _next:', next && next.type, next && next.props); - insert(internal, parentDom, getDomSibling(internal)); - } else { - console.log('update', internal.type, internal.data.textContent); - console.log( - ' > expected _next:', - internal._next && internal._next.type, - internal._next && internal._next.props - ); - console.log(' > _next:', next && next.type, next && next.props); - } + // TODO: Skip over non-renderable vnodes + patch(internal, children[index], parentDom); } - // if (prevInternal) prevInternal._next = internal; - // else parentInternal._child = internal; + if (internal._index > LDSMarker) { + insert(internal, parentDom); + } if (!prevInternal) parentInternal._child = internal; From e04e7e7d2121490c77ea35296f0366fa6fe1089a Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 19 Jan 2023 19:31:30 -0800 Subject: [PATCH 12/71] WIP child diffing explanation --- src/diff/vdom-children-diffing.md | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/diff/vdom-children-diffing.md diff --git a/src/diff/vdom-children-diffing.md b/src/diff/vdom-children-diffing.md new file mode 100644 index 0000000000..6e3f4d224f --- /dev/null +++ b/src/diff/vdom-children-diffing.md @@ -0,0 +1,39 @@ +# Virtual DOM Children diffing + +One problem all virtual dom implementations must solve is diffing children: Given the list of the current children, and the list of the new children, which children moved? To do this, there are generally 3 steps: + +1. Match up nodes between current and new children +2. Determine which nodes are added, removed, or changed positions +3. Apply changes to the DOM + +For the first step, virtual dom libraries need a way to match the current children to the new children. When children are a dynamically generated array, this matching is typically done using user-provided keys on the each child. If a user doesn't provide keys on children, then the key is typically the index of the child. Our discussion today will focus on the algorithms to determine which children moved when each child is identified using a key. + +## Determining movement + +Given a list of current children: `0 1 2 3` and new children `3 0 1 2`, what nodes would you move to make the current children match the new children? One approach could be to append `0` after `3` (`1 2 3 0`), append `1` after `0` (`2 3 0 1`), and then append `2` after `1` (`3 0 1 2`). However, a simpler approach would be to just insert `3` before `0` (`3 0 1 2`). + +When determining which nodes to move, our goal is minimize the number of nodes that move. Compared to other operations virtual dom libraries do, moving DOM nodes is an expensive operation, so reducing DOM node moves increases performance. + +How do we determine the fewest number of movements? Looking at our previous example (`0 1 2 3` -> `3 0 1 2`), the key to seeing that moving + +TODO: mention we want find the longest sequence of _relatively_ in place nodes, i.e. nodes that are in the same order with relationship to each other. So we compare the existing + +### Longest common subsequence + +`0123` -> `3012` + +`012345` -> `024135` + +Time Complexity: `O(n^2)` +Space Complexity: `O(min(m,n))` + +### Longest increasing subsequence + +Time Complexity: `O(n*log(n))` +Space Complexity: `O(n)` + +TODO: Why longest increasing subsequence (only looking at one array?) and not longest common subsequence (comparing the two arrays)? Numbers that are increasing are in in the correct order (`2 4 5 8`), perhaps? We are looking for the longest subsequence of nodes whose new positions are in the same relative old position ("relative old position" meaning "position relative to other nodes"). The relative old position is marked by the values in the array. + +### Adding new nodes and deletions + +TODO: fill out From ffb7d52ddb324318abff883f9eea4b8aa1f06f81 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 20 Jan 2023 17:09:52 -0800 Subject: [PATCH 13/71] WIP child diffing explanation progress --- src/diff/vdom-children-diffing.md | 79 ++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/src/diff/vdom-children-diffing.md b/src/diff/vdom-children-diffing.md index 6e3f4d224f..9989189d7b 100644 --- a/src/diff/vdom-children-diffing.md +++ b/src/diff/vdom-children-diffing.md @@ -1,5 +1,7 @@ # Virtual DOM Children diffing +## Introduction + One problem all virtual dom implementations must solve is diffing children: Given the list of the current children, and the list of the new children, which children moved? To do this, there are generally 3 steps: 1. Match up nodes between current and new children @@ -8,32 +10,87 @@ One problem all virtual dom implementations must solve is diffing children: Give For the first step, virtual dom libraries need a way to match the current children to the new children. When children are a dynamically generated array, this matching is typically done using user-provided keys on the each child. If a user doesn't provide keys on children, then the key is typically the index of the child. Our discussion today will focus on the algorithms to determine which children moved when each child is identified using a key. +For example, you may have a virtual tree such as: + +```jsx +
                                                                                +
                                                                              • a
                                                                              • +
                                                                              • b
                                                                              • +
                                                                              • c
                                                                              • +
                                                                              • d
                                                                              • +
                                                                              +``` + +And a next render might produce this tree instead: + +```jsx +
                                                                                +
                                                                              • d
                                                                              • +
                                                                              • a
                                                                              • +
                                                                              • b
                                                                              • +
                                                                              • c
                                                                              • +
                                                                              +``` + +Determining how to morph the original children of the `ul` element into the newly rendered children is the job your virtual DOM library. + +For here one out, will will simplify our discussion of virtual dom children and just refer to the list of keys. So we would represent the first JSX example as `0 1 2 3` and the new rendered output as `3 0 1 2`. + ## Determining movement Given a list of current children: `0 1 2 3` and new children `3 0 1 2`, what nodes would you move to make the current children match the new children? One approach could be to append `0` after `3` (`1 2 3 0`), append `1` after `0` (`2 3 0 1`), and then append `2` after `1` (`3 0 1 2`). However, a simpler approach would be to just insert `3` before `0` (`3 0 1 2`). When determining which nodes to move, our goal is minimize the number of nodes that move. Compared to other operations virtual dom libraries do, moving DOM nodes is an expensive operation, so reducing DOM node moves increases performance. -How do we determine the fewest number of movements? Looking at our previous example (`0 1 2 3` -> `3 0 1 2`), the key to seeing that moving +How do we determine the fewest number of movements? Looking at our previous example (`0 1 2 3` -> `3 0 1 2`), the key to seeing that we can just move the `3` is noticing that `0 1 2` doesn't change between the old and new children. Only the `3` changes! `0 1 2` is the _longest common substring_ between the children (here, "substring" refers to contiguous sequence of items in our children array, i.e. a subset of items that are next to each other). Holding the longest common substring constant and moving elements around it helped us identify smaller number of movements to morph `0 1 2 3` into `3 0 1 2`. + +But let's look at another example: `0 1 2 3 4 5` -> `0 3 1 4 2 5`. Here, there are no common substrings! But if you look closely we can morph the old children into the new children in just 2 moves: insert `3` before `1` and insert `4` before `2`. How did we determine that? + +While there aren't any common substrings between the children arrays, there are common _subsequences_. A subsequence is set of elements from an array that are in the same order as the original sequence, but aren't necessarily next to each other. So while a _substring_ the items must be next to each other, in a _subsequence_ they do not. For example, `0 3 5`, `0 1 5`, and `2 4` are a subsequences of `0 1 2 3 4 5`. `0 2 1` is not a subsequence of `0 1 2 3 4 5` because the `2` does not occur after `0` and before `1` in the original sequence. -TODO: mention we want find the longest sequence of _relatively_ in place nodes, i.e. nodes that are in the same order with relationship to each other. So we compare the existing +In our example above (`0 1 2 3 4 5` -> `0 3 1 4 2 5`), let's use our understanding of subsequences to examine the differences between the two children arrays. If we remove the two nodes that moved (`3` and `4`) from the arrays, we are left a common subsequence between the two arrays: `0 1 2 5`. This subsequence is actually the _longest common subsequence_ between the two arrays! -### Longest common subsequence +If we can identify _longest common subsequence_ between two arrays, we have identified the most nodes that we can hold in place (i.e. **not move**). Every other node has changed positions and can move around these nodes. This approach leads to the minimal number of moves because we have found the most nodes that we don't have to move. Said another way, we have found the longest sequence of _relatively_ in place nodes, i.e. nodes that are in the same order with relationship to other nodes in the array. -`0123` -> `3012` +The longest common subsequence algorithm is a well-known algorithm in computer science. You can read more about [longest common subsequence on Wikipedia](https://en.wikipedia.org/wiki/Longest_common_subsequence). The best time complexity for determining the longest common subsequence is $O(n^2)$. This time complexity is acceptable, but can we do better for our specific use case of diffing virtual DOM children? -`012345` -> `024135` +## Longest increasing subsequence -Time Complexity: `O(n^2)` -Space Complexity: `O(min(m,n))` +Typically, virtual dom keys are strings. A more realistic example might look something like trying to morph `a c b e d f` -> `a b c d e f`. Let's map this to numbers using the indices of each key in the original array. So the original array becomes `0 1 2 3 4 5` (with `{a: 0, c: 1, b: 2, e: 3, d: 4, f: 5}`) and the new array becomes `0 2 1 4 3 5`. -### Longest increasing subsequence +```text +Original array + key: a c b e d f +original index: 0 1 2 3 4 5 -Time Complexity: `O(n*log(n))` -Space Complexity: `O(n)` +New array + key: a b c d e f +original index: 0 2 1 4 3 5 +``` + +Two interesting properties emerge from doing this: + +1. The original array will always be an array of numbers from `0` to `array.length`, and so will always be numbers sorted in increasing order. +2. The new array is a mapping of an item's new index to it's old index in the original array. Said another way, the item at index `0` in `0 2 1 4 3 5` (`a`) was at index `0` in the original way, the item at index `1` in the new array (`b`) was at index `2` in the original array, and so on. + +We can use these properties to simplify the problem we are trying to solve. Remember, we are searching for items that are in the relatively same order between the two arrays (in the same order relative to each other). Since the original array will always be in increasing sorted order, any subsequence of the original array that is also present in the new array will also be in increasing order! + +This insight is big! We can simplify what we need to search for now. Instead of solving the general _longest common subsequence_ problem, we now only need to look for the longest subsequence of increasing numbers in the new array. Again, any subsequence of the new array that is in increasing order is also a subsequence of the original array which was in increasing sorted order. TODO: Why longest increasing subsequence (only looking at one array?) and not longest common subsequence (comparing the two arrays)? Numbers that are increasing are in in the correct order (`2 4 5 8`), perhaps? We are looking for the longest subsequence of nodes whose new positions are in the same relative old position ("relative old position" meaning "position relative to other nodes"). The relative old position is marked by the values in the array. -### Adding new nodes and deletions +Finding the longest increasing subsequence is an easier algorithm because we are only looking at one array (the new array with the mapped indices) and has a better time complexity: $O(n\log(n))$. + +### Algorithm + +TODO: Show algorithm + +## Adding new nodes and deletions TODO: fill out + +## Acknowledgements + +- @localvoid and his work in `ivi` +- Domenic and his work in inferno +- leetcode editors for the great descriptions of these algorithms on their website From 91c8247583f2908e2a222af9077f60520034e8b8 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 22 Jan 2023 01:11:44 -0800 Subject: [PATCH 14/71] Use INSERT_INTERNAL flag to mark nodes for insertion --- src/constants.js | 3 +++ src/diff/children.js | 29 +++++++++++++++++------------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/constants.js b/src/constants.js index a2e805effa..14d7e26b88 100644 --- a/src/constants.js +++ b/src/constants.js @@ -48,6 +48,9 @@ export const DIRTY_BIT = 1 << 14; /** Signals the component can skip children due to a non-update */ export const SKIP_CHILDREN = 1 << 15; +/** Indicates that this node needs to be inserted while patching children */ +export const INSERT_INTERNAL = 1 << 16; + /** Reset all mode flags */ export const RESET_MODE = ~( MODE_HYDRATE | diff --git a/src/diff/children.js b/src/diff/children.js index c06e2d8555..72f6873aba 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -8,16 +8,14 @@ import { EMPTY_ARR, TYPE_DOM, UNDEFINED, - TYPE_ELEMENT + TYPE_ELEMENT, + INSERT_INTERNAL } from '../constants'; import { mount } from './mount'; import { patch } from './patch'; import { unmount } from './unmount'; import { createInternal, getChildDom, getDomSibling } from '../tree'; -// TODO: Use a flag? -const LDSMarker = -2; - /** * Scenarios: * @@ -93,9 +91,9 @@ export function patchChildren(parentInternal, children, parentDom) { // no match, create a new Internal: if (!internal) { internal = createInternal(normalizedVNode, parentInternal); - console.log('creating new', internal.type); + // console.log('creating new', internal.type); } else { - console.log('updating', internal.type); + // console.log('updating', internal.type); // patch(internal, vnode, parentDom); } @@ -124,8 +122,13 @@ export function patchChildren(parentInternal, children, parentDom) { internal = prevInternal; /** @type {Internal[]} */ const wipLDS = [internal]; + internal.flags |= INSERT_INTERNAL; while ((internal = internal._prev) !== parentInternal) { + // Mark all internals as requiring insertion. We will clear this flag for + // internals on longest decreasing subsequence + internal.flags |= INSERT_INTERNAL; + // Skip over newly mounted internals. They will be mounted in place. if (internal._index === -1) continue; @@ -155,8 +158,8 @@ export function patchChildren(parentInternal, children, parentDom) { /** @type {Internal | null} */ let ldsNode = wipLDS[wipLDS.length - 1]; while (ldsNode) { - // Mark node as being in the longest increasing subsequence (_index = -2) - ldsNode._index = LDSMarker; + // This node is on the longest decreasing subsequence so clear INSERT_NODE flag + ldsNode.flags &= ~INSERT_INTERNAL; ldsNode = ldsNode._prevLDS; } @@ -179,17 +182,19 @@ export function patchChildren(parentInternal, children, parentDom) { if (internal._index === -1) { mount(internal, parentDom, getDomSibling(internal)); + insert(internal, parentDom); } else { // TODO: Skip over non-renderable vnodes patch(internal, children[index], parentDom); - } - - if (internal._index > LDSMarker) { - insert(internal, parentDom); + if (internal.flags & INSERT_INTERNAL) { + insert(internal, parentDom); + } } if (!prevInternal) parentInternal._child = internal; + internal.flags &= ~INSERT_INTERNAL; + // for now, we're only using double-links internally to this function: internal._prev = internal._prevLDS = null; internal._index = index; From 133d09a9e590f4474b12b87dbb6274ceff4a7fae Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 22 Jan 2023 01:25:41 -0800 Subject: [PATCH 15/71] Eagerly clear _prev to prevent code in mount, patch, or insert from trying to read it --- src/diff/children.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/diff/children.js b/src/diff/children.js index 72f6873aba..73c4f29863 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -118,6 +118,7 @@ export function patchChildren(parentInternal, children, parentDom) { // Step 3. Find the longest decreasing subsequence // TODO: Ideally only run this if something has moved + // TODO: Replace _prevLDS with _next. Doing this will make _next meaningless for a moment // TODO: Explore trying to do this without an array, maybe next pointers? Or maybe reuse the array internal = prevInternal; /** @type {Internal[]} */ @@ -178,6 +179,8 @@ export function patchChildren(parentInternal, children, parentDom) { index--; prevInternal = internal._prev; + internal._prev = internal._prevLDS = null; + if (prevInternal === parentInternal) prevInternal = undefined; if (internal._index === -1) { @@ -196,7 +199,6 @@ export function patchChildren(parentInternal, children, parentDom) { internal.flags &= ~INSERT_INTERNAL; // for now, we're only using double-links internally to this function: - internal._prev = internal._prevLDS = null; internal._index = index; internal = prevInternal; } From b0484d0b1db352864f32c5fb0175d01557e7f58e Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 22 Jan 2023 01:52:12 -0800 Subject: [PATCH 16/71] WIP: progress on children diffing doc --- src/diff/vdom-children-diffing.md | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/diff/vdom-children-diffing.md b/src/diff/vdom-children-diffing.md index 9989189d7b..b2f558e53b 100644 --- a/src/diff/vdom-children-diffing.md +++ b/src/diff/vdom-children-diffing.md @@ -46,17 +46,17 @@ How do we determine the fewest number of movements? Looking at our previous exam But let's look at another example: `0 1 2 3 4 5` -> `0 3 1 4 2 5`. Here, there are no common substrings! But if you look closely we can morph the old children into the new children in just 2 moves: insert `3` before `1` and insert `4` before `2`. How did we determine that? -While there aren't any common substrings between the children arrays, there are common _subsequences_. A subsequence is set of elements from an array that are in the same order as the original sequence, but aren't necessarily next to each other. So while a _substring_ the items must be next to each other, in a _subsequence_ they do not. For example, `0 3 5`, `0 1 5`, and `2 4` are a subsequences of `0 1 2 3 4 5`. `0 2 1` is not a subsequence of `0 1 2 3 4 5` because the `2` does not occur after `0` and before `1` in the original sequence. +While there aren't any common substrings between the children arrays, there are common _subsequences_. A subsequence is set of elements from an array that are in the same order as the original sequence, but aren't necessarily next to each other. So while in a _substring_ the items must be next to each other, in a _subsequence_ they do not. For example, `0 3 5`, `0 1 5`, and `2 4` are a subsequences of `0 1 2 3 4 5`. `0 2 1` is not a subsequence of `0 1 2 3 4 5` because the `2` does not occur after `0` and before `1` in the original sequence. In our example above (`0 1 2 3 4 5` -> `0 3 1 4 2 5`), let's use our understanding of subsequences to examine the differences between the two children arrays. If we remove the two nodes that moved (`3` and `4`) from the arrays, we are left a common subsequence between the two arrays: `0 1 2 5`. This subsequence is actually the _longest common subsequence_ between the two arrays! If we can identify _longest common subsequence_ between two arrays, we have identified the most nodes that we can hold in place (i.e. **not move**). Every other node has changed positions and can move around these nodes. This approach leads to the minimal number of moves because we have found the most nodes that we don't have to move. Said another way, we have found the longest sequence of _relatively_ in place nodes, i.e. nodes that are in the same order with relationship to other nodes in the array. -The longest common subsequence algorithm is a well-known algorithm in computer science. You can read more about [longest common subsequence on Wikipedia](https://en.wikipedia.org/wiki/Longest_common_subsequence). The best time complexity for determining the longest common subsequence is $O(n^2)$. This time complexity is acceptable, but can we do better for our specific use case of diffing virtual DOM children? +The longest common subsequence algorithm is a well-known algorithm in computer science. You can read more about [the longest common subsequence algorithm on Wikipedia](https://en.wikipedia.org/wiki/Longest_common_subsequence). The best time complexity for determining the longest common subsequence is $O(n^2)$. This time complexity is acceptable, but can we do better for our specific use case of diffing virtual DOM children? ## Longest increasing subsequence -Typically, virtual dom keys are strings. A more realistic example might look something like trying to morph `a c b e d f` -> `a b c d e f`. Let's map this to numbers using the indices of each key in the original array. So the original array becomes `0 1 2 3 4 5` (with `{a: 0, c: 1, b: 2, e: 3, d: 4, f: 5}`) and the new array becomes `0 2 1 4 3 5`. +Typically, virtual dom keys are strings. A more realistic example might look something like trying to morph `a c b e d f` -> `a b c d e f`. Let's map this to numbers using the indices of each key in the original array. Let's assign to each key its index in the original array: `{a: 0, c: 1, b: 2, e: 3, d: 4, f: 5}`. Now, let's replace each key with this number in both arrays. So the original array becomes `0 1 2 3 4 5` and the new array becomes `0 2 1 4 3 5`. ```text Original array @@ -70,20 +70,29 @@ original index: 0 2 1 4 3 5 Two interesting properties emerge from doing this: -1. The original array will always be an array of numbers from `0` to `array.length`, and so will always be numbers sorted in increasing order. +1. The original array will always be an array of numbers from `0` to `array.length - 1`, and will always be numbers sorted in increasing order. 2. The new array is a mapping of an item's new index to it's old index in the original array. Said another way, the item at index `0` in `0 2 1 4 3 5` (`a`) was at index `0` in the original way, the item at index `1` in the new array (`b`) was at index `2` in the original array, and so on. -We can use these properties to simplify the problem we are trying to solve. Remember, we are searching for items that are in the relatively same order between the two arrays (in the same order relative to each other). Since the original array will always be in increasing sorted order, any subsequence of the original array that is also present in the new array will also be in increasing order! +We can use these properties to simplify the problem we are trying to solve. Remember, we are searching for items that are in the relatively same order between the two arrays (in the same order relative to each other). Since the original array will always be in increasing sorted order, any subsequence of the original array that is present in the new array will also be in increasing order. This insight is big! We can simplify what we need to search for now. Instead of solving the general _longest common subsequence_ problem, we now only need to look for the longest subsequence of increasing numbers in the new array. Again, any subsequence of the new array that is in increasing order is also a subsequence of the original array which was in increasing sorted order. -TODO: Why longest increasing subsequence (only looking at one array?) and not longest common subsequence (comparing the two arrays)? Numbers that are increasing are in in the correct order (`2 4 5 8`), perhaps? We are looking for the longest subsequence of nodes whose new positions are in the same relative old position ("relative old position" meaning "position relative to other nodes"). The relative old position is marked by the values in the array. - -Finding the longest increasing subsequence is an easier algorithm because we are only looking at one array (the new array with the mapped indices) and has a better time complexity: $O(n\log(n))$. +Finding the longest increasing subsequence is an easier algorithm because we are only looking at one array (the new array with the mapped indices) and has a better time complexity: $O(n\log(n))$. Let's take a look at the algorithm to do this. ### Algorithm -TODO: Show algorithm +TODO: Talk about `insertBefore` and how DOM objects such as Document and DocumentFragment only support `insertBefore` and not newer methods such as `after` or `before` since they are only containers. You can't place a node after the document which is the root container. This limitation motivates using `prev` pointers in our algorithm. + +For understanding this algorithm, we are going to use a linked list to represent our array of children. Here is the structure of the linked list node we will use: + +```ts +interface Node { + originalIndex: number; + next: Node | null; +} +``` + +The list we are searching through is the list of ## Adding new nodes and deletions From 14a23f66740fc9fa8c0d59c012abe7b1ce66c794 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 22 Jan 2023 01:57:01 -0800 Subject: [PATCH 17/71] Fix unmounting --- src/diff/children.js | 2 ++ src/diff/unmount.js | 17 ++++++++--------- test/browser/keys.test.js | 3 --- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 73c4f29863..721b002142 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -117,6 +117,8 @@ export function patchChildren(parentInternal, children, parentDom) { } // Step 3. Find the longest decreasing subsequence + // TODO: Move this into it's own function + // TODO: Check prevInternal exits before running (aka we are unmounting everything); // TODO: Ideally only run this if something has moved // TODO: Replace _prevLDS with _next. Doing this will make _next meaningless for a moment // TODO: Explore trying to do this without an array, maybe next pointers? Or maybe reuse the array diff --git a/src/diff/unmount.js b/src/diff/unmount.js index 0b28db91da..639e847266 100644 --- a/src/diff/unmount.js +++ b/src/diff/unmount.js @@ -34,15 +34,14 @@ export function unmount(internal, parentInternal, skipRemove) { } } - if ((r = internal._children)) { - for (; i < r.length; i++) { - if (r[i]) { - unmount( - r[i], - parentInternal, - skipRemove ? ~internal.flags & TYPE_ROOT : internal.flags & TYPE_DOM - ); - } + if ((r = internal._child)) { + while (r) { + unmount( + r, + parentInternal, + skipRemove ? ~internal.flags & TYPE_ROOT : internal.flags & TYPE_DOM + ); + r = r._next; } } diff --git a/test/browser/keys.test.js b/test/browser/keys.test.js index 27da6dd65f..05c2176894 100644 --- a/test/browser/keys.test.js +++ b/test/browser/keys.test.js @@ -443,8 +443,6 @@ describe.only('keys', () => { // ]); }); - // eslint-disable-next-line jest/no-commented-out-tests - /* it("should not preserve state when a component's keys are different", () => { const Stateful = createStateful('Stateful'); @@ -844,5 +842,4 @@ describe.only('keys', () => { done(); }); }); - */ }); From 1337b1dc21c1ab15e6100793e18f2c5f81e9efe4 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 23 Jan 2023 12:19:02 -0800 Subject: [PATCH 18/71] Add "summary" and some more notes to children algorithm --- src/diff/children.js | 7 +- src/diff/vdom-children-diffing.md | 113 ++++++++++++++++++++++++++++-- 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 721b002142..aa9b1884cb 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -181,6 +181,8 @@ export function patchChildren(parentInternal, children, parentDom) { index--; prevInternal = internal._prev; + + // for now, we're only using double-links internally to this function: internal._prev = internal._prevLDS = null; if (prevInternal === parentInternal) prevInternal = undefined; @@ -200,7 +202,10 @@ export function patchChildren(parentInternal, children, parentDom) { internal.flags &= ~INSERT_INTERNAL; - // for now, we're only using double-links internally to this function: + // TODO: this index should match the index of the matching vnode. So any + // skipping over of non-renderables should move this index accordingly. + // Doing this should help with null placeholders and enable us to skip the + // LIS algorithm for situations like `{condition &&
                                                                              }` internal._index = index; internal = prevInternal; } diff --git a/src/diff/vdom-children-diffing.md b/src/diff/vdom-children-diffing.md index b2f558e53b..41e40eb2f4 100644 --- a/src/diff/vdom-children-diffing.md +++ b/src/diff/vdom-children-diffing.md @@ -60,8 +60,8 @@ Typically, virtual dom keys are strings. A more realistic example might look som ```text Original array - key: a c b e d f -original index: 0 1 2 3 4 5 + key: a c b e d f +index: 0 1 2 3 4 5 New array key: a b c d e f @@ -79,7 +79,65 @@ This insight is big! We can simplify what we need to search for now. Instead of Finding the longest increasing subsequence is an easier algorithm because we are only looking at one array (the new array with the mapped indices) and has a better time complexity: $O(n\log(n))$. Let's take a look at the algorithm to do this. -### Algorithm +### Basic dynamic programming algorithm + +Let's start by writing a simple loop to find and track the first increasing subsequence to get us started: + +```js +/** + * Longest increasing subsequence (LIS) algorithm + * @param {number[]} nums An array of numbers to find the longest increasing subsequence + * @returns {number[]} The items in the longest increasing subsequence of nums + */ +function findLIS(nums) { + // If there are no items, just return an empty array + if (nums.length < 0) { + return []; + } + + // There is at least one item in the array, so let's get started by adding it to + // start our longest increasing subsequence. + /** @type {number[]} The indicies of the items in the longest increasing subsequence */ + const lisIndices = [0]; + + for (let i = 1; i < nums.length; i++) { + /** The next value from nums to consider */ + const nextValue = nums[i]; + + /** Last index of current longest increasing subsequence */ + const lastIndex = lisIndices[lisIndices.length - 1]; + /** Last value in our current longest increasing subsequence */ + const lastValue = nums[lastIndex]; + + if (nums[i] > lastValue) { + // The next value in nums is greater than the last value in our current + // increasing subsequence. Let's tack this index at the end of the + // sequence + lisIndices.push(i); + } else { + // We'll come back to this part + } + } + + // Now that lisIndices has all the indices of our increasing subsequence, + // let's add those items to an array and return the result. + const result = []; + for (let index of lisIndices) { + result.push(nums[index]); + } + + return result; +} +``` + +TODO: This code currently only finds + +## Algorithm + +Design decisions: + +1. Use linked list for perf and memory reasons +2. Iterate backwards cuz `insertBefore` is the API available on `Node` in DOM. We can't use `after` or `before` cuz DocumentFragment and Document don't support it. TODO: Talk about `insertBefore` and how DOM objects such as Document and DocumentFragment only support `insertBefore` and not newer methods such as `after` or `before` since they are only containers. You can't place a node after the document which is the root container. This limitation motivates using `prev` pointers in our algorithm. @@ -87,17 +145,62 @@ For understanding this algorithm, we are going to use a linked list to represent ```ts interface Node { - originalIndex: number; + /** The position of this node in the original list of children */ + index: number; next: Node | null; } ``` -The list we are searching through is the list of +TODO: finish ## Adding new nodes and deletions TODO: fill out +## Summary + +Basic algorithm for diffing virtual dom children: + +1. Find matches between current and new children +2. Determine which children should move +3. Unmount, mount, and move children to match the new order + +When determining which children should move, we want to minimize the number of children that move (moving DOM nodes can be expensive). To do this, we need to determine longest subsequence of children that didn't move. One way to do this is to find the longest common subsequence between the current children and next children. However, the longest common subsequence runs in $O(n^2)$ time. + +If we map the array of current and next children into new arrays using their indices from the current children array, we can run a more efficient algorithm. For example, given current children of `a c b e d f` and next children of `a b c d e f`, we use the current children array to create a map of item to index in that array: `{a: 0, c: 1, b: 2, e: 3, d: 4, f: 5}`. Now create arrays using this map. + +```text +Current children + key: a c b e d f +index: 0 1 2 3 4 5 + +New children + key: a b c d e f +original index: 0 2 1 4 3 5 +``` + +Because the original array will always be sorted in increasing order, any common subsequence between the current and new children will also be increasing. Said another way, any increasing subsequence of the new children array is also a subsequence in the current children. So if we find the longest increasing subsequence in the new children array, we've found the longest common subsequence between the two arrays! Looking for the longest increasing subsequence, we no longer need to compare the two arrays and can instead just look at the new children array. + +To find the longest increasing subsequence, we need to keep track of two things as we traverse the array: + +1. The node/index that ends the smallest (in value) subsequence of each length + + For example, the subsequence `0 1 2` is smaller than `4 5 6` since the values in the subsequence are smaller + +2. The complete subsequence for each length + +For #1, let's take a look at two examples, `0 8 3 6` and `4 5 3 2`. Upon traversing over the first two elements (`0 8` in the first example and `4 5` in the second), we add them to our current increasing subsequence since they are all increasing. However, when we get to `3`, we need to decide whether to throw away our existing subsequence to include `3` or not. In first example, we should use `3` because using `3` gives us a longer subsequence `0 3 6`. But in the second example, we should not because `4 5` is the longest increasing subsequence. + +This decision is where #1 comes into play. After traversing the first two elements we end up with array `[0, 1]` signaling that the smallest increasing subsequence of length 1 starts at index `0`, the smallest subsequence of length 2 starts at index `2`, and so on. Upon reaching the value `3`, we search through our existing array to see if it can form start a smaller subsequence. + +In the first example, `3 < 8` so we replace the `8`'s index (`1`) with `3`'s index: `[0, 2]`. When we get to `6`, the tip of our subsequence is the value at index `2` (`3`). Since `6 > 3`, we add to our subsequence. + +In the second example, `3 < 4` so we replace `4`'s index (`0`) with `3`'s index: `[2, 1]`. However, we have now broken our subsequence. `3` doesn't come before `5` in the original array. Doing this is okay though because this array we are using is only keeping track of the index that starts the subsequence at that length. + +And this is where #2 comes into play. Since #1 only keeps tracks of the start of an increasing subsequence, we use an additional data structure to keep track of what the actual subsequence that starts at that index is. This data structure can be an array of the index that comes before current index in its increasing subsequence, or if using a linked list, a pointer to the previous node in its increasing subsequence. + +In Preact, we use linked lists to traverse the virtual DOM tree for better memory and performance. Also, to support DOM objects such as Document and DocumentFragment, we only use the `insertBefore` method to move DOM nodes around. Because `insertBefore` requires knowing the node that comes after the node to insert, we loop through children backwards, setting up a nodes next sibling before itself. + ## Acknowledgements - @localvoid and his work in `ivi` From fce7ce4a06da1a5e353525af2a06b3e96606ae7f Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 26 Jan 2023 09:33:15 -0800 Subject: [PATCH 19/71] WIP: progress on doc --- src/diff/vdom-children-diffing.md | 34 ++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/diff/vdom-children-diffing.md b/src/diff/vdom-children-diffing.md index 41e40eb2f4..7530137061 100644 --- a/src/diff/vdom-children-diffing.md +++ b/src/diff/vdom-children-diffing.md @@ -81,7 +81,7 @@ Finding the longest increasing subsequence is an easier algorithm because we are ### Basic dynamic programming algorithm -Let's start by writing a simple loop to find and track the first increasing subsequence to get us started: +Let's start by writing a simple loop to find and track the first increasing subsequence to get us started. In this code we'll use the acronym LIS (or `lis`) to represent "longest increasing subsequence". ```js /** @@ -109,7 +109,7 @@ function findLIS(nums) { /** Last value in our current longest increasing subsequence */ const lastValue = nums[lastIndex]; - if (nums[i] > lastValue) { + if (lastValue < nextValue) { // The next value in nums is greater than the last value in our current // increasing subsequence. Let's tack this index at the end of the // sequence @@ -130,7 +130,35 @@ function findLIS(nums) { } ``` -TODO: This code currently only finds +This code currently only finds the first increasing subsequence starting at the first item. To fix it to find the longest increasing subsequence, let's walk through some examples and talk about what we should change. + +#### Example 1: 0 8 3 6 + +Let's run the array `0 8 3 6` through our algorithm: + +1. `nums.length` is `4` so we continue on +2. We initialize `lisIndices` to `[0]` and start our loop at `1` +3. Loop iteration `i=1`: + `lisIndices` is currently `[0]` + 1. We initialize some variables: + `nextValue = 8` (`nums[i]`) + `lastIndex = 0` (`lisIndices[lisIndices.length - 1]`) + `lastValue = 0` (`nums[lasIndex]`) + 2. `0 < 8 == true` (`lastValue < nextValue`) + The next value in our array is greater than the last value in our current increasing subsequence, so let's add it to the subsequence: + 3. `lisIndices.push(1)` +4. Loop iteration `i=2` + `lisIndices` is currently `[0, 1]` + + 1. Initialize variables + `nextValue = 3` (`nums[i]`) + `lastIndex = 1` (`lisIndices[lisIndices.length - 1]`) + `lastValue = 8` (`nums[lasIndex]`) + 2. `8 < 3 == false` (`lastValue < nextValue`) + + TODO: Here we've reached our first + +#### Example 2: 1 4 5 6 2 3 ## Algorithm From d6bc74108b764cd6cd866cebbeab6534f8f0a973 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 27 Jan 2023 16:22:12 -0800 Subject: [PATCH 20/71] Update getParentDom tests to use linked list structure --- test/browser/getParentDom.test.js | 68 +++++++++++++++++++------------ 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/test/browser/getParentDom.test.js b/test/browser/getParentDom.test.js index 237816ee95..7aa9c29e23 100644 --- a/test/browser/getParentDom.test.js +++ b/test/browser/getParentDom.test.js @@ -4,17 +4,36 @@ import { setupScratch, teardown } from '../_util/helpers'; /** @jsx createElement */ +/** + * @typedef {import('../../src/internal').Internal} Internal + * @typedef {import('../../src/internal').PreactElement} PreactElement + */ + describe('getParentDom', () => { - /** @type {import('../../src/internal').PreactElement} */ + /** @type {PreactElement} */ let scratch; - const getRoot = dom => dom._children; + /** @type {(dom: PreactElement) => Internal} */ + const getRoot = dom => dom._child; const Root = props => props.children; const createPortal = (vnode, parent) => ( {vnode} ); + /** + * @param {Internal} parent + * @param {(internal: Internal, index: number) => void} cb + */ + function forEachChild(parent, cb) { + let index = 0; + let child = parent._child; + while (child) { + cb(child, index++); + child = child._next; + } + } + beforeEach(() => { scratch = setupScratch(); }); @@ -34,11 +53,11 @@ describe('getParentDom', () => { scratch ); - let domInternals = getRoot(scratch)._children[0]._children; - for (let internal of domInternals) { + let parentInternal = getRoot(scratch)._child; + forEachChild(parentInternal, internal => { expect(internal.type).to.equal('div'); expect(getParentDom(internal)).to.equalNode(scratch.firstChild); - } + }); }); it('should find direct parent of text node', () => { @@ -49,12 +68,12 @@ describe('getParentDom', () => { scratch ); - let domInternals = getRoot(scratch)._children[0]._children; + let parent = getRoot(scratch)._child; let expectedTypes = ['div', null, 'div']; - for (let i = 0; i < domInternals.length; i++) { - expect(domInternals[i].type).to.equal(expectedTypes[i]); - expect(getParentDom(domInternals[i])).to.equalNode(scratch.firstChild); - } + forEachChild(parent, (internal, i) => { + expect(internal.type).to.equal(expectedTypes[i]); + expect(getParentDom(internal)).to.equalNode(scratch.firstChild); + }); }); it('should find parent through Fragments', () => { @@ -69,8 +88,8 @@ describe('getParentDom', () => { ); let domInternals = [ - getRoot(scratch)._children[0]._children[0]._children[0], - getRoot(scratch)._children[0]._children[1]._children[0] + getRoot(scratch)._child._child._child, + getRoot(scratch)._child._child._next._child ]; let expectedTypes = ['div', null]; @@ -97,8 +116,8 @@ describe('getParentDom', () => { ); let domInternals = [ - getRoot(scratch)._children[0]._children[0]._children[0]._children[0], - getRoot(scratch)._children[0]._children[1]._children[0]._children[0] + getRoot(scratch)._child._child._child._child, + getRoot(scratch)._child._child._next._child._child ]; let expectedTypes = ['div', null]; @@ -127,8 +146,8 @@ describe('getParentDom', () => { ); let domInternals = [ - getRoot(scratch)._children[0]._children[0]._children[0], - getRoot(scratch)._children[0]._children[1]._children[0] + getRoot(scratch)._child._child._child, + getRoot(scratch)._child._child._next._child ]; let expectedTypes = [Foo, Fragment]; @@ -150,8 +169,7 @@ describe('getParentDom', () => { scratch ); - let internal = getRoot(scratch)._children[0]._children[1]._children[0] - ._children[0]; + let internal = getRoot(scratch)._child._child._next._child._child; let parentDom = getParentDom(internal); expect(internal.type).to.equal('span'); @@ -175,8 +193,7 @@ describe('getParentDom', () => { scratch ); - let internal = getRoot(scratch)._children[0]._children[0]._children[0] - ._children[0]; + let internal = getRoot(scratch)._child._child._child._child; let parent = getParentDom(internal); expect(internal.type).to.equal('p'); @@ -192,7 +209,7 @@ describe('getParentDom', () => { scratch ); - const internal = getRoot(scratch)._children[0]; + const internal = getRoot(scratch)._child; expect(internal.type).to.equal(Foo); expect(getParentDom(internal)).to.equal(scratch); }); @@ -215,7 +232,7 @@ describe('getParentDom', () => { expect(scratch.innerHTML).to.equal('
                                                                              '); - let internal = getRoot(scratch)._children[0]._children[0]; + let internal = getRoot(scratch)._child._child; expect(internal.type).to.equal(Root); expect(getParentDom(internal)).to.equalNode(portalParent); }); @@ -238,11 +255,11 @@ describe('getParentDom', () => { expect(scratch.innerHTML).to.equal('
                                                                              '); - let fooInternal = getRoot(scratch)._children[0]._children[0]._children[0]; + let fooInternal = getRoot(scratch)._child._child._child; expect(fooInternal.type).to.equal(Foo); expect(getParentDom(fooInternal)).to.equalNode(portalParent); - let divInternal = fooInternal._children[0]; + let divInternal = fooInternal._child; expect(divInternal.type).to.equal('div'); expect(getParentDom(divInternal)).to.equalNode(portalParent); }); @@ -260,8 +277,7 @@ describe('getParentDom', () => { expect(scratch.innerHTML).to.equal('
                                                                              '); - let internal = getRoot(scratch)._children[0]._children[0]._children[0] - ._children[0]; + let internal = getRoot(scratch)._child._child._child._child; expect(internal.type).to.equal('div'); expect(getParentDom(internal)).to.equalNode(portalParent); }); From bc4bc4cdfda23f4a516f2545a237fd103ef02389 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 27 Jan 2023 16:54:03 -0800 Subject: [PATCH 21/71] Improve getDomSibling tests --- test/browser/getDomSibling.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/browser/getDomSibling.test.js b/test/browser/getDomSibling.test.js index 87e6c85f4c..65ac0832f6 100644 --- a/test/browser/getDomSibling.test.js +++ b/test/browser/getDomSibling.test.js @@ -85,6 +85,7 @@ describe('getDomSibling', () => { scratch ); let internal = getRoot(scratch)._children[0]._children[0]; + expect(internal.key).to.equal('A'); expect(getDomSibling(internal)).to.equalNode( scratch.firstChild.childNodes[1] ); @@ -104,6 +105,7 @@ describe('getDomSibling', () => { scratch ); let internal = getRoot(scratch)._children[0]._children[0]._children[0]; + expect(internal.key).to.equal('A'); expect(getDomSibling(internal)).to.equalNode( scratch.firstChild.childNodes[1] ); @@ -154,6 +156,7 @@ describe('getDomSibling', () => { let divAInternal = getRoot(scratch)._children[0]._children[0]._children[0] ._children[0]._children[0]; expect(divAInternal.type).to.equal('div'); + expect(divAInternal.key).to.equal('A'); expect(getDomSibling(divAInternal)).to.equalNode( scratch.firstChild.childNodes[1] ); @@ -181,6 +184,7 @@ describe('getDomSibling', () => { let fragment = getRoot(scratch)._children[0]._children[0]._children[0] ._children[0]; expect(fragment.type).to.equal(Fragment); + expect(fragment.key).to.equal('0.0.0.0'); expect(getDomSibling(fragment)).to.equalNode( scratch.firstChild.childNodes[1] ); @@ -209,6 +213,7 @@ describe('getDomSibling', () => { let foo = getRoot(scratch)._children[0]._children[0]._children[0] ._children[0]; expect(foo.type).to.equal(Foo); + expect(foo.key).to.equal('0.0.0.0'); expect(getDomSibling(foo)).to.equalNode(scratch.firstChild.childNodes[1]); }); @@ -231,6 +236,7 @@ describe('getDomSibling', () => { let divAInternal = getRoot(scratch)._children[0]._children[0]._children[0]; expect(divAInternal.type).to.equal('div'); + expect(divAInternal.key).to.equal('A'); expect(getDomSibling(divAInternal)).to.equalNode( scratch.firstChild.childNodes[1] ); @@ -250,6 +256,7 @@ describe('getDomSibling', () => { let divAInternal = getRoot(scratch)._children[0]._children[0]; expect(divAInternal.type).to.equal('div'); + expect(divAInternal.key).to.equal('A'); let sibling = getDomSibling(divAInternal); expect(sibling).to.equalNode(scratch.firstChild.childNodes[1]); @@ -267,6 +274,7 @@ describe('getDomSibling', () => { let divAInternal = getRoot(scratch)._children[0]._children[0]; expect(divAInternal.type).to.equal('div'); + expect(divAInternal.key).to.equal('A'); let sibling = getDomSibling(divAInternal); expect(sibling).to.equalNode(scratch.firstChild.childNodes[1]); @@ -308,6 +316,7 @@ describe('getDomSibling', () => { const divCInternal = getRoot(scratch)._children[0]._children[2] ._children[0]; + expect(divCInternal.key).to.equal('C'); expect(getDomSibling(divCInternal)).to.equal(null); }); @@ -330,6 +339,7 @@ describe('getDomSibling', () => { let divAInternal = getRoot(scratch)._children[0]._children[0]._children[0] ._children[0]._children[0]; + expect(divAInternal.key).to.equal('A'); expect(getDomSibling(divAInternal)).to.equal(null); }); @@ -359,6 +369,7 @@ describe('getDomSibling', () => { let divAInternal = getRoot(scratch)._children[0]._children[0]._children[0] ._children[0]._children[0]; + expect(divAInternal.key).to.equal('A'); expect(getDomSibling(divAInternal)).to.equal(null); }); From 4bd061ba8ae73af853ac5f9c5fa3b165bc93f387 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 28 Jan 2023 17:40:39 -0800 Subject: [PATCH 22/71] Fix up getDomSibling for linked list --- compat/src/index.js | 4 +- src/tree.js | 97 ++++++++++++++++++------------ test/browser/getDomSibling.test.js | 92 +++++++++++++++++----------- test/browser/keys.test.js | 2 +- 4 files changed, 119 insertions(+), 76 deletions(-) diff --git a/compat/src/index.js b/compat/src/index.js index 0f965815c9..b3df85bd20 100644 --- a/compat/src/index.js +++ b/compat/src/index.js @@ -89,9 +89,11 @@ function findDOMNode(component) { return null; } else if (component.nodeType == 1) { return component; + } else if (component._internal._child == null) { + return null; } - return getChildDom(component._internal, 0); + return getChildDom(component._internal._child, 0); } /** diff --git a/src/tree.js b/src/tree.js index bf2c072563..34a2274a2c 100644 --- a/src/tree.js +++ b/src/tree.js @@ -110,10 +110,12 @@ export function createInternal(vnode, parentInternal) { return internal; } +/** @type {(internal: import('./internal').Internal) => boolean} */ const shouldSearchComponent = internal => internal.flags & TYPE_COMPONENT && (!(internal.flags & TYPE_ROOT) || - internal.props._parentDom == getParentDom(internal._parent)); + (internal._parent && + internal.props._parentDom == getParentDom(internal._parent))); /** * Get the next DOM Internal after a given index within a parent Internal. @@ -123,48 +125,65 @@ const shouldSearchComponent = internal => * @returns {import('./internal').PreactNode} */ export function getDomSibling(internal, childIndex) { - // basically looking for the next pointer that can be used to perform an insertBefore: - // @TODO inline the null case, since it's only used in patch. - if (childIndex == null) { - // Use childIndex==null as a signal to resume the search from the vnode's sibling - const next = internal._next; - return next && (getChildDom(next) || getDomSibling(next)); - - // return next && (getChildDom(next) || getDomSibling(next)); - // let sibling = internal; - // while (sibling = sibling._next) { - // let domChildInternal = getChildDom(sibling); - // if (domChildInternal) return domChildInternal; - // } - - // const parent = internal._parent; - // let child = parent._child; - // while (child) { - // if (child === internal) { - // return getDomSibling(child._next); - // } - // child = child._next; - // } + // // basically looking for the next pointer that can be used to perform an insertBefore: + // // @TODO inline the null case, since it's only used in patch. + // if (childIndex == null) { + // // Use childIndex==null as a signal to resume the search from the vnode's sibling + // const next = internal._next; + // return next && (getChildDom(next) || getDomSibling(next)); + + // // return next && (getChildDom(next) || getDomSibling(next)); + // // let sibling = internal; + // // while (sibling = sibling._next) { + // // let domChildInternal = getChildDom(sibling); + // // if (domChildInternal) return domChildInternal; + // // } + + // // const parent = internal._parent; + // // let child = parent._child; + // // while (child) { + // // if (child === internal) { + // // return getDomSibling(child._next); + // // } + // // child = child._next; + // // } + + // // return getDomSibling( + // // internal._parent, + // // internal._parent._children.indexOf(internal) + 1 + // // ); + // } - // return getDomSibling( - // internal._parent, - // internal._parent._children.indexOf(internal) + 1 - // ); - } + // let childDom = getChildDom(internal._child); + // if (childDom) { + // return childDom; + // } - let childDom = getChildDom(internal._child); - if (childDom) { - return childDom; + // // If we get here, we have not found a DOM node in this vnode's children. We + // // must resume from this vnode's sibling (in it's parent _children array). + // // Only climb up and search the parent if we aren't searching through a DOM + // // VNode (meaning we reached the DOM parent of the original vnode that began + // // the search). Note, the top of the tree has _parent == null so avoiding that + // // here. + // return internal._parent && shouldSearchComponent(internal) + // ? getDomSibling(internal) + // : null; + + if (internal._next) { + let childDom = getChildDom(internal._next); + if (childDom) { + return childDom; + } } - // If we get here, we have not found a DOM node in this vnode's children. We - // must resume from this vnode's sibling (in it's parent _children array). - // Only climb up and search the parent if we aren't searching through a DOM - // VNode (meaning we reached the DOM parent of the original vnode that began - // the search). Note, the top of the tree has _parent == null so avoiding that - // here. - return internal._parent && shouldSearchComponent(internal) - ? getDomSibling(internal) + // If we get here, we have not found a DOM node in this internal's siblings so + // we need to search up through the parent's sibling's sub tree for a DOM + // Node, assuming the parent's siblings should be searched. If the parent is + // DOM node or a root node with a different parentDOM, we shouldn't search + // through it since any siblings we'd find wouldn't be siblings of the current + // internal's DOM + return internal._parent && shouldSearchComponent(internal._parent) + ? getDomSibling(internal._parent) : null; } diff --git a/test/browser/getDomSibling.test.js b/test/browser/getDomSibling.test.js index 65ac0832f6..6cde79800e 100644 --- a/test/browser/getDomSibling.test.js +++ b/test/browser/getDomSibling.test.js @@ -4,11 +4,17 @@ import { setupScratch, teardown } from '../_util/helpers'; /** @jsx createElement */ +/** + * @typedef {import('../../src/internal').Internal} Internal + * @typedef {import('../../src/internal').PreactElement} PreactElement + */ + describe('getDomSibling', () => { - /** @type {import('../../src/internal').PreactElement} */ + /** @type {PreactElement} */ let scratch; - const getRoot = dom => dom._children; + /** @type {(dom: PreactElement) => Internal} */ + const getRoot = dom => dom._child; const Root = props => props.children; const createPortal = (vnode, parent) => ( @@ -32,7 +38,7 @@ describe('getDomSibling', () => {
                                                                              , scratch ); - let internal = getRoot(scratch)._children[0]._children[0]; + let internal = getRoot(scratch)._child._child; expect(getDomSibling(internal)).to.equalNode( scratch.firstChild.childNodes[1] ); @@ -45,7 +51,7 @@ describe('getDomSibling', () => {
                              • , scratch ); - let internal = getRoot(scratch)._children[0]._children[0]; + let internal = getRoot(scratch)._child._child; expect(getDomSibling(internal)).to.equalNode( scratch.firstChild.childNodes[1] ); @@ -61,7 +67,24 @@ describe('getDomSibling', () => {
          • , scratch ); - let internal = getRoot(scratch)._children[0]._children[0]; + let internal = getRoot(scratch)._child._child; + expect(getDomSibling(internal)).to.equalNode( + scratch.firstChild.childNodes[1] + ); + }); + + it('should find direct sibling through empty Components', () => { + render( +
            +
            A
            + + +
            B
            +
            , + scratch + ); + let internal = getRoot(scratch)._child._child; + expect(internal.key).to.equal('A'); expect(getDomSibling(internal)).to.equalNode( scratch.firstChild.childNodes[1] ); @@ -69,7 +92,7 @@ describe('getDomSibling', () => { it('should find text node sibling with placeholder', () => { render(
            A{null}B
            , scratch); - let internal = getRoot(scratch)._children[0]._children[0]; + let internal = getRoot(scratch)._child._child; expect(getDomSibling(internal)).to.equalNode( scratch.firstChild.childNodes[1] ); @@ -84,7 +107,7 @@ describe('getDomSibling', () => { , scratch ); - let internal = getRoot(scratch)._children[0]._children[0]; + let internal = getRoot(scratch)._child._child; expect(internal.key).to.equal('A'); expect(getDomSibling(internal)).to.equalNode( scratch.firstChild.childNodes[1] @@ -104,7 +127,7 @@ describe('getDomSibling', () => { , scratch ); - let internal = getRoot(scratch)._children[0]._children[0]._children[0]; + let internal = getRoot(scratch)._child._child._child; expect(internal.key).to.equal('A'); expect(getDomSibling(internal)).to.equalNode( scratch.firstChild.childNodes[1] @@ -121,7 +144,7 @@ describe('getDomSibling', () => { , scratch ); - let internal = getRoot(scratch)._children[0]._children[0]._children[0]; + let internal = getRoot(scratch)._child._child._child; expect(getDomSibling(internal)).to.equalNode( scratch.firstChild.childNodes[1] ); @@ -153,8 +176,7 @@ describe('getDomSibling', () => { scratch ); - let divAInternal = getRoot(scratch)._children[0]._children[0]._children[0] - ._children[0]._children[0]; + let divAInternal = getRoot(scratch)._child._child._child._child._child; expect(divAInternal.type).to.equal('div'); expect(divAInternal.key).to.equal('A'); expect(getDomSibling(divAInternal)).to.equalNode( @@ -181,8 +203,7 @@ describe('getDomSibling', () => { scratch ); - let fragment = getRoot(scratch)._children[0]._children[0]._children[0] - ._children[0]; + let fragment = getRoot(scratch)._child._child._child._child; expect(fragment.type).to.equal(Fragment); expect(fragment.key).to.equal('0.0.0.0'); expect(getDomSibling(fragment)).to.equalNode( @@ -210,8 +231,7 @@ describe('getDomSibling', () => { scratch ); - let foo = getRoot(scratch)._children[0]._children[0]._children[0] - ._children[0]; + let foo = getRoot(scratch)._child._child._child._child; expect(foo.type).to.equal(Foo); expect(foo.key).to.equal('0.0.0.0'); expect(getDomSibling(foo)).to.equalNode(scratch.firstChild.childNodes[1]); @@ -234,7 +254,7 @@ describe('getDomSibling', () => { scratch ); - let divAInternal = getRoot(scratch)._children[0]._children[0]._children[0]; + let divAInternal = getRoot(scratch)._child._child._child; expect(divAInternal.type).to.equal('div'); expect(divAInternal.key).to.equal('A'); expect(getDomSibling(divAInternal)).to.equalNode( @@ -254,7 +274,7 @@ describe('getDomSibling', () => { scratch ); - let divAInternal = getRoot(scratch)._children[0]._children[0]; + let divAInternal = getRoot(scratch)._child._child; expect(divAInternal.type).to.equal('div'); expect(divAInternal.key).to.equal('A'); @@ -272,7 +292,7 @@ describe('getDomSibling', () => { scratch ); - let divAInternal = getRoot(scratch)._children[0]._children[0]; + let divAInternal = getRoot(scratch)._child._child; expect(divAInternal.type).to.equal('div'); expect(divAInternal.key).to.equal('A'); @@ -291,7 +311,7 @@ describe('getDomSibling', () => { scratch ); - let divAInternal = getRoot(scratch)._children[0]._children[0]._children[0]; + let divAInternal = getRoot(scratch)._child._child._child; expect(divAInternal.key).to.equal('A'); let sibling = getDomSibling(divAInternal); @@ -314,8 +334,7 @@ describe('getDomSibling', () => { scratch ); - const divCInternal = getRoot(scratch)._children[0]._children[2] - ._children[0]; + const divCInternal = getRoot(scratch)._child._child._next._next._child; expect(divCInternal.key).to.equal('C'); expect(getDomSibling(divCInternal)).to.equal(null); }); @@ -337,8 +356,7 @@ describe('getDomSibling', () => { scratch ); - let divAInternal = getRoot(scratch)._children[0]._children[0]._children[0] - ._children[0]._children[0]; + let divAInternal = getRoot(scratch)._child._child._child._child._child; expect(divAInternal.key).to.equal('A'); expect(getDomSibling(divAInternal)).to.equal(null); }); @@ -367,8 +385,7 @@ describe('getDomSibling', () => { scratch ); - let divAInternal = getRoot(scratch)._children[0]._children[0]._children[0] - ._children[0]._children[0]; + let divAInternal = getRoot(scratch)._child._child._child._child._child; expect(divAInternal.key).to.equal('A'); expect(getDomSibling(divAInternal)).to.equal(null); }); @@ -389,7 +406,7 @@ describe('getDomSibling', () => { scratch ); - let divAInternal = getRoot(scratch)._children[0]._children[0]._children[1]; + let divAInternal = getRoot(scratch)._child._child._child._next; expect(divAInternal.key).to.equal('A'); expect(getDomSibling(divAInternal)).to.equal(null); }); @@ -397,7 +414,7 @@ describe('getDomSibling', () => { it("should return null if it's the only child", () => { render(
            A
            , scratch); - let internal = getRoot(scratch)._children[0]; + let internal = getRoot(scratch)._child; expect(internal.key).to.equal('A'); expect(getDomSibling(internal)).to.be.null; }); @@ -418,7 +435,7 @@ describe('getDomSibling', () => { scratch ); - let internal = getRoot(scratch)._children[0]._children[0]._children[0]; + let internal = getRoot(scratch)._child._child._child; expect(internal.key).to.equal('A'); expect(getDomSibling(internal)).to.equalNode( scratch.firstChild.childNodes[1] @@ -439,7 +456,7 @@ describe('getDomSibling', () => { scratch ); - let internal = getRoot(scratch)._children[0]._children[0]._children[0]; + let internal = getRoot(scratch)._child._child._child; expect(internal.key).to.equal('A'); expect(getDomSibling(internal)).to.equalNode(scratch.childNodes[1]); }); @@ -458,7 +475,7 @@ describe('getDomSibling', () => { scratch ); - let internal = getRoot(scratch)._children[0]._children[0]._children[0]; + let internal = getRoot(scratch)._child._child._child; expect(internal.key).to.equal('A'); expect(getDomSibling(internal)).to.equalNode(scratch.childNodes[1]); }); @@ -479,7 +496,8 @@ describe('getDomSibling', () => { scratch ); - let internal = getRoot(scratch)._children[0]._children[1]; + let internal = getRoot(scratch)._child._child._next; + expect(internal.type).to.equal(Root); expect(internal.props._parentDom).to.equal(portalParent); expect(getDomSibling(internal)).to.equalNode(scratch.firstChild.lastChild); }); @@ -498,7 +516,8 @@ describe('getDomSibling', () => { scratch ); - let internal = getRoot(scratch)._children[0]._children[1]; + let internal = getRoot(scratch)._child._child._next; + expect(internal.type).to.equal(Root); expect(internal.props._parentDom).to.equal(scratch); expect(getDomSibling(internal)).to.equalNode(scratch.lastChild); }); @@ -519,7 +538,7 @@ describe('getDomSibling', () => { scratch ); - let internal = getRoot(scratch)._children[0]._children[1]._children[0]; + let internal = getRoot(scratch)._child._child._next._child; expect(internal.key).to.equal('B'); expect(getDomSibling(internal)).to.equal(null); }); @@ -548,7 +567,7 @@ describe('getDomSibling', () => { scratch ); - let internal = getRoot(scratch)._children[0]._children[1]._children[0]; + let internal = getRoot(scratch)._child._child._next._child; expect(internal.key).to.equal('B'); expect(getDomSibling(internal)).to.equal(portalParent.lastChild); }); @@ -567,7 +586,10 @@ describe('getDomSibling', () => { scratch ); - let internal = getRoot(scratch)._children[0]._children[1]._children[0]; + let portalInternal = getRoot(scratch)._child._child._next; + let internal = portalInternal._child; + expect(portalInternal.type).to.equal(Root); + expect(portalInternal.props._parentDom).to.equalNode(scratch); expect(internal.key).to.equal('B'); expect(getDomSibling(internal)).to.equalNode(scratch.lastChild); }); diff --git a/test/browser/keys.test.js b/test/browser/keys.test.js index 05c2176894..3b21438fac 100644 --- a/test/browser/keys.test.js +++ b/test/browser/keys.test.js @@ -6,7 +6,7 @@ import { div } from '../_util/dom'; /** @jsx createElement */ -describe.only('keys', () => { +describe('keys', () => { /** @type {HTMLDivElement} */ let scratch; From 7fd9f591afd3b53b593b6fc919e084f023cb85d0 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 28 Jan 2023 17:42:39 -0800 Subject: [PATCH 23/71] Rename getChildDom to getFirstDom --- compat/src/index.js | 4 ++-- src/diff/children.js | 2 +- src/tree.js | 16 +++++++++------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/compat/src/index.js b/compat/src/index.js index b3df85bd20..86d2c7237d 100644 --- a/compat/src/index.js +++ b/compat/src/index.js @@ -32,7 +32,7 @@ import { REACT_ELEMENT_TYPE, __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from './render'; -import { getChildDom } from '../../src/tree'; +import { getFirstDom } from '../../src/tree'; export * from './scheduler'; const version = '17.0.2'; // trick libraries to think we are react @@ -93,7 +93,7 @@ function findDOMNode(component) { return null; } - return getChildDom(component._internal._child, 0); + return getFirstDom(component._internal._child, 0); } /** diff --git a/src/diff/children.js b/src/diff/children.js index aa9b1884cb..de0c11d620 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -14,7 +14,7 @@ import { import { mount } from './mount'; import { patch } from './patch'; import { unmount } from './unmount'; -import { createInternal, getChildDom, getDomSibling } from '../tree'; +import { createInternal, getDomSibling } from '../tree'; /** * Scenarios: diff --git a/src/tree.js b/src/tree.js index 34a2274a2c..8816ba0b88 100644 --- a/src/tree.js +++ b/src/tree.js @@ -170,7 +170,7 @@ export function getDomSibling(internal, childIndex) { // : null; if (internal._next) { - let childDom = getChildDom(internal._next); + let childDom = getFirstDom(internal._next); if (childDom) { return childDom; } @@ -188,13 +188,15 @@ export function getDomSibling(internal, childIndex) { } /** - * Get the root DOM element for a given subtree. - * Returns the nearest DOM element within a given Internal's subtree. - * If the provided Internal _is_ a DOM Internal, its DOM will be returned. - * @param {import('./internal').Internal} internal The internal to begin the search + * Search the given internal and it's siblings for a DOM element. If the + * provided Internal _is_ a DOM Internal, its DOM will be returned. If you want + * to find the first DOM node of an Internal's subtree, than pass in + * `internal._child` + * @param {import('./internal').Internal} internal The internal to begin the + * search * @returns {import('./internal').PreactElement} */ -export function getChildDom(internal) { +export function getFirstDom(internal) { while (internal) { // this is a DOM internal if (internal.flags & TYPE_DOM) { @@ -209,7 +211,7 @@ export function getChildDom(internal) { !(internal.flags & TYPE_ROOT) || internal.props._parentDom == getParentDom(internal._parent) ) { - const childDom = getChildDom(internal._child); + const childDom = getFirstDom(internal._child); if (childDom) return childDom; } From d759b08da45c4d87a6ec85bc508c20fac524991a Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 29 Jan 2023 10:55:19 -0800 Subject: [PATCH 24/71] Skip over non-renderable children in insertion loop --- src/constants.js | 1 + src/diff/children.js | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/constants.js b/src/constants.js index 14d7e26b88..ea627adba7 100644 --- a/src/constants.js +++ b/src/constants.js @@ -66,4 +66,5 @@ export const RESET_MODE = ~( export const INHERITED_MODES = MODE_HYDRATE | MODE_MUTATIVE_HYDRATE | MODE_SVG; export const EMPTY_ARR = []; +/** @type {undefined} */ export const UNDEFINED = undefined; diff --git a/src/diff/children.js b/src/diff/children.js index de0c11d620..9867180b23 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -191,8 +191,13 @@ export function patchChildren(parentInternal, children, parentDom) { mount(internal, parentDom, getDomSibling(internal)); insert(internal, parentDom); } else { - // TODO: Skip over non-renderable vnodes - patch(internal, children[index], parentDom); + let vnode = children[index]; + while (vnode == null || vnode === true || vnode === false) { + index--; + vnode = children[index]; + } + + patch(internal, vnode, parentDom); if (internal.flags & INSERT_INTERNAL) { insert(internal, parentDom); } From 682399b77cd3cec765b5034516b302917e726cf9 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 29 Jan 2023 10:55:35 -0800 Subject: [PATCH 25/71] Apply refs in insertion loop --- src/diff/children.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/diff/children.js b/src/diff/children.js index 9867180b23..b2d301fd97 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -207,6 +207,13 @@ export function patchChildren(parentInternal, children, parentDom) { internal.flags &= ~INSERT_INTERNAL; + let oldRef = internal._prevRef; + if (internal.ref != oldRef) { + if (oldRef) applyRef(oldRef, null, internal); + if (internal.ref) + applyRef(internal.ref, internal._component || internal.data, internal); + } + // TODO: this index should match the index of the matching vnode. So any // skipping over of non-renderables should move this index accordingly. // Doing this should help with null placeholders and enable us to skip the From 0576c457e68a6d07625f253687cf1705214a9878 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 29 Jan 2023 10:59:27 -0800 Subject: [PATCH 26/71] Check if any children exist before running LDS --- src/diff/children.js | 74 +++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index b2d301fd97..39624d5dae 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -123,47 +123,49 @@ export function patchChildren(parentInternal, children, parentDom) { // TODO: Replace _prevLDS with _next. Doing this will make _next meaningless for a moment // TODO: Explore trying to do this without an array, maybe next pointers? Or maybe reuse the array internal = prevInternal; - /** @type {Internal[]} */ - const wipLDS = [internal]; - internal.flags |= INSERT_INTERNAL; - - while ((internal = internal._prev) !== parentInternal) { - // Mark all internals as requiring insertion. We will clear this flag for - // internals on longest decreasing subsequence + if (internal) { + /** @type {Internal[]} */ + const wipLDS = [internal]; internal.flags |= INSERT_INTERNAL; - // Skip over newly mounted internals. They will be mounted in place. - if (internal._index === -1) continue; + while ((internal = internal._prev) !== parentInternal) { + // Mark all internals as requiring insertion. We will clear this flag for + // internals on longest decreasing subsequence + internal.flags |= INSERT_INTERNAL; - let ldsTail = wipLDS[wipLDS.length - 1]; - if (ldsTail._index > internal._index) { - internal._prevLDS = ldsTail; - wipLDS.push(internal); - } else { - // Search for position in wipLIS where node should go. It should replace - // the first node where node > wip[i] (though keep in mind, we are - // iterating over the list backwards). Example: - // ``` - // wipLIS = [4,3,1], node = 2. - // Node should replace 1: [4,3,2] - // ``` - let i = wipLDS.length; - // TODO: Binary search? - while (--i >= 0 && wipLDS[i]._index < internal._index) {} - - wipLDS[i + 1] = internal; - let prevLDS = i < 0 ? null : wipLDS[i]; - internal._prevLDS = prevLDS; + // Skip over newly mounted internals. They will be mounted in place. + if (internal._index === -1) continue; + + let ldsTail = wipLDS[wipLDS.length - 1]; + if (ldsTail._index > internal._index) { + internal._prevLDS = ldsTail; + wipLDS.push(internal); + } else { + // Search for position in wipLIS where node should go. It should replace + // the first node where node > wip[i] (though keep in mind, we are + // iterating over the list backwards). Example: + // ``` + // wipLIS = [4,3,1], node = 2. + // Node should replace 1: [4,3,2] + // ``` + let i = wipLDS.length; + // TODO: Binary search? + while (--i >= 0 && wipLDS[i]._index < internal._index) {} + + wipLDS[i + 1] = internal; + let prevLDS = i < 0 ? null : wipLDS[i]; + internal._prevLDS = prevLDS; + } } - } - // Step 4. Mark internals in longest decreasing subsequence - /** @type {Internal | null} */ - let ldsNode = wipLDS[wipLDS.length - 1]; - while (ldsNode) { - // This node is on the longest decreasing subsequence so clear INSERT_NODE flag - ldsNode.flags &= ~INSERT_INTERNAL; - ldsNode = ldsNode._prevLDS; + // Step 4. Mark internals in longest decreasing subsequence + /** @type {Internal | null} */ + let ldsNode = wipLDS[wipLDS.length - 1]; + while (ldsNode) { + // This node is on the longest decreasing subsequence so clear INSERT_NODE flag + ldsNode.flags &= ~INSERT_INTERNAL; + ldsNode = ldsNode._prevLDS; + } } // Step 5. Walk backwards over the newly-assigned _prev properties, visiting From a43e55909e7db6726fdd77faeea4c1a55356e569 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 30 Jan 2023 10:56:50 -0800 Subject: [PATCH 27/71] Fix updating child pointer on parent internal --- src/diff/children.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/diff/children.js b/src/diff/children.js index 39624d5dae..64005534ca 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -172,6 +172,7 @@ export function patchChildren(parentInternal, children, parentDom) { // each Internal to set its _next ptr and perform insert/mount/update. /** @type {Internal} */ let nextInternal = null; + let firstChild = null; let index = children.length; internal = prevInternal; @@ -205,7 +206,7 @@ export function patchChildren(parentInternal, children, parentDom) { } } - if (!prevInternal) parentInternal._child = internal; + if (!prevInternal) firstChild = internal; internal.flags &= ~INSERT_INTERNAL; @@ -223,6 +224,8 @@ export function patchChildren(parentInternal, children, parentDom) { internal._index = index; internal = prevInternal; } + + parentInternal._child = firstChild; } /* export function patchChildren(internal, children, parentDom) { From 4ee048353a25d807c3d8b0046c9400cbc0e32a9e Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 30 Jan 2023 11:01:11 -0800 Subject: [PATCH 28/71] Fix forwardRef --- compat/src/forwardRef.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/compat/src/forwardRef.js b/compat/src/forwardRef.js index f72a011b49..4e881e0239 100644 --- a/compat/src/forwardRef.js +++ b/compat/src/forwardRef.js @@ -2,9 +2,8 @@ import { options } from 'preact'; let oldDiffHook = options._diff; options._diff = (internal, vnode) => { - if (internal.type && internal.type._forwarded && vnode.ref) { - vnode.props.ref = vnode.ref; - vnode.ref = null; + if (internal.type && internal.type._forwarded && internal.ref) { + internal.props.ref = internal.ref; internal.ref = null; } if (oldDiffHook) oldDiffHook(internal, vnode); From f9aaf2c75a0d35aaddf57d56178b285193e93cad Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 30 Jan 2023 11:21:55 -0800 Subject: [PATCH 29/71] Convert nested arrays into Fragments --- src/diff/children.js | 15 ++++++++++----- src/diff/mount.js | 8 ++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 64005534ca..ed595c2b11 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -1,5 +1,5 @@ import { applyRef } from './refs'; -import { normalizeToVNode } from '../create-element'; +import { createElement, Fragment, normalizeToVNode } from '../create-element'; import { TYPE_COMPONENT, TYPE_TEXT, @@ -30,7 +30,7 @@ import { createInternal, getDomSibling } from '../tree'; /** * Update an internal with new children. * @param {Internal} parentInternal The internal whose children should be patched - * @param {import('../internal').ComponentChild[]} children The new children, represented as VNodes + * @param {import('../internal').ComponentChildren[]} children The new children, represented as VNodes * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered */ export function patchChildren(parentInternal, children, parentDom) { @@ -59,9 +59,10 @@ export function patchChildren(parentInternal, children, parentDom) { typeFlag = TYPE_TEXT; normalizedVNode += ''; } else { - type = vnode.type; + const isArray = Array.isArray(vnode); + type = isArray ? Fragment : vnode.type; typeFlag = typeof type === 'function' ? TYPE_COMPONENT : TYPE_ELEMENT; - key = vnode.key; + key = isArray ? null : vnode.key; } /** @type {Internal?} */ @@ -200,7 +201,11 @@ export function patchChildren(parentInternal, children, parentDom) { vnode = children[index]; } - patch(internal, vnode, parentDom); + patch( + internal, + Array.isArray(vnode) ? createElement(Fragment, null, vnode) : vnode, + parentDom + ); if (internal.flags & INSERT_INTERNAL) { insert(internal, parentDom); } diff --git a/src/diff/mount.js b/src/diff/mount.js index 34571c7f40..e17d20deeb 100644 --- a/src/diff/mount.js +++ b/src/diff/mount.js @@ -13,7 +13,7 @@ import { MODE_SVG, DIRTY_BIT } from '../constants'; -import { normalizeToVNode, Fragment } from '../create-element'; +import { normalizeToVNode, createElement, Fragment } from '../create-element'; import { setProperty } from './props'; import { createInternal, getParentContext } from '../tree'; import options from '../options'; @@ -238,10 +238,10 @@ export function mountChildren(parentInternal, children, parentDom, startDom) { // account for holes by incrementing the index: if (vnode == null || vnode === true || vnode === false) continue; + else if (Array.isArray(vnode)) vnode = createElement(Fragment, null, vnode); + else if (typeof vnode !== 'object') vnode = String(vnode); - const normalizedVNode = typeof vnode === 'object' ? vnode : String(vnode); - - internal = createInternal(normalizedVNode, parentInternal); + internal = createInternal(vnode, parentInternal); internal._index = i; if (prevInternal) prevInternal._next = internal; From 6ccd7e9754842beb21e95de9cb2b2bf04d560564 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 30 Jan 2023 11:32:19 -0800 Subject: [PATCH 30/71] Properly cleanup DOM in portal tests --- test/browser/portals.test.js | 43 +++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/test/browser/portals.test.js b/test/browser/portals.test.js index bbf37d5d43..b4bf1c33dc 100644 --- a/test/browser/portals.test.js +++ b/test/browser/portals.test.js @@ -22,13 +22,28 @@ describe('Portal', () => { let resetRemoveChild; let resetRemove; + let containers; + /** @type {(type: string) => Element} */ + function createContainer(type) { + const container = document.createElement(type); + containers.push(container); + return container; + } + beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); + + containers = []; }); afterEach(() => { teardown(scratch); + + for (let container of containers) { + container.remove(); + } + containers = null; }); before(() => { @@ -46,7 +61,7 @@ describe('Portal', () => { }); it('should render into a different root node', () => { - let root = document.createElement('div'); + let root = createContainer('div'); document.body.appendChild(root); function Foo(props) { @@ -60,10 +75,10 @@ describe('Portal', () => { }); it('should preserve mount order of non-portal siblings', () => { - let portals = document.createElement('portals'); + let portals = createContainer('portals'); scratch.appendChild(portals); - let main = document.createElement('main'); + let main = createContainer('main'); scratch.appendChild(main); function Foo(props) { @@ -99,10 +114,10 @@ describe('Portal', () => { }); it('should preserve hydration order of non-portal siblings', () => { - let portals = document.createElement('portals'); + let portals = createContainer('portals'); scratch.appendChild(portals); - let main = document.createElement('main'); + let main = createContainer('main'); scratch.appendChild(main); main.innerHTML = '

            A

            C

            E
            '; @@ -246,8 +261,8 @@ describe('Portal', () => { }); it('should not render for Portal nodes', () => { - let root = document.createElement('div'); - let dialog = document.createElement('div'); + let root = createContainer('div'); + let dialog = createContainer('div'); dialog.id = 'container'; scratch.appendChild(root); @@ -266,8 +281,8 @@ describe('Portal', () => { }); it('should unmount Portal', () => { - let root = document.createElement('div'); - let dialog = document.createElement('div'); + let root = createContainer('div'); + let dialog = createContainer('div'); dialog.id = 'container'; scratch.appendChild(root); @@ -612,8 +627,8 @@ describe('Portal', () => { }); it('should not unmount when parent renders', () => { - let root = document.createElement('div'); - let dialog = document.createElement('div'); + let root = createContainer('div'); + let dialog = createContainer('div'); dialog.id = 'container'; scratch.appendChild(root); @@ -793,6 +808,8 @@ describe('Portal', () => { }); it('should order complex effects well', () => { + const container = createContainer('div'); + const calls = []; const Parent = ({ children, isPortal }) => { useEffect(() => { @@ -819,7 +836,7 @@ describe('Portal', () => { calls.push('Portal'); }, []); - return createPortal({content}, document.body); + return createPortal({content}, container); }; const App = () => { @@ -853,7 +870,7 @@ describe('Portal', () => { }); it('should include containerInfo', () => { - let root = document.createElement('div'); + let root = createContainer('div'); document.body.appendChild(root); const A = () => A; From 1e3f6946bdfbcc92d07789433340541f5408b435 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 30 Jan 2023 15:00:11 -0800 Subject: [PATCH 31/71] Fix handling errors while unmounting --- src/diff/children.js | 2 +- src/diff/unmount.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index ed595c2b11..d22fd635e7 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -110,7 +110,7 @@ export function patchChildren(parentInternal, children, parentDom) { if (oldHead._prev == null) { if (lastMatchedInternal) lastMatchedInternal._next = next; // else parentInternal._child = next; - unmount(oldHead, parentInternal, 0); + unmount(oldHead, oldHead, 0); } else { lastMatchedInternal = oldHead; } diff --git a/src/diff/unmount.js b/src/diff/unmount.js index 639e847266..84793a09f3 100644 --- a/src/diff/unmount.js +++ b/src/diff/unmount.js @@ -7,19 +7,19 @@ import { ENABLE_CLASSES } from '../component'; /** * Unmount a virtual node from the tree and apply DOM changes * @param {import('../internal').Internal} internal The virtual node to unmount - * @param {import('../internal').Internal} parentInternal The parent of the VNode that - * initiated the unmount + * @param {import('../internal').Internal} topUnmountedInternal The top of the + * subtree that is being unmounted * @param {number} [skipRemove] Flag that indicates that a parent node of the * current element is already detached from the DOM. */ -export function unmount(internal, parentInternal, skipRemove) { +export function unmount(internal, topUnmountedInternal, skipRemove) { let r, i = 0; if (options.unmount) options.unmount(internal); internal.flags |= MODE_UNMOUNTING; if ((r = internal.ref)) { - applyRef(r, null, parentInternal); + applyRef(r, null, topUnmountedInternal); } if ((r = internal._component)) { @@ -29,7 +29,7 @@ export function unmount(internal, parentInternal, skipRemove) { try { r.componentWillUnmount(); } catch (e) { - options._catchError(e, parentInternal); + options._catchError(e, topUnmountedInternal); } } } @@ -38,7 +38,7 @@ export function unmount(internal, parentInternal, skipRemove) { while (r) { unmount( r, - parentInternal, + topUnmountedInternal, skipRemove ? ~internal.flags & TYPE_ROOT : internal.flags & TYPE_DOM ); r = r._next; From 560d4036d6735a13bfbe00fee7714668ec26f97b Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 30 Jan 2023 16:43:29 -0800 Subject: [PATCH 32/71] Correctly mount new DOM nodes while patching --- src/diff/children.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/diff/children.js b/src/diff/children.js index d22fd635e7..e454dac8b3 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -193,7 +193,13 @@ export function patchChildren(parentInternal, children, parentDom) { if (internal._index === -1) { mount(internal, parentDom, getDomSibling(internal)); - insert(internal, parentDom); + if (internal.flags & TYPE_DOM) { + // If we are mounting a component, it's DOM children will get inserted + // into the DOM in mountChildren. If we are mounting a DOM node, then + // it's children will be mounted into itself and we need to insert this + // DOM in place. + insert(internal, parentDom); + } } else { let vnode = children[index]; while (vnode == null || vnode === true || vnode === false) { From 9fe14d707a51f92ee0d48850ccb7656736534069 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 30 Jan 2023 16:00:30 -0800 Subject: [PATCH 33/71] Skip over root nodes with different parentDOMs when inserting --- src/diff/children.js | 33 +++++++++++++++- test/browser/portals.test.js | 73 ++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index e454dac8b3..b1af4d1ff3 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -9,7 +9,8 @@ import { TYPE_DOM, UNDEFINED, TYPE_ELEMENT, - INSERT_INTERNAL + INSERT_INTERNAL, + TYPE_ROOT } from '../constants'; import { mount } from './mount'; import { patch } from './patch'; @@ -125,17 +126,45 @@ export function patchChildren(parentInternal, children, parentDom) { // TODO: Explore trying to do this without an array, maybe next pointers? Or maybe reuse the array internal = prevInternal; if (internal) { + // TODO: Argh, we need to skip this first internal if it is a TYPE_ROOT node with a different parentDOM :/ /** @type {Internal[]} */ const wipLDS = [internal]; internal.flags |= INSERT_INTERNAL; while ((internal = internal._prev) !== parentInternal) { + // Skip over Root nodes whose parentDOM is different from the current + // parentDOM (aka Portals). Don't mark them for insertion since the + // recursive calls to mountChildren/patchChildren will handle + // mounting/inserting any DOM nodes under the root node. + // + // If a root node's parentDOM is the same as the current parentDOM then + // treat it as an unkeyed fragment and prepare it for moving or insertion + // if necessary. + // + // TODO: Consider the case where a root node has the same parent, goes + // into a different parent, a new node is inserted before the portal, and + // then the portal goes back to the original parent. Do we correctly + // insert the portal into the right place? Currently yes, because the + // beginning of patch calls insert whenever parentDom changes. Could we + // move that logic here? + // + // TODO: We do the props._parentDom !== parentDom in a couple places. + // Could we do this check once and cache the result in a flag? + if ( + internal.flags & TYPE_ROOT && + internal.props._parentDom !== parentDom + ) { + continue; + } + // Mark all internals as requiring insertion. We will clear this flag for // internals on longest decreasing subsequence internal.flags |= INSERT_INTERNAL; // Skip over newly mounted internals. They will be mounted in place. - if (internal._index === -1) continue; + if (internal._index === -1) { + continue; + } let ldsTail = wipLDS[wipLDS.length - 1]; if (ldsTail._index > internal._index) { diff --git a/test/browser/portals.test.js b/test/browser/portals.test.js index b4bf1c33dc..d6f4231cd2 100644 --- a/test/browser/portals.test.js +++ b/test/browser/portals.test.js @@ -891,4 +891,77 @@ describe('Portal', () => { root.parentNode.removeChild(root); }); + + it('should preserve portal behavior when moving nodes around a portal', () => { + const portalRoot = createContainer('div'); + document.body.appendChild(portalRoot); + + render( + [ +
            A
            , +
            B
            , +
            C
            , +
            D
            , + createPortal(
            Portal
            , portalRoot) + ], + scratch + ); + + expect(scratch.innerHTML).to.equal( + '
            A
            B
            C
            D
            ' + ); + expect(portalRoot.innerHTML).to.equal('
            Portal
            '); + + render( + [ +
            A
            , +
            B
            , + createPortal(
            Portal
            , portalRoot), +
            C
            , +
            D
            + ], + scratch + ); + + expect(scratch.innerHTML).to.equal( + '
            A
            B
            C
            D
            ' + ); + expect(portalRoot.innerHTML).to.equal('
            Portal
            '); + }); + + it('should insert a portal before new siblings when changing container to match siblings', () => { + const portalRoot = createContainer('div'); + document.body.appendChild(portalRoot); + + render( + [ +
            A
            , +
            B
            , + createPortal(
            Portal
            , portalRoot), +
            D
            + ], + scratch + ); + + expect(scratch.innerHTML).to.equal('
            A
            B
            D
            '); + expect(portalRoot.innerHTML).to.equal('
            Portal
            '); + + render( + [ +
            A
            , +
            B
            , + // Change container to match siblings container + createPortal(
            Portal
            , scratch), + // While adding a new sibling +
            C
            , +
            D
            + ], + scratch + ); + + expect(scratch.innerHTML).to.equal( + '
            A
            B
            Portal
            C
            D
            ' + ); + expect(portalRoot.innerHTML).to.equal(''); + }); }); From 1e22b2227b01f1ebffe0b75b823276ad12dd04af Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Tue, 31 Jan 2023 01:31:56 -0800 Subject: [PATCH 34/71] Properly account and track null placeholders --- src/diff/children.js | 87 +++++++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 30 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index b1af4d1ff3..20a0b65f3a 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -10,7 +10,8 @@ import { UNDEFINED, TYPE_ELEMENT, INSERT_INTERNAL, - TYPE_ROOT + TYPE_ROOT, + MODE_UNMOUNTING } from '../constants'; import { mount } from './mount'; import { patch } from './patch'; @@ -35,20 +36,32 @@ import { createInternal, getDomSibling } from '../tree'; * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered */ export function patchChildren(parentInternal, children, parentDom) { - /** @type {Internal | undefined} */ - let internal; + /** @type {Internal} */ + let internal = parentInternal._child; /** @type {Internal} */ let prevInternal; // let newTail; - let oldHead = parentInternal._child; + let oldHead = internal; // let skewedOldHead = oldHead; - // Step 1. Find matches and set up _prev pointers + // Step 1. Find matches and set up _prev pointers. Identity null placeholders + // by also walking the old internal children at the same time for (let index = 0; index < children.length; index++) { const vnode = children[index]; // holes get accounted for in the index property: - if (vnode == null || vnode === true || vnode === false) continue; + if (vnode == null || vnode === true || vnode === false) { + if (internal && index == internal._index && internal.key == null) { + // The current internal is unkeyed, has the same index as this VNode + // child, and the VNode is now null. So we'll unmount the Internal and + // treat this slot in the children array as a null placeholder. We mark + // this node as unmounting to prevent it from being used in future + // searches for matching internals + internal.flags |= MODE_UNMOUNTING; + internal = internal._next; + } + continue; + } let type = null; let typeFlag = 0; @@ -67,40 +80,54 @@ export function patchChildren(parentInternal, children, parentDom) { } /** @type {Internal?} */ - let internal; - - // seek forward through the Internals list, starting at the head (either first, or first unused). - // only match unused items, which are internals where _prev === undefined. - // note: _prev=null for the first matched internal, and should be considered "used". - let match = oldHead; - while (match) { - const flags = match.flags; - const isUnused = match._prev == null; - if ( - isUnused && - (flags & typeFlag) !== 0 && - match.type === type && - match.key == key - ) { - internal = match; - // if the match was the first unused item, bump the start ptr forward: - if (match === oldHead) oldHead = oldHead._next; - break; + let matchedInternal; + + // TODO: See if doing a fast path (special if condition) for already in + // place matches is faster than while loop + if (key == null && internal && index < internal._index) { + // If we are doing an unkeyed diff, and the old index of the current + // internal is greater than the current VNode index, then this vnode + // represents a new element that is mounting into what was previous a null + // placeholder slot. We should create a new internal to mount this VNode. + } else { + // seek forward through the Internals list, starting at the head (either first, or first unused). + // only match unused items, which are internals where _prev === undefined. + // note: _prev=null for the first matched internal, and should be considered "used". + let search = oldHead; + while (search) { + const flags = search.flags; + const isUnused = + search._prev == null && ~search.flags & MODE_UNMOUNTING; + if ( + isUnused && + (flags & typeFlag) !== 0 && + search.type === type && + search.key == key + ) { + matchedInternal = search; + // if the match was the first unused item, bump the start ptr forward: + if (search === oldHead) oldHead = oldHead._next; + break; + } + search = search._next; } - match = match._next; } // no match, create a new Internal: - if (!internal) { - internal = createInternal(normalizedVNode, parentInternal); + if (!matchedInternal) { + matchedInternal = createInternal(normalizedVNode, parentInternal); // console.log('creating new', internal.type); } else { // console.log('updating', internal.type); // patch(internal, vnode, parentDom); } - internal._prev = prevInternal || parentInternal; - prevInternal = internal; + matchedInternal._prev = prevInternal || parentInternal; + prevInternal = matchedInternal; + + if (internal && internal._index == index) { + internal = internal._next; + } } // Step 2. Walk over the unused children and unmount: From 5db19bd6dc04b51ec525591bfae7a44c5742577a Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Tue, 31 Jan 2023 01:32:07 -0800 Subject: [PATCH 35/71] Fix forward ref, pt. 2 --- compat/src/forwardRef.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/compat/src/forwardRef.js b/compat/src/forwardRef.js index 4e881e0239..75be7f26d6 100644 --- a/compat/src/forwardRef.js +++ b/compat/src/forwardRef.js @@ -2,9 +2,18 @@ import { options } from 'preact'; let oldDiffHook = options._diff; options._diff = (internal, vnode) => { - if (internal.type && internal.type._forwarded && internal.ref) { - internal.props.ref = internal.ref; - internal.ref = null; + if ( + internal.type && + internal.type._forwarded && + (internal.ref || (vnode && vnode.ref)) + ) { + if (vnode) { + vnode.props.ref = vnode.ref; + vnode.ref = null; + } else { + internal.props.ref = internal.ref; + internal.ref = null; + } } if (oldDiffHook) oldDiffHook(internal, vnode); }; From 6e6a5d6ae5be13e60c309cb6193af024d0d4fb65 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Tue, 31 Jan 2023 14:47:06 -0800 Subject: [PATCH 36/71] Properly skip over non-renderable children in insertion loop --- src/diff/children.js | 11 ++--- test/browser/placeholders.test.js | 68 +++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 20a0b65f3a..fd5bef69c4 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -238,7 +238,10 @@ export function patchChildren(parentInternal, children, parentDom) { internal._next = nextInternal; nextInternal = internal; - index--; + let vnode = children[--index]; + while (vnode == null || vnode === true || vnode === false) { + vnode = children[--index]; + } prevInternal = internal._prev; @@ -257,12 +260,6 @@ export function patchChildren(parentInternal, children, parentDom) { insert(internal, parentDom); } } else { - let vnode = children[index]; - while (vnode == null || vnode === true || vnode === false) { - index--; - vnode = children[index]; - } - patch( internal, Array.isArray(vnode) ? createElement(Fragment, null, vnode) : vnode, diff --git a/test/browser/placeholders.test.js b/test/browser/placeholders.test.js index e69cad8554..5c0e3c4fae 100644 --- a/test/browser/placeholders.test.js +++ b/test/browser/placeholders.test.js @@ -304,4 +304,72 @@ describe('null placeholders', () => { expect(scratch.innerHTML).to.equal(div([div('false'), div('the middle')])); expect(getLog()).to.deep.equal(['#text.remove()', '#text.remove()']); }); + + it('should properly mount & unmount null placeholders from the start', () => { + /** @type {(props: { show: [boolean, boolean, boolean] }) => any} */ + function App({ show }) { + return ( +
              + {show[0] &&
            1. first
            2. } + {show[1] &&
            3. middle
            4. } + {show[2] &&
            5. last
            6. } +
            + ); + } + + render(, scratch); + expect(scratch.innerHTML).to.equal('
            1. last
            '); + + render(, scratch); + expect(scratch.innerHTML).to.equal( + '
            1. middle
            2. last
            ' + ); + + render(, scratch); + expect(scratch.innerHTML).to.equal( + '
            1. first
            2. middle
            3. last
            ' + ); + + render(, scratch); + expect(scratch.innerHTML).to.equal( + '
            1. middle
            2. last
            ' + ); + + render(, scratch); + expect(scratch.innerHTML).to.equal('
            1. last
            '); + }); + + it('should properly mount && unmount null placeholders from the back', () => { + /** @type {(props: { show: [boolean, boolean, boolean] }) => any} */ + function App({ show }) { + return ( +
              + {show[0] &&
            1. first
            2. } + {show[1] &&
            3. middle
            4. } + {show[2] &&
            5. last
            6. } +
            + ); + } + + render(, scratch); + expect(scratch.innerHTML).to.equal('
            1. first
            '); + + render(, scratch); + expect(scratch.innerHTML).to.equal( + '
            1. first
            2. middle
            ' + ); + + render(, scratch); + expect(scratch.innerHTML).to.equal( + '
            1. first
            2. middle
            3. last
            ' + ); + + render(, scratch); + expect(scratch.innerHTML).to.equal( + '
            1. first
            2. middle
            ' + ); + + render(, scratch); + expect(scratch.innerHTML).to.equal('
            1. first
            '); + }); }); From e3a5320d049ae8cc31f50eb3dd1f9fdd5ee78247 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Tue, 31 Jan 2023 15:35:01 -0800 Subject: [PATCH 37/71] Convert nested arrays to Fragments, pt. 2 --- src/diff/children.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index fd5bef69c4..00aef58a54 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -66,17 +66,24 @@ export function patchChildren(parentInternal, children, parentDom) { let type = null; let typeFlag = 0; let key; - let normalizedVNode = /** @type {VNode | string} */ (vnode); + /** @type {VNode | string} */ + let normalizedVNode; // text VNodes (strings, numbers, bigints, etc): if (typeof vnode !== 'object') { typeFlag = TYPE_TEXT; - normalizedVNode += ''; + normalizedVNode = '' + vnode; } else { - const isArray = Array.isArray(vnode); - type = isArray ? Fragment : vnode.type; + // TODO: Investigate avoiding this VNode allocation (and the one below in + // the call to `patch`) by passing through the raw VNode type and handling + // nested arrays directly in mount, patch, createInternal, etc. + normalizedVNode = Array.isArray(vnode) + ? createElement(Fragment, null, vnode) + : vnode; + + type = normalizedVNode.type; typeFlag = typeof type === 'function' ? TYPE_COMPONENT : TYPE_ELEMENT; - key = isArray ? null : vnode.key; + key = normalizedVNode.key; } /** @type {Internal?} */ From 6db4fb6936caed104cc393aac325eb6750e4b253 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Tue, 31 Jan 2023 17:03:20 -0800 Subject: [PATCH 38/71] Fix suspended hydration When we are rendering after a hydration that suspended, we need to detect the node that suspended while hydrating and send through the mount code path so it can resume hydration if on this rerender it can resume. Doing this also ensures the suspended hydration flags and state are preserved so additional rerenders have the same behavior. --- src/diff/children.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/diff/children.js b/src/diff/children.js index 00aef58a54..42b7c8d28a 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -266,6 +266,11 @@ export function patchChildren(parentInternal, children, parentDom) { // DOM in place. insert(internal, parentDom); } + } else if ( + (internal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) === + (MODE_HYDRATE | MODE_SUSPENDED) + ) { + mount(internal, parentDom, internal.data); } else { patch( internal, From 0a0fa833ad8293636e7176dc726eb117b019aabe Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 2 Feb 2023 16:24:18 -0800 Subject: [PATCH 39/71] Move oldHead pointer on null placeholders --- src/diff/children.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/diff/children.js b/src/diff/children.js index 42b7c8d28a..a7b64a661f 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -58,6 +58,11 @@ export function patchChildren(parentInternal, children, parentDom) { // this node as unmounting to prevent it from being used in future // searches for matching internals internal.flags |= MODE_UNMOUNTING; + + // If this internal is the first unmatched internal, then bump our + // pointer to the next node so our search will skip over this internal + if (oldHead == internal) oldHead = oldHead._next; + internal = internal._next; } continue; From 0d60437eb7aada801b58b153b6b6a715440f422c Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 2 Feb 2023 16:31:14 -0800 Subject: [PATCH 40/71] Remove internal check around LDS loop Instead, check the length of the wipLDS array in the loop body. This change allows us to use the LDS loop to detect portals at the start of the child internal list --- src/diff/children.js | 133 ++++++++++++++++++++++--------------------- 1 file changed, 68 insertions(+), 65 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index a7b64a661f..a6469b4327 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -164,77 +164,80 @@ export function patchChildren(parentInternal, children, parentDom) { // TODO: Replace _prevLDS with _next. Doing this will make _next meaningless for a moment // TODO: Explore trying to do this without an array, maybe next pointers? Or maybe reuse the array internal = prevInternal; - if (internal) { - // TODO: Argh, we need to skip this first internal if it is a TYPE_ROOT node with a different parentDOM :/ - /** @type {Internal[]} */ - const wipLDS = [internal]; - internal.flags |= INSERT_INTERNAL; - - while ((internal = internal._prev) !== parentInternal) { - // Skip over Root nodes whose parentDOM is different from the current - // parentDOM (aka Portals). Don't mark them for insertion since the - // recursive calls to mountChildren/patchChildren will handle - // mounting/inserting any DOM nodes under the root node. - // - // If a root node's parentDOM is the same as the current parentDOM then - // treat it as an unkeyed fragment and prepare it for moving or insertion - // if necessary. - // - // TODO: Consider the case where a root node has the same parent, goes - // into a different parent, a new node is inserted before the portal, and - // then the portal goes back to the original parent. Do we correctly - // insert the portal into the right place? Currently yes, because the - // beginning of patch calls insert whenever parentDom changes. Could we - // move that logic here? - // - // TODO: We do the props._parentDom !== parentDom in a couple places. - // Could we do this check once and cache the result in a flag? - if ( - internal.flags & TYPE_ROOT && - internal.props._parentDom !== parentDom - ) { - continue; - } + /** @type {Internal[]} */ + const wipLDS = []; + + while (internal && internal !== parentInternal) { + // Skip over Root nodes whose parentDOM is different from the current + // parentDOM (aka Portals). Don't mark them for insertion since the + // recursive calls to mountChildren/patchChildren will handle + // mounting/inserting any DOM nodes under the root node. + // + // If a root node's parentDOM is the same as the current parentDOM then + // treat it as an unkeyed fragment and prepare it for moving or insertion + // if necessary. + // + // TODO: Consider the case where a root node has the same parent, goes + // into a different parent, a new node is inserted before the portal, and + // then the portal goes back to the original parent. Do we correctly + // insert the portal into the right place? Currently yes, because the + // beginning of patch calls insert whenever parentDom changes. Could we + // move that logic here? + // + // TODO: We do the props._parentDom !== parentDom in a couple places. + // Could we do this check once and cache the result in a flag? + if (internal.flags & TYPE_ROOT && internal.props._parentDom !== parentDom) { + internal = internal._prev; + continue; + } - // Mark all internals as requiring insertion. We will clear this flag for - // internals on longest decreasing subsequence - internal.flags |= INSERT_INTERNAL; + // Mark all internals as requiring insertion. We will clear this flag for + // internals on longest decreasing subsequence + internal.flags |= INSERT_INTERNAL; - // Skip over newly mounted internals. They will be mounted in place. - if (internal._index === -1) { - continue; - } + // Skip over newly mounted internals. They will be mounted in place. + if (internal._index === -1) { + internal = internal._prev; + continue; + } - let ldsTail = wipLDS[wipLDS.length - 1]; - if (ldsTail._index > internal._index) { - internal._prevLDS = ldsTail; - wipLDS.push(internal); - } else { - // Search for position in wipLIS where node should go. It should replace - // the first node where node > wip[i] (though keep in mind, we are - // iterating over the list backwards). Example: - // ``` - // wipLIS = [4,3,1], node = 2. - // Node should replace 1: [4,3,2] - // ``` - let i = wipLDS.length; - // TODO: Binary search? - while (--i >= 0 && wipLDS[i]._index < internal._index) {} - - wipLDS[i + 1] = internal; - let prevLDS = i < 0 ? null : wipLDS[i]; - internal._prevLDS = prevLDS; - } + if (wipLDS.length == 0) { + wipLDS.push(internal); + internal = internal._prev; + continue; } - // Step 4. Mark internals in longest decreasing subsequence - /** @type {Internal | null} */ - let ldsNode = wipLDS[wipLDS.length - 1]; - while (ldsNode) { - // This node is on the longest decreasing subsequence so clear INSERT_NODE flag - ldsNode.flags &= ~INSERT_INTERNAL; - ldsNode = ldsNode._prevLDS; + let ldsTail = wipLDS[wipLDS.length - 1]; + if (ldsTail._index > internal._index) { + internal._prevLDS = ldsTail; + wipLDS.push(internal); + } else { + // Search for position in wipLIS where node should go. It should replace + // the first node where node > wip[i] (though keep in mind, we are + // iterating over the list backwards). Example: + // ``` + // wipLIS = [4,3,1], node = 2. + // Node should replace 1: [4,3,2] + // ``` + let i = wipLDS.length; + // TODO: Binary search? + while (--i >= 0 && wipLDS[i]._index < internal._index) {} + + wipLDS[i + 1] = internal; + let prevLDS = i < 0 ? null : wipLDS[i]; + internal._prevLDS = prevLDS; } + + internal = internal._prev; + } + + // Step 4. Mark internals in longest decreasing subsequence + /** @type {Internal | null} */ + let ldsNode = wipLDS.length ? wipLDS[wipLDS.length - 1] : null; + while (ldsNode) { + // This node is on the longest decreasing subsequence so clear INSERT_NODE flag + ldsNode.flags &= ~INSERT_INTERNAL; + ldsNode = ldsNode._prevLDS; } // Step 5. Walk backwards over the newly-assigned _prev properties, visiting From 13ade350810f10efcf29818122372ef73317b099 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 6 Feb 2023 13:43:31 -0800 Subject: [PATCH 41/71] Remove old commented out patchChildren code --- src/diff/children.js | 218 ------------------------------------------- 1 file changed, 218 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index a6469b4327..557f32cccb 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -40,9 +40,7 @@ export function patchChildren(parentInternal, children, parentDom) { let internal = parentInternal._child; /** @type {Internal} */ let prevInternal; - // let newTail; let oldHead = internal; - // let skewedOldHead = oldHead; // Step 1. Find matches and set up _prev pointers. Identity null placeholders // by also walking the old internal children at the same time @@ -128,10 +126,6 @@ export function patchChildren(parentInternal, children, parentDom) { // no match, create a new Internal: if (!matchedInternal) { matchedInternal = createInternal(normalizedVNode, parentInternal); - // console.log('creating new', internal.type); - } else { - // console.log('updating', internal.type); - // patch(internal, vnode, parentDom); } matchedInternal._prev = prevInternal || parentInternal; @@ -149,7 +143,6 @@ export function patchChildren(parentInternal, children, parentDom) { const next = oldHead._next; if (oldHead._prev == null) { if (lastMatchedInternal) lastMatchedInternal._next = next; - // else parentInternal._child = next; unmount(oldHead, oldHead, 0); } else { lastMatchedInternal = oldHead; @@ -311,217 +304,6 @@ export function patchChildren(parentInternal, children, parentDom) { parentInternal._child = firstChild; } -/* -export function patchChildren(internal, children, parentDom) { - // let oldChildren = - // (internal._children && internal._children.slice()) || EMPTY_ARR; - - // let oldChildrenLength = oldChildren.length; - // let remainingOldChildren = oldChildrenLength; - - - let skew = 0; - let i; - - /** @type {import('../internal').Internal} *\/ - let childInternal; - - /** @type {import('../internal').ComponentChild} *\/ - let childVNode; - - /** @type {import('../internal').Internal[]} *\/ - const newChildren = []; - - for (i = 0; i < children.length; i++) { - childVNode = normalizeToVNode(children[i]); - - // Terser removes the `continue` here and wraps the loop body - // in a `if (childVNode) { ... } condition - if (childVNode == null) { - // newChildren[i] = null; - continue; - } - - let skewedIndex = i + skew; - - /// TODO: Reconsider if we should bring back the "not moving text nodes" logic? - let matchingIndex = findMatchingIndex( - childVNode, - oldChildren, - skewedIndex, - remainingOldChildren - ); - - if (matchingIndex === -1) { - childInternal = UNDEFINED; - } else { - childInternal = oldChildren[matchingIndex]; - oldChildren[matchingIndex] = UNDEFINED; - remainingOldChildren--; - } - - let mountingChild = childInternal == null; - - if (mountingChild) { - childInternal = createInternal(childVNode, internal); - - // We are mounting a new VNode - mount( - childInternal, - childVNode, - parentDom, - getDomSibling(internal, skewedIndex) - ); - } - // If this node suspended during hydration, and no other flags are set: - // @TODO: might be better to explicitly check for MODE_ERRORED here. - else if ( - (childInternal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) === - (MODE_HYDRATE | MODE_SUSPENDED) - ) { - // We are resuming the hydration of a VNode - mount(childInternal, childVNode, parentDom, childInternal.data); - } else { - // Morph the old element into the new one, but don't append it to the dom yet - patch(childInternal, childVNode, parentDom); - } - - go: if (mountingChild) { - if (matchingIndex == -1) { - skew--; - } - - // Perform insert of new dom - if (childInternal.flags & TYPE_DOM) { - parentDom.insertBefore( - childInternal.data, - getDomSibling(internal, skewedIndex) - ); - } - } else if (matchingIndex !== skewedIndex) { - // Move this DOM into its correct place - if (matchingIndex === skewedIndex + 1) { - skew++; - break go; - } else if (matchingIndex > skewedIndex) { - if (remainingOldChildren > children.length - skewedIndex) { - skew += matchingIndex - skewedIndex; - break go; - } else { - // ### Change from keyed: I think this was missing from the algo... - skew--; - } - } else if (matchingIndex < skewedIndex) { - if (matchingIndex == skewedIndex - 1) { - skew = matchingIndex - skewedIndex; - } else { - skew = 0; - } - } else { - skew = 0; - } - - skewedIndex = i + skew; - - if (matchingIndex == i) break go; - - let nextSibling = getDomSibling(internal, skewedIndex + 1); - if (childInternal.flags & TYPE_DOM) { - parentDom.insertBefore(childInternal.data, nextSibling); - } else { - insertComponentDom(childInternal, nextSibling, parentDom); - } - } - - newChildren[i] = childInternal; - } - - internal._children = newChildren; - - // Remove remaining oldChildren if there are any. - if (remainingOldChildren > 0) { - for (i = oldChildrenLength; i--; ) { - if (oldChildren[i] != null) { - unmount(oldChildren[i], oldChildren[i]); - } - } - } - - // Set refs only after unmount - for (i = 0; i < newChildren.length; i++) { - childInternal = newChildren[i]; - if (childInternal) { - let oldRef = childInternal._prevRef; - if (childInternal.ref != oldRef) { - if (oldRef) applyRef(oldRef, null, childInternal); - if (childInternal.ref) - applyRef( - childInternal.ref, - childInternal._component || childInternal.data, - childInternal - ); - } - } - } -} -*/ - -/** - * @param {import('../internal').VNode | string} childVNode - * @param {import('../internal').Internal[]} oldChildren - * @param {number} skewedIndex - * @param {number} remainingOldChildren - * @returns {number} - */ -/* -function findMatchingIndex( - childVNode, - oldChildren, - skewedIndex, - remainingOldChildren -) { - const type = typeof childVNode == 'string' ? null : childVNode.type; - const key = type !== null ? childVNode.key : UNDEFINED; - let match = -1; - let x = skewedIndex - 1; // i - 1; - let y = skewedIndex + 1; // i + 1; - let oldChild = oldChildren[skewedIndex]; // i - - if ( - // ### Change from keyed: support for matching null placeholders - oldChild === null || - (oldChild != null && oldChild.type === type && oldChild.key == key) - ) { - match = skewedIndex; // i - } - // If there are any unused children left (ignoring an available in-place child which we just checked) - else if (remainingOldChildren > (oldChild != null ? 1 : 0)) { - // eslint-disable-next-line no-constant-condition - while (true) { - if (x >= 0) { - oldChild = oldChildren[x]; - if (oldChild != null && oldChild.type === type && oldChild.key == key) { - match = x; - break; - } - x--; - } - if (y < oldChildren.length) { - oldChild = oldChildren[y]; - if (oldChild != null && oldChild.type === type && oldChild.key == key) { - match = y; - break; - } - y++; - } else if (x < 0) { - break; - } - } - } - - return match; -} -*/ /** * @param {import('../internal').Internal} internal From 1970840d7563db8ea65c8451fb0b29c9da9274ae Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 6 Feb 2023 14:02:38 -0800 Subject: [PATCH 42/71] Break up patchChildren into functions --- src/diff/children.js | 71 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 557f32cccb..3a86df6a2a 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -28,22 +28,47 @@ import { createInternal, getDomSibling } from '../tree'; /** @typedef {import('../internal').Internal} Internal */ /** @typedef {import('../internal').VNode} VNode */ +/** @typedef {import('../internal').PreactElement} PreactElement */ +/** @typedef {import('../internal').ComponentChildren} ComponentChildren */ /** * Update an internal with new children. * @param {Internal} parentInternal The internal whose children should be patched - * @param {import('../internal').ComponentChildren[]} children The new children, represented as VNodes - * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered + * @param {ComponentChildren[]} children The new children, represented as VNodes + * @param {PreactElement} parentDom The element into which this subtree is rendered */ export function patchChildren(parentInternal, children, parentDom) { + // Step 1. Find matches and set up _prev pointers. Identity null placeholders + // by also walking the old internal children at the same time + let prevInternal = findMatches(parentInternal, children); + + // Step 2. Walk over the unused children and unmount: + unmountUnusedChildren(parentInternal); + + // Step 3. Find the longest decreasing subsequence + // TODO: Check prevInternal exits before running (aka we are unmounting everything); + // TODO: Ideally only run this if something has moved + // TODO: Replace _prevLDS with _next. Doing this will make _next meaningless for a moment + // TODO: Explore trying to do this without an array, maybe next pointers? Or maybe reuse the array + runLDS(prevInternal, parentInternal, parentDom); + + // Step 5. Walk backwards over the newly-assigned _prev properties, visiting + // each Internal to set its _next ptr and perform insert/mount/update. + insertionLoop(prevInternal, children, parentDom, parentInternal); +} + +/** + * @param {Internal} parentInternal + * @param {ComponentChildren[]} children + * @returns {Internal} + */ +function findMatches(parentInternal, children) { /** @type {Internal} */ let internal = parentInternal._child; /** @type {Internal} */ let prevInternal; let oldHead = internal; - // Step 1. Find matches and set up _prev pointers. Identity null placeholders - // by also walking the old internal children at the same time for (let index = 0; index < children.length; index++) { const vnode = children[index]; @@ -136,9 +161,15 @@ export function patchChildren(parentInternal, children, parentDom) { } } - // Step 2. Walk over the unused children and unmount: + return prevInternal; +} + +/** + * @param {Internal} parentInternal + */ +function unmountUnusedChildren(parentInternal) { let lastMatchedInternal; - oldHead = parentInternal._child; + let oldHead = parentInternal._child; while (oldHead) { const next = oldHead._next; if (oldHead._prev == null) { @@ -149,14 +180,15 @@ export function patchChildren(parentInternal, children, parentDom) { } oldHead = next; } +} - // Step 3. Find the longest decreasing subsequence - // TODO: Move this into it's own function - // TODO: Check prevInternal exits before running (aka we are unmounting everything); - // TODO: Ideally only run this if something has moved - // TODO: Replace _prevLDS with _next. Doing this will make _next meaningless for a moment - // TODO: Explore trying to do this without an array, maybe next pointers? Or maybe reuse the array - internal = prevInternal; +/** + * @param {Internal} internal + * @param {Internal} parentInternal + * @param {PreactElement} parentDom + */ +function runLDS(internal, parentInternal, parentDom) { + // let internal = prevInternal; /** @type {Internal[]} */ const wipLDS = []; @@ -232,15 +264,22 @@ export function patchChildren(parentInternal, children, parentDom) { ldsNode.flags &= ~INSERT_INTERNAL; ldsNode = ldsNode._prevLDS; } +} - // Step 5. Walk backwards over the newly-assigned _prev properties, visiting - // each Internal to set its _next ptr and perform insert/mount/update. +/** + * @param {Internal} internal + * @param {ComponentChildren[]} children + * @param {PreactElement} parentDom + * @param {Internal} parentInternal + */ +function insertionLoop(internal, children, parentDom, parentInternal) { /** @type {Internal} */ let nextInternal = null; let firstChild = null; let index = children.length; - internal = prevInternal; + // internal = prevInternal; + let prevInternal = internal; while (internal) { // set this internal's next ptr to the previous loop entry internal._next = nextInternal; From ef7a9b640ffb7047ae0f40d0c7f0f3fbe855c48e Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 6 Feb 2023 14:48:58 -0800 Subject: [PATCH 43/71] Use microtick outside of events when rerendering --- src/component.js | 16 +++++++++++++++- src/diff/props.js | 18 ++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/component.js b/src/component.js index 795b652b41..e495a1847e 100644 --- a/src/component.js +++ b/src/component.js @@ -4,6 +4,7 @@ import { createElement, Fragment } from './create-element'; import { patch } from './diff/patch'; import { DIRTY_BIT, FORCE_UPDATE, MODE_UNMOUNTING } from './constants'; import { getParentDom } from './tree'; +import { inEvent } from './diff/props'; export let ENABLE_CLASSES = false; @@ -122,6 +123,19 @@ let renderQueue = []; let prevDebounce; +const microTick = + typeof Promise == 'function' + ? Promise.prototype.then.bind(Promise.resolve()) + : setTimeout; + +function defer(cb) { + if (inEvent) { + setTimeout(cb); + } else { + microTick(cb); + } +} + /** * Enqueue a rerender of an internal * @param {import('./internal').Internal} internal The internal to rerender @@ -135,7 +149,7 @@ export function enqueueRender(internal) { prevDebounce !== options.debounceRendering ) { prevDebounce = options.debounceRendering; - (prevDebounce || setTimeout)(processRenderQueue); + (prevDebounce || defer)(processRenderQueue); } } diff --git a/src/diff/props.js b/src/diff/props.js index 24cc19ba23..c904a70bdb 100644 --- a/src/diff/props.js +++ b/src/diff/props.js @@ -102,15 +102,29 @@ export function setProperty(dom, name, value, oldValue, isSvg) { } } +export let inEvent = false; + /** * Proxy an event to hooked event handlers * @param {Event} e The event object from the browser * @private */ function eventProxy(e) { - return this._listeners[e.type + false](options.event ? options.event(e) : e); + inEvent = true; + try { + return this._listeners[e.type + false]( + options.event ? options.event(e) : e + ); + } finally { + inEvent = false; + } } function eventProxyCapture(e) { - return this._listeners[e.type + true](options.event ? options.event(e) : e); + inEvent = true; + try { + return this._listeners[e.type + true](options.event ? options.event(e) : e); + } finally { + inEvent = false; + } } From a5c25c3f1ceb82a705970d3e28e61a5154d5247d Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 6 Feb 2023 14:53:42 -0800 Subject: [PATCH 44/71] Add fast path for diffing no moved Internals --- src/diff/children.js | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 3a86df6a2a..74023f86ff 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -31,6 +31,9 @@ import { createInternal, getDomSibling } from '../tree'; /** @typedef {import('../internal').PreactElement} PreactElement */ /** @typedef {import('../internal').ComponentChildren} ComponentChildren */ +/** @type {boolean} */ +let moved; + /** * Update an internal with new children. * @param {Internal} parentInternal The internal whose children should be patched @@ -40,17 +43,19 @@ import { createInternal, getDomSibling } from '../tree'; export function patchChildren(parentInternal, children, parentDom) { // Step 1. Find matches and set up _prev pointers. Identity null placeholders // by also walking the old internal children at the same time + moved = false; let prevInternal = findMatches(parentInternal, children); // Step 2. Walk over the unused children and unmount: unmountUnusedChildren(parentInternal); // Step 3. Find the longest decreasing subsequence - // TODO: Check prevInternal exits before running (aka we are unmounting everything); - // TODO: Ideally only run this if something has moved // TODO: Replace _prevLDS with _next. Doing this will make _next meaningless for a moment // TODO: Explore trying to do this without an array, maybe next pointers? Or maybe reuse the array - runLDS(prevInternal, parentInternal, parentDom); + if (prevInternal && moved) { + runLDS(prevInternal, parentInternal, parentDom); + } + moved = false; // Step 5. Walk backwards over the newly-assigned _prev properties, visiting // each Internal to set its _next ptr and perform insert/mount/update. @@ -117,18 +122,30 @@ function findMatches(parentInternal, children) { /** @type {Internal?} */ let matchedInternal; - // TODO: See if doing a fast path (special if condition) for already in - // place matches is faster than while loop if (key == null && internal && index < internal._index) { // If we are doing an unkeyed diff, and the old index of the current // internal is greater than the current VNode index, then this vnode // represents a new element that is mounting into what was previous a null // placeholder slot. We should create a new internal to mount this VNode. - } else { + } else if ( + oldHead && + oldHead._prev == null && + ~oldHead.flags & MODE_UNMOUNTING && + (oldHead.flags & typeFlag) !== 0 && + oldHead.type === type && + oldHead.key == key + ) { + // Fast path checking if this current vnode matches the first unused + // Internal. By doing this we can avoid the search loop and setting the + // move flag, which allows us to skip the LDS algorithm if no Internals + // moved + matchedInternal = oldHead; + oldHead = oldHead._next; + } else if (oldHead) { // seek forward through the Internals list, starting at the head (either first, or first unused). // only match unused items, which are internals where _prev === undefined. // note: _prev=null for the first matched internal, and should be considered "used". - let search = oldHead; + let search = oldHead._next; while (search) { const flags = search.flags; const isUnused = @@ -139,6 +156,7 @@ function findMatches(parentInternal, children) { search.type === type && search.key == key ) { + moved = true; matchedInternal = search; // if the match was the first unused item, bump the start ptr forward: if (search === oldHead) oldHead = oldHead._next; From b3d8fe7fbaa26691dc87b63731da05d09222b3d8 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 2 Feb 2023 16:33:10 -0800 Subject: [PATCH 45/71] === BEGIN v11-linked-list-prev-index-nextDom === Use flags to track matched vs unmatched internals --- src/constants.js | 3 +++ src/diff/children.js | 13 ++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/constants.js b/src/constants.js index ea627adba7..ec8fa4773f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -51,6 +51,9 @@ export const SKIP_CHILDREN = 1 << 15; /** Indicates that this node needs to be inserted while patching children */ export const INSERT_INTERNAL = 1 << 16; +/** Indicates that this node matched a VNode while diffing and is not going to be unmounted */ +export const MATCHED_INTERNAL = 1 << 17; + /** Reset all mode flags */ export const RESET_MODE = ~( MODE_HYDRATE | diff --git a/src/diff/children.js b/src/diff/children.js index 74023f86ff..20c626c69d 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -11,7 +11,8 @@ import { TYPE_ELEMENT, INSERT_INTERNAL, TYPE_ROOT, - MODE_UNMOUNTING + MODE_UNMOUNTING, + MATCHED_INTERNAL } from '../constants'; import { mount } from './mount'; import { patch } from './patch'; @@ -148,16 +149,17 @@ function findMatches(parentInternal, children) { let search = oldHead._next; while (search) { const flags = search.flags; - const isUnused = - search._prev == null && ~search.flags & MODE_UNMOUNTING; + const isUsed = + search.flags & MODE_UNMOUNTING || search.flags & MATCHED_INTERNAL; if ( - isUnused && + !isUsed && (flags & typeFlag) !== 0 && search.type === type && search.key == key ) { moved = true; matchedInternal = search; + matchedInternal.flags |= MATCHED_INTERNAL; // if the match was the first unused item, bump the start ptr forward: if (search === oldHead) oldHead = oldHead._next; break; @@ -190,10 +192,11 @@ function unmountUnusedChildren(parentInternal) { let oldHead = parentInternal._child; while (oldHead) { const next = oldHead._next; - if (oldHead._prev == null) { + if (~oldHead.flags & MATCHED_INTERNAL) { if (lastMatchedInternal) lastMatchedInternal._next = next; unmount(oldHead, oldHead, 0); } else { + oldHead.flags &= ~MATCHED_INTERNAL; lastMatchedInternal = oldHead; } oldHead = next; From 9f7db6d0db439077cefa9d48ad7ac5c8fea61d08 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 2 Feb 2023 17:34:04 -0800 Subject: [PATCH 46/71] Remove older commented code --- src/tree.js | 44 -------------------------------------------- 1 file changed, 44 deletions(-) diff --git a/src/tree.js b/src/tree.js index 8816ba0b88..8c446398a2 100644 --- a/src/tree.js +++ b/src/tree.js @@ -125,50 +125,6 @@ const shouldSearchComponent = internal => * @returns {import('./internal').PreactNode} */ export function getDomSibling(internal, childIndex) { - // // basically looking for the next pointer that can be used to perform an insertBefore: - // // @TODO inline the null case, since it's only used in patch. - // if (childIndex == null) { - // // Use childIndex==null as a signal to resume the search from the vnode's sibling - // const next = internal._next; - // return next && (getChildDom(next) || getDomSibling(next)); - - // // return next && (getChildDom(next) || getDomSibling(next)); - // // let sibling = internal; - // // while (sibling = sibling._next) { - // // let domChildInternal = getChildDom(sibling); - // // if (domChildInternal) return domChildInternal; - // // } - - // // const parent = internal._parent; - // // let child = parent._child; - // // while (child) { - // // if (child === internal) { - // // return getDomSibling(child._next); - // // } - // // child = child._next; - // // } - - // // return getDomSibling( - // // internal._parent, - // // internal._parent._children.indexOf(internal) + 1 - // // ); - // } - - // let childDom = getChildDom(internal._child); - // if (childDom) { - // return childDom; - // } - - // // If we get here, we have not found a DOM node in this vnode's children. We - // // must resume from this vnode's sibling (in it's parent _children array). - // // Only climb up and search the parent if we aren't searching through a DOM - // // VNode (meaning we reached the DOM parent of the original vnode that began - // // the search). Note, the top of the tree has _parent == null so avoiding that - // // here. - // return internal._parent && shouldSearchComponent(internal) - // ? getDomSibling(internal) - // : null; - if (internal._next) { let childDom = getFirstDom(internal._next); if (childDom) { From 0354b3d8847c4eb4aa5eca64cc37b6fcaa670da4 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 5 Feb 2023 00:58:41 -0800 Subject: [PATCH 47/71] WIP: Walk loop forwards and pass along the boundary dom node in patch recursion I focused on the keys, render, fragments, and placholder tests for this commit --- src/component.js | 4 +- src/create-root.js | 3 +- src/diff/children.js | 275 +++++++++++++++++++++++++++---------------- src/diff/patch.js | 12 +- src/internal.d.ts | 2 +- src/tree.js | 2 +- 6 files changed, 187 insertions(+), 111 deletions(-) diff --git a/src/component.js b/src/component.js index e495a1847e..fcdf49f0dc 100644 --- a/src/component.js +++ b/src/component.js @@ -3,7 +3,7 @@ import options from './options'; import { createElement, Fragment } from './create-element'; import { patch } from './diff/patch'; import { DIRTY_BIT, FORCE_UPDATE, MODE_UNMOUNTING } from './constants'; -import { getParentDom } from './tree'; +import { getDomSibling, getParentDom } from './tree'; import { inEvent } from './diff/props'; export let ENABLE_CLASSES = false; @@ -102,7 +102,7 @@ function renderQueuedInternal(internal) { const vnode = createElement(internal.type, internal.props); vnode.props = internal.props; - patch(internal, vnode, getParentDom(internal)); + patch(internal, vnode, getParentDom(internal), getDomSibling(internal)); commitRoot(internal); } diff --git a/src/create-root.js b/src/create-root.js index c40327efbe..9fbc639aa3 100644 --- a/src/create-root.js +++ b/src/create-root.js @@ -30,7 +30,8 @@ export function createRoot(parentDom) { /** @type {import('./internal').PreactElement} */ (parentDom.firstChild); if (rootInternal) { - patch(rootInternal, vnode, parentDom); + // TODO: Huh...... Do we have to assume the boundaryNode is null? + patch(rootInternal, vnode, parentDom, null); } else { rootInternal = createInternal(vnode); diff --git a/src/diff/children.js b/src/diff/children.js index 20c626c69d..a89946c185 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -17,7 +17,7 @@ import { import { mount } from './mount'; import { patch } from './patch'; import { unmount } from './unmount'; -import { createInternal, getDomSibling } from '../tree'; +import { createInternal, getDomSibling, getFirstDom } from '../tree'; /** * Scenarios: @@ -40,41 +40,64 @@ let moved; * @param {Internal} parentInternal The internal whose children should be patched * @param {ComponentChildren[]} children The new children, represented as VNodes * @param {PreactElement} parentDom The element into which this subtree is rendered + * @param {PreactElement} boundaryNode The DOM element that all children of this internal + * should be inserted before, i.e. the DOM boundary or edge of this Internal, a.k.a. the next DOM sibling of + * this internal */ -export function patchChildren(parentInternal, children, parentDom) { - // Step 1. Find matches and set up _prev pointers. Identity null placeholders - // by also walking the old internal children at the same time - moved = false; - let prevInternal = findMatches(parentInternal, children); - - // Step 2. Walk over the unused children and unmount: - unmountUnusedChildren(parentInternal); - - // Step 3. Find the longest decreasing subsequence +export function patchChildren( + parentInternal, + children, + parentDom, + boundaryNode +) { + // Step 1. Find matches and set up _next pointers. All unused internals are at + // attached to oldHead. + // + // TODO: Identity null placeholders by also walking the old internal children + // at the same time. + // + // TODO: Remove usages of MODE_UNMOUNTING and MATCHED_INTERNALS flags since + // they are unnecessary now + moved = true; // TODO: Bring back skipping LIS on no moves. `moved` should start out as `false` + findMatches(parentInternal._child, children, parentInternal); + + // Step 3. Find the longest increasing subsequence // TODO: Replace _prevLDS with _next. Doing this will make _next meaningless for a moment // TODO: Explore trying to do this without an array, maybe next pointers? Or maybe reuse the array - if (prevInternal && moved) { - runLDS(prevInternal, parentInternal, parentDom); + let lisHead = null; + if (parentInternal._child && moved) { + lisHead = runLIS(parentInternal._child, parentDom); } moved = false; - // Step 5. Walk backwards over the newly-assigned _prev properties, visiting - // each Internal to set its _next ptr and perform insert/mount/update. - insertionLoop(prevInternal, children, parentDom, parentInternal); + // Step 5. Walk forwards over the newly-assigned _next properties, inserting + // Internals that require insertion. We track the next dom sibling Internals + // should be inserted before by walking over the LIS at the same time + insertionLoop( + parentInternal._child, + children, + parentDom, + boundaryNode, + lisHead + ); } /** - * @param {Internal} parentInternal + * @param {Internal} internal * @param {ComponentChildren[]} children - * @returns {Internal} + * @param {Internal} parentInternal */ -function findMatches(parentInternal, children) { - /** @type {Internal} */ - let internal = parentInternal._child; +function findMatches(internal, children, parentInternal) { /** @type {Internal} */ - let prevInternal; + // let internal = parentInternal._child; + parentInternal._child = null; + + /** @type {Internal} The start of the list of unmatched Internals */ let oldHead = internal; + /** @type {Internal} The last matched internal */ + let prevMatchedInternal; + for (let index = 0; index < children.length; index++) { const vnode = children[index]; @@ -87,6 +110,7 @@ function findMatches(parentInternal, children) { // this node as unmounting to prevent it from being used in future // searches for matching internals internal.flags |= MODE_UNMOUNTING; + unmount(internal, internal, 0); // If this internal is the first unmatched internal, then bump our // pointer to the next node so our search will skip over this internal @@ -124,13 +148,13 @@ function findMatches(parentInternal, children) { let matchedInternal; if (key == null && internal && index < internal._index) { + // TODO: FIX NULL PLACEHOLDERS // If we are doing an unkeyed diff, and the old index of the current // internal is greater than the current VNode index, then this vnode // represents a new element that is mounting into what was previous a null // placeholder slot. We should create a new internal to mount this VNode. } else if ( oldHead && - oldHead._prev == null && ~oldHead.flags & MODE_UNMOUNTING && (oldHead.flags & typeFlag) !== 0 && oldHead.type === type && @@ -146,11 +170,24 @@ function findMatches(parentInternal, children) { // seek forward through the Internals list, starting at the head (either first, or first unused). // only match unused items, which are internals where _prev === undefined. // note: _prev=null for the first matched internal, and should be considered "used". - let search = oldHead._next; + /** @type {Internal} */ + let prevSearch = oldHead; + + // TODO: Try to get this optimization to work + // // Let's start our search at the node where our previous match left off. + // // We do this cuz it optimizes the more common case of holes over a keyed + // // shuffles. + // let searchStart = prevMatchedInternal._next; + let searchStart = oldHead._next; + /** @type {Internal} */ + let search = searchStart; + while (search) { const flags = search.flags; const isUsed = search.flags & MODE_UNMOUNTING || search.flags & MATCHED_INTERNAL; + + // Match found! if ( !isUsed && (flags & typeFlag) !== 0 && @@ -160,60 +197,73 @@ function findMatches(parentInternal, children) { moved = true; matchedInternal = search; matchedInternal.flags |= MATCHED_INTERNAL; - // if the match was the first unused item, bump the start ptr forward: - if (search === oldHead) oldHead = oldHead._next; + + // Let's update our list of nodes to search to remove the new matchedInternal. + prevSearch._next = matchedInternal._next; + + break; + } + + // No match found. Let's move our pointers to the next node in our + // search. + prevSearch = search; + + // If our current node we are searching has a _next node, then let's + // continue from there. If it doesn't, let's loop back around to the + // start of the list of unmatched nodes (i.e. oldHead). + search = search._next ? search._next : oldHead._next; + if (search === searchStart) { + // However, it's possible that oldHead was the start of our search. If + // so, we can stop searching. No match was found. break; } - search = search._next; } } // no match, create a new Internal: if (!matchedInternal) { matchedInternal = createInternal(normalizedVNode, parentInternal); + matchedInternal.flags |= MATCHED_INTERNAL; + // console.log('creating new', internal.type); } - matchedInternal._prev = prevInternal || parentInternal; - prevInternal = matchedInternal; + // move into place in new list + if (prevMatchedInternal) prevMatchedInternal._next = matchedInternal; + else parentInternal._child = matchedInternal; + prevMatchedInternal = matchedInternal; if (internal && internal._index == index) { internal = internal._next; } } - return prevInternal; + if (prevMatchedInternal) prevMatchedInternal._next = null; + + // Step 2. Walk over the unused children and unmount: + unmountUnusedChildren(oldHead); } /** - * @param {Internal} parentInternal + * @param {Internal} internal */ -function unmountUnusedChildren(parentInternal) { - let lastMatchedInternal; - let oldHead = parentInternal._child; - while (oldHead) { - const next = oldHead._next; - if (~oldHead.flags & MATCHED_INTERNAL) { - if (lastMatchedInternal) lastMatchedInternal._next = next; - unmount(oldHead, oldHead, 0); - } else { - oldHead.flags &= ~MATCHED_INTERNAL; - lastMatchedInternal = oldHead; - } - oldHead = next; +function unmountUnusedChildren(internal) { + while (internal) { + unmount(internal, internal, 0); + internal = internal._next; } } /** * @param {Internal} internal - * @param {Internal} parentInternal * @param {PreactElement} parentDom + * @returns {Internal} */ -function runLDS(internal, parentInternal, parentDom) { +function runLIS(internal, parentDom) { // let internal = prevInternal; /** @type {Internal[]} */ - const wipLDS = []; + const wipLIS = []; - while (internal && internal !== parentInternal) { + while (internal) { // Skip over Root nodes whose parentDOM is different from the current // parentDOM (aka Portals). Don't mark them for insertion since the // recursive calls to mountChildren/patchChildren will handle @@ -233,7 +283,9 @@ function runLDS(internal, parentInternal, parentDom) { // TODO: We do the props._parentDom !== parentDom in a couple places. // Could we do this check once and cache the result in a flag? if (internal.flags & TYPE_ROOT && internal.props._parentDom !== parentDom) { - internal = internal._prev; + // if (prevMatchedInternal) prevMatchedInternal._next = internal; + // prevMatchedInternal = internal; + internal = internal._next; continue; } @@ -243,20 +295,25 @@ function runLDS(internal, parentInternal, parentDom) { // Skip over newly mounted internals. They will be mounted in place. if (internal._index === -1) { - internal = internal._prev; + // if (prevMatchedInternal) prevMatchedInternal._next = internal; + // prevMatchedInternal = internal; + internal = internal._next; continue; } - if (wipLDS.length == 0) { - wipLDS.push(internal); - internal = internal._prev; + if (wipLIS.length == 0) { + wipLIS.push(internal); + + // if (prevMatchedInternal) prevMatchedInternal._next = internal; + // prevMatchedInternal = internal; + internal = internal._next; continue; } - let ldsTail = wipLDS[wipLDS.length - 1]; - if (ldsTail._index > internal._index) { - internal._prevLDS = ldsTail; - wipLDS.push(internal); + let ldsTail = wipLIS[wipLIS.length - 1]; + if (ldsTail._index < internal._index) { + internal._prevLIS = ldsTail; + wipLIS.push(internal); } else { // Search for position in wipLIS where node should go. It should replace // the first node where node > wip[i] (though keep in mind, we are @@ -265,67 +322,86 @@ function runLDS(internal, parentInternal, parentDom) { // wipLIS = [4,3,1], node = 2. // Node should replace 1: [4,3,2] // ``` - let i = wipLDS.length; + let i = wipLIS.length; // TODO: Binary search? - while (--i >= 0 && wipLDS[i]._index < internal._index) {} + while (--i >= 0 && wipLIS[i]._index > internal._index) {} - wipLDS[i + 1] = internal; - let prevLDS = i < 0 ? null : wipLDS[i]; - internal._prevLDS = prevLDS; + wipLIS[i + 1] = internal; + let prevLIS = i < 0 ? null : wipLIS[i]; + internal._prevLIS = prevLIS; } - internal = internal._prev; + // if (prevMatchedInternal) prevMatchedInternal._next = internal; + // prevMatchedInternal = internal; + internal = internal._next; } - // Step 4. Mark internals in longest decreasing subsequence + // Step 4. Mark internals in longest increasing subsequence and reverse the + // _prevLIS pointers to be _nextLIS pointers for use in the insertion loop /** @type {Internal | null} */ - let ldsNode = wipLDS.length ? wipLDS[wipLDS.length - 1] : null; - while (ldsNode) { + let lisNode = wipLIS.length ? wipLIS[wipLIS.length - 1] : null; + let lisHead = lisNode; + let nextLIS = null; + while (lisNode) { // This node is on the longest decreasing subsequence so clear INSERT_NODE flag - ldsNode.flags &= ~INSERT_INTERNAL; - ldsNode = ldsNode._prevLDS; + lisNode.flags &= ~INSERT_INTERNAL; + let temp = lisNode._prevLIS; + lisNode._prevLIS = nextLIS; + nextLIS = lisNode; + lisNode = temp; + if (lisNode) lisHead = lisNode; } + + return lisHead; } /** * @param {Internal} internal * @param {ComponentChildren[]} children * @param {PreactElement} parentDom - * @param {Internal} parentInternal + * @param {PreactElement} boundaryNode + * @param {Internal} lisHead */ -function insertionLoop(internal, children, parentDom, parentInternal) { - /** @type {Internal} */ - let nextInternal = null; - let firstChild = null; +function insertionLoop(internal, children, parentDom, boundaryNode, lisHead) { + /** @type {Internal} The next in-place Internal whose DOM previous Internals should be inserted before */ + let lisNode = lisHead; + /** @type {PreactElement} The DOM element of the next LIS internal */ + let nextDomSibling; + if (lisNode) { + if (lisNode.flags & TYPE_DOM) { + nextDomSibling = lisNode.data; + } else { + nextDomSibling = getFirstDom(lisNode._child); + } + } else { + nextDomSibling = boundaryNode; + } - let index = children.length; - // internal = prevInternal; - let prevInternal = internal; + let index = 0; while (internal) { - // set this internal's next ptr to the previous loop entry - internal._next = nextInternal; - nextInternal = internal; - - let vnode = children[--index]; + let vnode = children[index]; while (vnode == null || vnode === true || vnode === false) { - vnode = children[--index]; + vnode = children[++index]; } - prevInternal = internal._prev; - - // for now, we're only using double-links internally to this function: - internal._prev = internal._prevLDS = null; - - if (prevInternal === parentInternal) prevInternal = undefined; + if (internal === lisNode) { + lisNode = lisNode._prevLIS; + if (lisNode) { + nextDomSibling = + lisNode.flags & TYPE_DOM ? lisNode.data : getFirstDom(lisNode._child); + } else { + nextDomSibling = boundaryNode; + } + } if (internal._index === -1) { - mount(internal, parentDom, getDomSibling(internal)); + mount(internal, parentDom, nextDomSibling); if (internal.flags & TYPE_DOM) { // If we are mounting a component, it's DOM children will get inserted // into the DOM in mountChildren. If we are mounting a DOM node, then // it's children will be mounted into itself and we need to insert this // DOM in place. - insert(internal, parentDom); + insert(internal, parentDom, nextDomSibling); } } else if ( (internal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) === @@ -336,16 +412,16 @@ function insertionLoop(internal, children, parentDom, parentInternal) { patch( internal, Array.isArray(vnode) ? createElement(Fragment, null, vnode) : vnode, - parentDom + parentDom, + nextDomSibling ); if (internal.flags & INSERT_INTERNAL) { - insert(internal, parentDom); + insert(internal, parentDom, nextDomSibling); } } - if (!prevInternal) firstChild = internal; - internal.flags &= ~INSERT_INTERNAL; + internal.flags &= ~MATCHED_INTERNAL; let oldRef = internal._prevRef; if (internal.ref != oldRef) { @@ -354,15 +430,10 @@ function insertionLoop(internal, children, parentDom, parentInternal) { applyRef(internal.ref, internal._component || internal.data, internal); } - // TODO: this index should match the index of the matching vnode. So any - // skipping over of non-renderables should move this index accordingly. - // Doing this should help with null placeholders and enable us to skip the - // LIS algorithm for situations like `{condition &&
            }` - internal._index = index; - internal = prevInternal; + internal._prevLIS = null; + internal._index = index++; + internal = internal._next; } - - parentInternal._child = firstChild; } /** diff --git a/src/diff/patch.js b/src/diff/patch.js index df372b944b..46525a34de 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -29,8 +29,11 @@ import { commitQueue } from './commit'; * @param {import('../internal').Internal} internal The Internal node to patch * @param {import('../internal').ComponentChild} vnode The new virtual node * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered + * @param {import('../internal').PreactElement} boundaryNode The DOM element that all children of this internal + * should be inserted before, i.e. the DOM boundary or edge of this Internal, a.k.a. the next DOM sibling of + * this internal */ -export function patch(internal, vnode, parentDom) { +export function patch(internal, vnode, parentDom, boundaryNode) { let flags = internal.flags; if (flags & TYPE_TEXT) { @@ -100,11 +103,11 @@ export function patch(internal, vnode, parentDom) { ? internal.data : internal.flags & MODE_HYDRATE ? null - : getDomSibling(internal); + : boundaryNode; mountChildren(internal, renderResult, parentDom, siblingDom); } else { - patchChildren(internal, renderResult, parentDom); + patchChildren(internal, renderResult, parentDom, boundaryNode); } // TODO: for some reason lazy-hydration stops working as .data is assigned a DOM-node @@ -190,7 +193,8 @@ function patchElement(internal, vnode) { patchChildren( internal, newChildren && Array.isArray(newChildren) ? newChildren : [newChildren], - dom + dom, + null ); } } diff --git a/src/internal.d.ts b/src/internal.d.ts index 48fdb33cd1..369d773d24 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -154,7 +154,7 @@ export interface Internal

            { /** temporarily holds previous sibling Internal while diffing. Is purposefully cleared after diffing due to how the diffing algorithm works */ _prev: Internal | null; _index: number; - _prevLDS: Internal | null; + _prevLIS: Internal | null; /** most recent vnode ID */ _vnodeId: number; /** diff --git a/src/tree.js b/src/tree.js index 8c446398a2..ea08d1f039 100644 --- a/src/tree.js +++ b/src/tree.js @@ -97,7 +97,7 @@ export function createInternal(vnode, parentInternal) { _child: null, _next: null, _prev: null, - _prevLDS: null, + _prevLIS: null, _index: -1, _vnodeId: vnodeId, _component: null, From 6be6e890392b39a834ed94fe4e56d6d9c8cb7698 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 5 Feb 2023 01:43:23 -0800 Subject: [PATCH 48/71] Fix null placeholder tracking See test "should support moving Fragments between beginning and end" for an example failure this change fixes --- src/diff/children.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/diff/children.js b/src/diff/children.js index a89946c185..fac0533fbc 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -199,6 +199,10 @@ function findMatches(internal, children, parentInternal) { matchedInternal.flags |= MATCHED_INTERNAL; // Let's update our list of nodes to search to remove the new matchedInternal. + // TODO: Better explain this: Temporarily keep the old next pointer + // around for tracking null placeholders. Particularly examine the + // test "should support moving Fragments between beginning and end" + prevSearch._prevLIS = prevSearch._next; prevSearch._next = matchedInternal._next; break; @@ -233,7 +237,7 @@ function findMatches(internal, children, parentInternal) { prevMatchedInternal = matchedInternal; if (internal && internal._index == index) { - internal = internal._next; + internal = internal._prevLIS || internal._next; } } From 1b940e6dd02cddbf6ff3b94b8fd248a246cbccdd Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 5 Feb 2023 01:48:01 -0800 Subject: [PATCH 49/71] Fix Portal placement See test "should insert a portal before new siblings when changing container to match siblings" --- src/diff/patch.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/diff/patch.js b/src/diff/patch.js index 46525a34de..d1f064c535 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -60,11 +60,7 @@ export function patch(internal, vnode, parentDom, boundaryNode) { parentDom = vnode.props._parentDom; if (internal.props._parentDom !== parentDom) { - // let nextSibling = - // parentDom == prevParentDom ? getDomSibling(internal) : null; - // insert(internal, nextSibling, parentDom); - let nextSibling = - parentDom == prevParentDom ? getDomSibling(internal) : null; + let nextSibling = parentDom == prevParentDom ? boundaryNode : null; insert(internal, parentDom, nextSibling); } } From cf64778671de58f27d78fb697cb8a74339988b07 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 5 Feb 2023 15:33:38 -0800 Subject: [PATCH 50/71] Fix up keyed test assertions --- test/browser/keys.test.js | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/browser/keys.test.js b/test/browser/keys.test.js index 3b21438fac..7564c8b168 100644 --- a/test/browser/keys.test.js +++ b/test/browser/keys.test.js @@ -143,7 +143,7 @@ describe('keys', () => { ); }); - it.skip('should remove keyed nodes (#232)', () => { + it('should remove keyed nodes (#232)', () => { class App extends Component { componentDidMount() { setTimeout(() => this.setState({ opened: true, loading: true }), 10); @@ -313,8 +313,8 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('abcde'); expect(getLog()).to.deep.equal([ - '

              cdeab.insertBefore(
            1. b,
            2. c)', - '
                bcdea.insertBefore(
              1. a,
              2. b)' + '
                  cdeab.insertBefore(
                1. a,
                2. c)', + '
                    acdeb.insertBefore(
                  1. b,
                  2. c)' ]); }); @@ -327,7 +327,7 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('ba'); - expect(getLog()).to.deep.equal(['
                      ab.insertBefore(
                    1. a, Null)']); + expect(getLog()).to.deep.equal(['
                        ab.insertBefore(
                      1. b,
                      2. a)']); }); it('should swap existing keyed children in the middle of a list efficiently', () => { @@ -343,7 +343,7 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('acbd', 'initial swap'); expect(getLog()).to.deep.equal( - ['
                          abcd.insertBefore(
                        1. b,
                        2. d)'], + ['
                            abcd.insertBefore(
                          1. c,
                          2. b)'], 'initial swap' ); @@ -354,7 +354,7 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('abcd', 'swap back'); expect(getLog()).to.deep.equal( - ['
                              acbd.insertBefore(
                            1. c,
                            2. d)'], + ['
                                acbd.insertBefore(
                              1. b,
                              2. c)'], 'swap back' ); }); @@ -429,18 +429,18 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal(values.join('')); - expect(getLog()).to.have.lengthOf(9); - // expect(getLog()).to.deep.equal([ - // '
                                  abcdefghij.insertBefore(
                                1. j,
                                2. a)', - // '
                                    jabcdefghi.insertBefore(
                                  1. i,
                                  2. a)', - // '
                                      jiabcdefgh.insertBefore(
                                    1. h,
                                    2. a)', - // '
                                        jihabcdefg.insertBefore(
                                      1. g,
                                      2. a)', - // '
                                          jihgabcdef.insertBefore(
                                        1. f,
                                        2. a)', - // '
                                            jihgfabcde.insertBefore(
                                          1. e,
                                          2. a)', - // '
                                              jihgfeabcd.insertBefore(
                                            1. d,
                                            2. a)', - // '
                                                jihgfedabc.insertBefore(
                                              1. c,
                                              2. a)', - // '
                                                  jihgfedcab.insertBefore(
                                                1. a, Null)' - // ]); + // expect(getLog()).to.have.lengthOf(9); + expect(getLog()).to.deep.equal([ + '
                                                    abcdefghij.insertBefore(
                                                  1. j,
                                                  2. a)', + '
                                                      jabcdefghi.insertBefore(
                                                    1. i,
                                                    2. a)', + '
                                                        jiabcdefgh.insertBefore(
                                                      1. h,
                                                      2. a)', + '
                                                          jihabcdefg.insertBefore(
                                                        1. g,
                                                        2. a)', + '
                                                            jihgabcdef.insertBefore(
                                                          1. f,
                                                          2. a)', + '
                                                              jihgfabcde.insertBefore(
                                                            1. e,
                                                            2. a)', + '
                                                                jihgfeabcd.insertBefore(
                                                              1. d,
                                                              2. a)', + '
                                                                  jihgfedabc.insertBefore(
                                                                1. c,
                                                                2. a)', + '
                                                                    jihgfedcab.insertBefore(
                                                                  1. b,
                                                                  2. a)' + ]); }); it("should not preserve state when a component's keys are different", () => { @@ -552,8 +552,8 @@ describe('keys', () => { expect(scratch.innerHTML).to.equal(expectedHtml); expect(ops).to.deep.equal([ - 'Unmount Stateful2', 'Unmount Stateful1', + 'Unmount Stateful2', 'Mount Stateful1', 'Mount Stateful2' ]); @@ -565,8 +565,8 @@ describe('keys', () => { expect(scratch.innerHTML).to.equal(expectedHtml); expect(ops).to.deep.equal([ - 'Unmount Stateful2', 'Unmount Stateful1', + 'Unmount Stateful2', 'Mount Stateful1', 'Mount Stateful2' ]); @@ -697,8 +697,8 @@ describe('keys', () => { expect(scratch.innerHTML).to.equal(expectedHtml); expect(ops).to.deep.equal([ - 'Unmount Stateful2', 'Unmount Stateful1', + 'Unmount Stateful2', 'Mount Stateful1', 'Mount Stateful2' ]); @@ -710,8 +710,8 @@ describe('keys', () => { expect(scratch.innerHTML).to.equal(expectedHtml); expect(ops).to.deep.equal([ - 'Unmount Stateful2', 'Unmount Stateful1', + 'Unmount Stateful2', 'Mount Stateful1', 'Mount Stateful2' ]); From c251d82e6338382eafd1aba8e9d9ebe100063e3b Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 5 Feb 2023 02:09:21 -0800 Subject: [PATCH 51/71] Clean up patchChildren implementation * Remove usages of MODE_UNMOUNTING & MATCHED_INTERNAL * Clean up and improve comments --- src/constants.js | 3 -- src/diff/children.js | 77 ++++++++++++++++++-------------------------- 2 files changed, 31 insertions(+), 49 deletions(-) diff --git a/src/constants.js b/src/constants.js index ec8fa4773f..ea627adba7 100644 --- a/src/constants.js +++ b/src/constants.js @@ -51,9 +51,6 @@ export const SKIP_CHILDREN = 1 << 15; /** Indicates that this node needs to be inserted while patching children */ export const INSERT_INTERNAL = 1 << 16; -/** Indicates that this node matched a VNode while diffing and is not going to be unmounted */ -export const MATCHED_INTERNAL = 1 << 17; - /** Reset all mode flags */ export const RESET_MODE = ~( MODE_HYDRATE | diff --git a/src/diff/children.js b/src/diff/children.js index fac0533fbc..fa8c1d5b4e 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -10,9 +10,7 @@ import { UNDEFINED, TYPE_ELEMENT, INSERT_INTERNAL, - TYPE_ROOT, - MODE_UNMOUNTING, - MATCHED_INTERNAL + TYPE_ROOT } from '../constants'; import { mount } from './mount'; import { patch } from './patch'; @@ -109,11 +107,14 @@ function findMatches(internal, children, parentInternal) { // treat this slot in the children array as a null placeholder. We mark // this node as unmounting to prevent it from being used in future // searches for matching internals - internal.flags |= MODE_UNMOUNTING; unmount(internal, internal, 0); // If this internal is the first unmatched internal, then bump our - // pointer to the next node so our search will skip over this internal + // pointer to the next node so our search will skip over this internal. + // + // TODO: What if this node is not the first unmatched Internal (and so + // remains in the search array) and shares the type with another + // Internal that is it matches? Do we have a test for this? if (oldHead == internal) oldHead = oldHead._next; internal = internal._next; @@ -148,14 +149,13 @@ function findMatches(internal, children, parentInternal) { let matchedInternal; if (key == null && internal && index < internal._index) { - // TODO: FIX NULL PLACEHOLDERS // If we are doing an unkeyed diff, and the old index of the current - // internal is greater than the current VNode index, then this vnode - // represents a new element that is mounting into what was previous a null - // placeholder slot. We should create a new internal to mount this VNode. + // internal in the old list of children is greater than the current VNode + // index, then this vnode represents a new element that is mounting into + // what was previous a null placeholder slot. We should create a new + // internal to mount this VNode. } else if ( oldHead && - ~oldHead.flags & MODE_UNMOUNTING && (oldHead.flags & typeFlag) !== 0 && oldHead.type === type && oldHead.key == key @@ -167,9 +167,11 @@ function findMatches(internal, children, parentInternal) { matchedInternal = oldHead; oldHead = oldHead._next; } else if (oldHead) { - // seek forward through the Internals list, starting at the head (either first, or first unused). - // only match unused items, which are internals where _prev === undefined. - // note: _prev=null for the first matched internal, and should be considered "used". + // We need to search for a matching internal for this VNode. We'll start + // at the first unmatched Internal and search all of its siblings. When we + // find a match, we'll remove it from the list of unmatched Internals and + // add to the new list of children internals, whose tail is + // prevMatchedInternal /** @type {Internal} */ let prevSearch = oldHead; @@ -184,21 +186,13 @@ function findMatches(internal, children, parentInternal) { while (search) { const flags = search.flags; - const isUsed = - search.flags & MODE_UNMOUNTING || search.flags & MATCHED_INTERNAL; // Match found! - if ( - !isUsed && - (flags & typeFlag) !== 0 && - search.type === type && - search.key == key - ) { + if (flags & typeFlag && search.type === type && search.key == key) { moved = true; matchedInternal = search; - matchedInternal.flags |= MATCHED_INTERNAL; - // Let's update our list of nodes to search to remove the new matchedInternal. + // Let's update our list of unmatched nodes to remove the new matchedInternal. // TODO: Better explain this: Temporarily keep the old next pointer // around for tracking null placeholders. Particularly examine the // test "should support moving Fragments between beginning and end" @@ -224,23 +218,25 @@ function findMatches(internal, children, parentInternal) { } } - // no match, create a new Internal: + // No match, create a new Internal: if (!matchedInternal) { matchedInternal = createInternal(normalizedVNode, parentInternal); - matchedInternal.flags |= MATCHED_INTERNAL; - // console.log('creating new', internal.type); } - // move into place in new list + // Put matched or new internal into the new list of children if (prevMatchedInternal) prevMatchedInternal._next = matchedInternal; else parentInternal._child = matchedInternal; prevMatchedInternal = matchedInternal; if (internal && internal._index == index) { + // Move forward our tracker for null placeholders internal = internal._prevLIS || internal._next; } } + // Ensure the last node of the last matched internal has a null _next pointer. + // Its possible that it still points to it's old sibling at this point so + // we'll manually clear it here. if (prevMatchedInternal) prevMatchedInternal._next = null; // Step 2. Walk over the unused children and unmount: @@ -287,8 +283,6 @@ function runLIS(internal, parentDom) { // TODO: We do the props._parentDom !== parentDom in a couple places. // Could we do this check once and cache the result in a flag? if (internal.flags & TYPE_ROOT && internal.props._parentDom !== parentDom) { - // if (prevMatchedInternal) prevMatchedInternal._next = internal; - // prevMatchedInternal = internal; internal = internal._next; continue; } @@ -299,17 +293,12 @@ function runLIS(internal, parentDom) { // Skip over newly mounted internals. They will be mounted in place. if (internal._index === -1) { - // if (prevMatchedInternal) prevMatchedInternal._next = internal; - // prevMatchedInternal = internal; internal = internal._next; continue; } if (wipLIS.length == 0) { wipLIS.push(internal); - - // if (prevMatchedInternal) prevMatchedInternal._next = internal; - // prevMatchedInternal = internal; internal = internal._next; continue; } @@ -335,8 +324,6 @@ function runLIS(internal, parentDom) { internal._prevLIS = prevLIS; } - // if (prevMatchedInternal) prevMatchedInternal._next = internal; - // prevMatchedInternal = internal; internal = internal._next; } @@ -349,10 +336,14 @@ function runLIS(internal, parentDom) { while (lisNode) { // This node is on the longest decreasing subsequence so clear INSERT_NODE flag lisNode.flags &= ~INSERT_INTERNAL; - let temp = lisNode._prevLIS; + + // Reverse the _prevLIS linked list + internal = lisNode._prevLIS; lisNode._prevLIS = nextLIS; nextLIS = lisNode; - lisNode = temp; + lisNode = internal; + + // Track the head of the linked list if (lisNode) lisHead = lisNode; } @@ -424,9 +415,6 @@ function insertionLoop(internal, children, parentDom, boundaryNode, lisHead) { } } - internal.flags &= ~INSERT_INTERNAL; - internal.flags &= ~MATCHED_INTERNAL; - let oldRef = internal._prevRef; if (internal.ref != oldRef) { if (oldRef) applyRef(oldRef, null, internal); @@ -434,6 +422,7 @@ function insertionLoop(internal, children, parentDom, boundaryNode, lisHead) { applyRef(internal.ref, internal._component || internal.data, internal); } + internal.flags &= ~INSERT_INTERNAL; internal._prevLIS = null; internal._index = index++; internal = internal._next; @@ -443,13 +432,9 @@ function insertionLoop(internal, children, parentDom, boundaryNode, lisHead) { /** * @param {import('../internal').Internal} internal * @param {import('../internal').PreactNode} parentDom - * @param {import('../internal').PreactNode} [nextSibling] + * @param {import('../internal').PreactNode} nextSibling */ export function insert(internal, parentDom, nextSibling) { - if (nextSibling === undefined) { - nextSibling = getDomSibling(internal); - } - if (internal.flags & TYPE_COMPONENT) { let child = internal._child; while (child) { From 0e3c34c158beb58f7b2689275783b6d6d6df4eba Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 5 Feb 2023 14:18:49 -0800 Subject: [PATCH 52/71] Remove _prev and rename _prevLIS to _tempNext... ... since we are reusing it for different purposes in the diff algorithm --- src/diff/children.js | 57 ++++++++++++++++++++++++++------------------ src/internal.d.ts | 8 ++++--- src/tree.js | 5 ++-- 3 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index fa8c1d5b4e..b341f89f6b 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -15,7 +15,7 @@ import { import { mount } from './mount'; import { patch } from './patch'; import { unmount } from './unmount'; -import { createInternal, getDomSibling, getFirstDom } from '../tree'; +import { createInternal, getFirstDom } from '../tree'; /** * Scenarios: @@ -51,17 +51,23 @@ export function patchChildren( // Step 1. Find matches and set up _next pointers. All unused internals are at // attached to oldHead. // - // TODO: Identity null placeholders by also walking the old internal children - // at the same time. - // - // TODO: Remove usages of MODE_UNMOUNTING and MATCHED_INTERNALS flags since - // they are unnecessary now + // In this step, _tempNext will hold the old next pointer for an internal. + // This algorithm changes `_next` when finding matching internals. This change + // breaks our null placeholder detection logic which compares the old internal + // at a particular index with the new VNode at that index. By using + // `_tempNext` to hold the old next pointers we are able to simultaneously + // iterate over the new VNodes, iterate over the old Internal list, and update + // _next pointers to the new Internals. moved = true; // TODO: Bring back skipping LIS on no moves. `moved` should start out as `false` findMatches(parentInternal._child, children, parentInternal); // Step 3. Find the longest increasing subsequence - // TODO: Replace _prevLDS with _next. Doing this will make _next meaningless for a moment - // TODO: Explore trying to do this without an array, maybe next pointers? Or maybe reuse the array + // + // In this step, `_tempNext` will hold the previous Internal in the longest + // increasing subsequence containing the current Internal. + // + // - [ ] TODO: Explore trying to do this without an array, maybe next + // pointers? Or maybe reuse the array let lisHead = null; if (parentInternal._child && moved) { lisHead = runLIS(parentInternal._child, parentDom); @@ -70,7 +76,8 @@ export function patchChildren( // Step 5. Walk forwards over the newly-assigned _next properties, inserting // Internals that require insertion. We track the next dom sibling Internals - // should be inserted before by walking over the LIS at the same time + // should be inserted before by walking over the LIS (using _tempNext) at the + // same time insertionLoop( parentInternal._child, children, @@ -104,8 +111,8 @@ function findMatches(internal, children, parentInternal) { if (internal && index == internal._index && internal.key == null) { // The current internal is unkeyed, has the same index as this VNode // child, and the VNode is now null. So we'll unmount the Internal and - // treat this slot in the children array as a null placeholder. We mark - // this node as unmounting to prevent it from being used in future + // treat this slot in the children array as a null placeholder. We'll + // eagerly unmount this node to prevent it from being used in future // searches for matching internals unmount(internal, internal, 0); @@ -196,7 +203,7 @@ function findMatches(internal, children, parentInternal) { // TODO: Better explain this: Temporarily keep the old next pointer // around for tracking null placeholders. Particularly examine the // test "should support moving Fragments between beginning and end" - prevSearch._prevLIS = prevSearch._next; + prevSearch._tempNext = prevSearch._next; prevSearch._next = matchedInternal._next; break; @@ -230,13 +237,13 @@ function findMatches(internal, children, parentInternal) { if (internal && internal._index == index) { // Move forward our tracker for null placeholders - internal = internal._prevLIS || internal._next; + internal = internal._tempNext || internal._next; } } // Ensure the last node of the last matched internal has a null _next pointer. - // Its possible that it still points to it's old sibling at this point so - // we'll manually clear it here. + // Its possible that it still points to it's old sibling at the end of Step 1, + // so we'll manually clear it here. if (prevMatchedInternal) prevMatchedInternal._next = null; // Step 2. Walk over the unused children and unmount: @@ -305,7 +312,7 @@ function runLIS(internal, parentDom) { let ldsTail = wipLIS[wipLIS.length - 1]; if (ldsTail._index < internal._index) { - internal._prevLIS = ldsTail; + internal._tempNext = ldsTail; wipLIS.push(internal); } else { // Search for position in wipLIS where node should go. It should replace @@ -321,14 +328,18 @@ function runLIS(internal, parentDom) { wipLIS[i + 1] = internal; let prevLIS = i < 0 ? null : wipLIS[i]; - internal._prevLIS = prevLIS; + internal._tempNext = prevLIS; } internal = internal._next; } // Step 4. Mark internals in longest increasing subsequence and reverse the - // _prevLIS pointers to be _nextLIS pointers for use in the insertion loop + // the longest increasing subsequence linked list. Before this step, _tempNext + // is actual the **previous** Internal in the longest increasing subsequence. + // + // After this step, _tempNext becomes the **next** Internal in the longest + // increasing subsequence. /** @type {Internal | null} */ let lisNode = wipLIS.length ? wipLIS[wipLIS.length - 1] : null; let lisHead = lisNode; @@ -337,9 +348,9 @@ function runLIS(internal, parentDom) { // This node is on the longest decreasing subsequence so clear INSERT_NODE flag lisNode.flags &= ~INSERT_INTERNAL; - // Reverse the _prevLIS linked list - internal = lisNode._prevLIS; - lisNode._prevLIS = nextLIS; + // Reverse the _tempNext LIS linked list + internal = lisNode._tempNext; + lisNode._tempNext = nextLIS; nextLIS = lisNode; lisNode = internal; @@ -380,7 +391,7 @@ function insertionLoop(internal, children, parentDom, boundaryNode, lisHead) { } if (internal === lisNode) { - lisNode = lisNode._prevLIS; + lisNode = lisNode._tempNext; if (lisNode) { nextDomSibling = lisNode.flags & TYPE_DOM ? lisNode.data : getFirstDom(lisNode._child); @@ -423,7 +434,7 @@ function insertionLoop(internal, children, parentDom, boundaryNode, lisHead) { } internal.flags &= ~INSERT_INTERNAL; - internal._prevLIS = null; + internal._tempNext = null; internal._index = index++; internal = internal._next; } diff --git a/src/internal.d.ts b/src/internal.d.ts index 369d773d24..e4b5a2c161 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -151,10 +151,12 @@ export interface Internal

                                                                    { _child: Internal | null; /** next sibling Internal node */ _next: Internal | null; - /** temporarily holds previous sibling Internal while diffing. Is purposefully cleared after diffing due to how the diffing algorithm works */ - _prev: Internal | null; _index: number; - _prevLIS: Internal | null; + /** + * A temporary pointer to another Internal, used for different purposes in + * the patching children algorithm. See comments there for its different uses + * */ + _tempNext: Internal | null; /** most recent vnode ID */ _vnodeId: number; /** diff --git a/src/tree.js b/src/tree.js index ea08d1f039..b0ee6628fe 100644 --- a/src/tree.js +++ b/src/tree.js @@ -96,8 +96,7 @@ export function createInternal(vnode, parentInternal) { _parent: parentInternal, _child: null, _next: null, - _prev: null, - _prevLIS: null, + _tempNext: null, _index: -1, _vnodeId: vnodeId, _component: null, @@ -144,7 +143,7 @@ export function getDomSibling(internal, childIndex) { } /** - * Search the given internal and it's siblings for a DOM element. If the + * Search the given internal and it's next siblings for a DOM element. If the * provided Internal _is_ a DOM Internal, its DOM will be returned. If you want * to find the first DOM node of an Internal's subtree, than pass in * `internal._child` From 994b6e3df4107d0f6a6854ea38ded88e68e13f13 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 5 Feb 2023 15:24:34 -0800 Subject: [PATCH 53/71] Start search from prevMatchedInternal._next --- src/diff/children.js | 38 ++++++++++++++++++++++++++++--------- test/browser/render.test.js | 30 ++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index b341f89f6b..832eb213f2 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -103,6 +103,18 @@ function findMatches(internal, children, parentInternal) { /** @type {Internal} The last matched internal */ let prevMatchedInternal; + /** + * @type {Internal} The previously searched internal, aka the old previous + * Internal of prevMatchedInternal. We store this outside of the loop so we + * can begin our search with prevMatchedInternal._next and have a previous + * Internal to update if prevMatchedInternal._next matches. In other words, it + * allows us to resume our search in the middle of the list of unused + * Internals. We initialize it to oldHead since the first time we enter the + * search loop, we just attempted to match oldHead so oldHead is the previous + * search. + */ + let prevSearch = oldHead; + for (let index = 0; index < children.length; index++) { const vnode = children[index]; @@ -179,15 +191,23 @@ function findMatches(internal, children, parentInternal) { // find a match, we'll remove it from the list of unmatched Internals and // add to the new list of children internals, whose tail is // prevMatchedInternal - /** @type {Internal} */ - let prevSearch = oldHead; - - // TODO: Try to get this optimization to work - // // Let's start our search at the node where our previous match left off. - // // We do this cuz it optimizes the more common case of holes over a keyed - // // shuffles. - // let searchStart = prevMatchedInternal._next; - let searchStart = oldHead._next; + // + // TODO: Measure if starting search from prevMatchedInternal._next is worth it. + // Let's start our search at the node where our previous match left off. + // We do this to optimize for the more common case of holes over keyed + // shuffles + let searchStart; + if ( + prevMatchedInternal && + prevMatchedInternal._next && + prevMatchedInternal._next !== oldHead + ) { + searchStart = prevMatchedInternal._next; + } else { + searchStart = oldHead._next; + prevSearch = oldHead; + } + /** @type {Internal} */ let search = searchStart; diff --git a/test/browser/render.test.js b/test/browser/render.test.js index 9159410654..6988334e8d 100644 --- a/test/browser/render.test.js +++ b/test/browser/render.test.js @@ -1,5 +1,5 @@ import { setupRerender } from 'preact/test-utils'; -import { createElement, render, Component, options } from 'preact'; +import { createElement, render, Component, options, Fragment } from 'preact'; import { setupScratch, teardown, @@ -1224,4 +1224,32 @@ describe('render()', () => { expect(items[0]).to.have.property('parentNode').that.should.exist; expect(items[1]).to.have.property('parentNode').that.should.exist; }); + + it('should replace in-place children with different types', () => { + const B1 = () =>

                                                                    b1
                                                                    ; + const B2 = () =>
                                                                    b2
                                                                    ; + + /** @type {(c: React.JSX.Element) => void} */ + let setChild; + function App() { + // Mimic some state that may cause a suspend + const [child, setChildInternal] = useState(); + setChild = setChildInternal; + + return ( + +
                                                                    a
                                                                    + {child} +
                                                                    c
                                                                    +
                                                                    + ); + } + + render(, scratch); + expect(scratch.innerHTML).to.equal('
                                                                    a
                                                                    b1
                                                                    c
                                                                    '); + + setChild(); + rerender(); + expect(scratch.innerHTML).to.equal('
                                                                    a
                                                                    b2
                                                                    c
                                                                    '); + }); }); From b65e1b62762403080d012977bc6be5a8a7bf62ad Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Tue, 7 Feb 2023 02:08:15 -0800 Subject: [PATCH 54/71] Skip LIS when nothing moves To fix this I had to teach getDomSibling to skip over nodes marked for insertion, as well as properly handle nodes with no children. See the fragment test "should preserve state when it does not change positions" for a test case that was fixed. Note: This breaks a Portal test when changing a portal from having a different parent to the same parent. Probably need to detect that and set the moved flag to true. Maybe we could detect it in findMatches and set the move flag as well as a PORTAL flag on the internal to speed up similar checks --- src/diff/children.js | 71 +++++++++++++++++++++++++++----------------- src/tree.js | 8 ++++- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 832eb213f2..3857114b8a 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -15,7 +15,7 @@ import { import { mount } from './mount'; import { patch } from './patch'; import { unmount } from './unmount'; -import { createInternal, getFirstDom } from '../tree'; +import { createInternal, getDomSibling, getFirstDom } from '../tree'; /** * Scenarios: @@ -30,9 +30,6 @@ import { createInternal, getFirstDom } from '../tree'; /** @typedef {import('../internal').PreactElement} PreactElement */ /** @typedef {import('../internal').ComponentChildren} ComponentChildren */ -/** @type {boolean} */ -let moved; - /** * Update an internal with new children. * @param {Internal} parentInternal The internal whose children should be patched @@ -58,8 +55,7 @@ export function patchChildren( // `_tempNext` to hold the old next pointers we are able to simultaneously // iterate over the new VNodes, iterate over the old Internal list, and update // _next pointers to the new Internals. - moved = true; // TODO: Bring back skipping LIS on no moves. `moved` should start out as `false` - findMatches(parentInternal._child, children, parentInternal); + let moved = findMatches(parentInternal._child, children, parentInternal); // Step 3. Find the longest increasing subsequence // @@ -72,27 +68,31 @@ export function patchChildren( if (parentInternal._child && moved) { lisHead = runLIS(parentInternal._child, parentDom); } - moved = false; // Step 5. Walk forwards over the newly-assigned _next properties, inserting // Internals that require insertion. We track the next dom sibling Internals // should be inserted before by walking over the LIS (using _tempNext) at the // same time - insertionLoop( - parentInternal._child, - children, - parentDom, - boundaryNode, - lisHead - ); + if (parentInternal._child) { + insertionLoop( + parentInternal._child, + children, + parentDom, + boundaryNode, + lisHead + ); + } } /** * @param {Internal} internal * @param {ComponentChildren[]} children * @param {Internal} parentInternal + * @returns {boolean} */ function findMatches(internal, children, parentInternal) { + let moved = false; + /** @type {Internal} */ // let internal = parentInternal._child; parentInternal._child = null; @@ -268,6 +268,8 @@ function findMatches(internal, children, parentInternal) { // Step 2. Walk over the unused children and unmount: unmountUnusedChildren(oldHead); + + return moved; } /** @@ -393,14 +395,13 @@ function insertionLoop(internal, children, parentDom, boundaryNode, lisHead) { let lisNode = lisHead; /** @type {PreactElement} The DOM element of the next LIS internal */ let nextDomSibling; - if (lisNode) { - if (lisNode.flags & TYPE_DOM) { - nextDomSibling = lisNode.data; - } else { - nextDomSibling = getFirstDom(lisNode._child); - } + if (lisHead) { + // If lisHead is non-null, then we have a LIS sequence of in-place Internal + // we can use to determine our next DOM sibling + nextDomSibling = + lisNode.flags & TYPE_DOM ? lisNode.data : getFirstDom(lisNode._child); } else { - nextDomSibling = boundaryNode; + nextDomSibling = getDomSibling(internal) || boundaryNode; } let index = 0; @@ -410,14 +411,28 @@ function insertionLoop(internal, children, parentDom, boundaryNode, lisHead) { vnode = children[++index]; } - if (internal === lisNode) { - lisNode = lisNode._tempNext; - if (lisNode) { - nextDomSibling = - lisNode.flags & TYPE_DOM ? lisNode.data : getFirstDom(lisNode._child); - } else { - nextDomSibling = boundaryNode; + if (lisHead) { + // If lisHead is non-null, then we have a LIS sequence of in-place + // Internal we can use to determine our next DOM sibling. If this internal + // is the current internal in our LIS in-place sequence, then let's go to + // the next Internal in the sequence and use it's DOM node as our new + // nextSibling + if (internal === lisNode) { + lisNode = lisNode._tempNext; + if (lisNode) { + nextDomSibling = + lisNode.flags & TYPE_DOM + ? lisNode.data + : getFirstDom(lisNode._child); + } else { + nextDomSibling = boundaryNode; + } } + } else { + // We may not have an LIS sequence if nothing moved. If so, we'll need to + // manually compute the next dom sibling in case any children below us do + // insertions before it. + nextDomSibling = getDomSibling(internal) || boundaryNode; } if (internal._index === -1) { diff --git a/src/tree.js b/src/tree.js index b0ee6628fe..82d9515c09 100644 --- a/src/tree.js +++ b/src/tree.js @@ -9,7 +9,8 @@ import { TYPE_COMPONENT, TYPE_DOM, MODE_SVG, - UNDEFINED + UNDEFINED, + INSERT_INTERNAL } from './constants'; import { enqueueRender } from './component'; @@ -153,6 +154,11 @@ export function getDomSibling(internal, childIndex) { */ export function getFirstDom(internal) { while (internal) { + if (internal.flags & INSERT_INTERNAL) { + internal = internal._next; + continue; + } + // this is a DOM internal if (internal.flags & TYPE_DOM) { // @ts-ignore this is an Element Internal, .data is a PreactElement. From fba212908a3b76e69f5ac5b8428c4ff358d27351 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Tue, 7 Feb 2023 03:00:17 -0800 Subject: [PATCH 55/71] Remove usage of boundary node parameter Speeds up benchmark perf on many_updates --- src/component.js | 4 +-- src/create-root.js | 3 +- src/diff/children.js | 83 ++++++++++++++++++-------------------------- src/diff/patch.js | 15 ++++---- 4 files changed, 43 insertions(+), 62 deletions(-) diff --git a/src/component.js b/src/component.js index fcdf49f0dc..e495a1847e 100644 --- a/src/component.js +++ b/src/component.js @@ -3,7 +3,7 @@ import options from './options'; import { createElement, Fragment } from './create-element'; import { patch } from './diff/patch'; import { DIRTY_BIT, FORCE_UPDATE, MODE_UNMOUNTING } from './constants'; -import { getDomSibling, getParentDom } from './tree'; +import { getParentDom } from './tree'; import { inEvent } from './diff/props'; export let ENABLE_CLASSES = false; @@ -102,7 +102,7 @@ function renderQueuedInternal(internal) { const vnode = createElement(internal.type, internal.props); vnode.props = internal.props; - patch(internal, vnode, getParentDom(internal), getDomSibling(internal)); + patch(internal, vnode, getParentDom(internal)); commitRoot(internal); } diff --git a/src/create-root.js b/src/create-root.js index 9fbc639aa3..c40327efbe 100644 --- a/src/create-root.js +++ b/src/create-root.js @@ -30,8 +30,7 @@ export function createRoot(parentDom) { /** @type {import('./internal').PreactElement} */ (parentDom.firstChild); if (rootInternal) { - // TODO: Huh...... Do we have to assume the boundaryNode is null? - patch(rootInternal, vnode, parentDom, null); + patch(rootInternal, vnode, parentDom); } else { rootInternal = createInternal(vnode); diff --git a/src/diff/children.js b/src/diff/children.js index 3857114b8a..71be6fe129 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -35,16 +35,8 @@ import { createInternal, getDomSibling, getFirstDom } from '../tree'; * @param {Internal} parentInternal The internal whose children should be patched * @param {ComponentChildren[]} children The new children, represented as VNodes * @param {PreactElement} parentDom The element into which this subtree is rendered - * @param {PreactElement} boundaryNode The DOM element that all children of this internal - * should be inserted before, i.e. the DOM boundary or edge of this Internal, a.k.a. the next DOM sibling of - * this internal */ -export function patchChildren( - parentInternal, - children, - parentDom, - boundaryNode -) { +export function patchChildren(parentInternal, children, parentDom) { // Step 1. Find matches and set up _next pointers. All unused internals are at // attached to oldHead. // @@ -74,13 +66,7 @@ export function patchChildren( // should be inserted before by walking over the LIS (using _tempNext) at the // same time if (parentInternal._child) { - insertionLoop( - parentInternal._child, - children, - parentDom, - boundaryNode, - lisHead - ); + insertionLoop(parentInternal._child, children, parentDom, lisHead); } } @@ -255,6 +241,10 @@ function findMatches(internal, children, parentInternal) { else parentInternal._child = matchedInternal; prevMatchedInternal = matchedInternal; + // TODO: Consider detecting if an internal is of TYPE_ROOT, whether or not + // it is a PORTAL, and setting a flag as such to use in getDomSibling and + // getFirstDom + if (internal && internal._index == index) { // Move forward our tracker for null placeholders internal = internal._tempNext || internal._next; @@ -387,21 +377,19 @@ function runLIS(internal, parentDom) { * @param {Internal} internal * @param {ComponentChildren[]} children * @param {PreactElement} parentDom - * @param {PreactElement} boundaryNode * @param {Internal} lisHead */ -function insertionLoop(internal, children, parentDom, boundaryNode, lisHead) { +function insertionLoop(internal, children, parentDom, lisHead) { /** @type {Internal} The next in-place Internal whose DOM previous Internals should be inserted before */ let lisNode = lisHead; - /** @type {PreactElement} The DOM element of the next LIS internal */ + /** @type {PreactElement | null} The DOM element of the next LIS internal */ let nextDomSibling; + + // If lisHead is non-null, then we have a LIS sequence of in-place Internal + // we can use to determine our next DOM sibling if (lisHead) { - // If lisHead is non-null, then we have a LIS sequence of in-place Internal - // we can use to determine our next DOM sibling nextDomSibling = lisNode.flags & TYPE_DOM ? lisNode.data : getFirstDom(lisNode._child); - } else { - nextDomSibling = getDomSibling(internal) || boundaryNode; } let index = 0; @@ -411,38 +399,32 @@ function insertionLoop(internal, children, parentDom, boundaryNode, lisHead) { vnode = children[++index]; } - if (lisHead) { - // If lisHead is non-null, then we have a LIS sequence of in-place - // Internal we can use to determine our next DOM sibling. If this internal - // is the current internal in our LIS in-place sequence, then let's go to - // the next Internal in the sequence and use it's DOM node as our new - // nextSibling - if (internal === lisNode) { - lisNode = lisNode._tempNext; - if (lisNode) { - nextDomSibling = - lisNode.flags & TYPE_DOM - ? lisNode.data - : getFirstDom(lisNode._child); - } else { - nextDomSibling = boundaryNode; - } + // If lisHead is non-null, then we have a LIS sequence of in-place + // Internals we can use to determine our next DOM sibling. If this internal + // is the current internal in our LIS in-place sequence, then let's go to + // the next Internal in the sequence and use it's DOM node as our new + // nextSibling + if (lisHead && internal === lisNode) { + lisNode = lisNode._tempNext; + if (lisNode) { + nextDomSibling = + lisNode.flags & TYPE_DOM ? lisNode.data : getFirstDom(lisNode._child); + } else { + nextDomSibling = getDomSibling(internal); } - } else { - // We may not have an LIS sequence if nothing moved. If so, we'll need to - // manually compute the next dom sibling in case any children below us do - // insertions before it. - nextDomSibling = getDomSibling(internal) || boundaryNode; } if (internal._index === -1) { - mount(internal, parentDom, nextDomSibling); + let mountNextDomSibling = lisHead + ? nextDomSibling + : getDomSibling(internal); + mount(internal, parentDom, mountNextDomSibling); if (internal.flags & TYPE_DOM) { // If we are mounting a component, it's DOM children will get inserted // into the DOM in mountChildren. If we are mounting a DOM node, then // it's children will be mounted into itself and we need to insert this // DOM in place. - insert(internal, parentDom, nextDomSibling); + insert(internal, parentDom, mountNextDomSibling); } } else if ( (internal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) === @@ -453,11 +435,14 @@ function insertionLoop(internal, children, parentDom, boundaryNode, lisHead) { patch( internal, Array.isArray(vnode) ? createElement(Fragment, null, vnode) : vnode, - parentDom, - nextDomSibling + parentDom ); if (internal.flags & INSERT_INTERNAL) { - insert(internal, parentDom, nextDomSibling); + insert( + internal, + parentDom, + lisHead ? nextDomSibling : getDomSibling(internal) + ); } } diff --git a/src/diff/patch.js b/src/diff/patch.js index d1f064c535..f0c522719e 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -29,11 +29,8 @@ import { commitQueue } from './commit'; * @param {import('../internal').Internal} internal The Internal node to patch * @param {import('../internal').ComponentChild} vnode The new virtual node * @param {import('../internal').PreactElement} parentDom The element into which this subtree is rendered - * @param {import('../internal').PreactElement} boundaryNode The DOM element that all children of this internal - * should be inserted before, i.e. the DOM boundary or edge of this Internal, a.k.a. the next DOM sibling of - * this internal */ -export function patch(internal, vnode, parentDom, boundaryNode) { +export function patch(internal, vnode, parentDom) { let flags = internal.flags; if (flags & TYPE_TEXT) { @@ -60,7 +57,8 @@ export function patch(internal, vnode, parentDom, boundaryNode) { parentDom = vnode.props._parentDom; if (internal.props._parentDom !== parentDom) { - let nextSibling = parentDom == prevParentDom ? boundaryNode : null; + let nextSibling = + parentDom == prevParentDom ? getDomSibling(internal) : null; insert(internal, parentDom, nextSibling); } } @@ -99,11 +97,11 @@ export function patch(internal, vnode, parentDom, boundaryNode) { ? internal.data : internal.flags & MODE_HYDRATE ? null - : boundaryNode; + : getDomSibling(internal); mountChildren(internal, renderResult, parentDom, siblingDom); } else { - patchChildren(internal, renderResult, parentDom, boundaryNode); + patchChildren(internal, renderResult, parentDom); } // TODO: for some reason lazy-hydration stops working as .data is assigned a DOM-node @@ -189,8 +187,7 @@ function patchElement(internal, vnode) { patchChildren( internal, newChildren && Array.isArray(newChildren) ? newChildren : [newChildren], - dom, - null + dom ); } } From 5c955a1020a60fca26a9ce6a9249dccee21f5695 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 12 Feb 2023 00:57:36 -0800 Subject: [PATCH 56/71] Use Internal class --- src/tree.js | 68 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/src/tree.js b/src/tree.js index 82d9515c09..bd092acec9 100644 --- a/src/tree.js +++ b/src/tree.js @@ -82,34 +82,68 @@ export function createInternal(vnode, parentInternal) { } /** @type {import('./internal').Internal} */ - const internal = { + // const internal = { + // type, + // props, + // key, + // ref, + // _prevRef: null, + // data: + // flags & TYPE_COMPONENT + // ? { _commitCallbacks: [], _stateCallbacks: [] } + // : null, + // rerender: enqueueRender, + // flags, + // _parent: parentInternal, + // _child: null, + // _next: null, + // _tempNext: null, + // _index: -1, + // _vnodeId: vnodeId, + // _component: null, + // _context: null, + // _depth: parentInternal ? parentInternal._depth + 1 : 0 + // }; + const internal = new Internal( type, props, key, ref, - _prevRef: null, - data: - flags & TYPE_COMPONENT - ? { _commitCallbacks: [], _stateCallbacks: [] } - : null, - rerender: enqueueRender, flags, - _parent: parentInternal, - _child: null, - _next: null, - _tempNext: null, - _index: -1, - _vnodeId: vnodeId, - _component: null, - _context: null, - _depth: parentInternal ? parentInternal._depth + 1 : 0 - }; + parentInternal, + vnodeId + ); if (options._internal) options._internal(internal, vnode); return internal; } +class Internal { + constructor(type, props, key, ref, flags, parentInternal, vnodeId) { + this.type = type; + this.props = props; + this.key = key; + this.ref = ref; + this._prevRef = null; + this.data = + flags & TYPE_COMPONENT + ? { _commitCallbacks: [], _stateCallbacks: [] } + : null; + this.rerender = enqueueRender; + this.flags = flags; + this._parent = parentInternal; + this._child = null; + this._next = null; + this._tempNext = null; + this._index = -1; + this._vnodeId = vnodeId; + this._component = null; + this._context = null; + this._depth = parentInternal ? parentInternal._depth + 1 : 0; + } +} + /** @type {(internal: import('./internal').Internal) => boolean} */ const shouldSearchComponent = internal => internal.flags & TYPE_COMPONENT && From 61985ec10ded2aa481ded841230e14f8f7c29700 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 12 Feb 2023 00:58:33 -0800 Subject: [PATCH 57/71] Use key and type map instead of loop --- src/diff/children.js | 173 ++++++++++++++++++++++++++++++------------- 1 file changed, 120 insertions(+), 53 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 71be6fe129..240fed0454 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -89,6 +89,11 @@ function findMatches(internal, children, parentInternal) { /** @type {Internal} The last matched internal */ let prevMatchedInternal; + /** @type {Record} */ + let keyMap; + /** @type {Map} */ + let typeMap; + /** * @type {Internal} The previously searched internal, aka the old previous * Internal of prevMatchedInternal. We store this outside of the loop so we @@ -160,6 +165,7 @@ function findMatches(internal, children, parentInternal) { // what was previous a null placeholder slot. We should create a new // internal to mount this VNode. } else if ( + !keyMap && oldHead && (oldHead.flags & typeFlag) !== 0 && oldHead.type === type && @@ -172,61 +178,96 @@ function findMatches(internal, children, parentInternal) { matchedInternal = oldHead; oldHead = oldHead._next; } else if (oldHead) { - // We need to search for a matching internal for this VNode. We'll start - // at the first unmatched Internal and search all of its siblings. When we - // find a match, we'll remove it from the list of unmatched Internals and - // add to the new list of children internals, whose tail is - // prevMatchedInternal - // - // TODO: Measure if starting search from prevMatchedInternal._next is worth it. - // Let's start our search at the node where our previous match left off. - // We do this to optimize for the more common case of holes over keyed - // shuffles - let searchStart; - if ( - prevMatchedInternal && - prevMatchedInternal._next && - prevMatchedInternal._next !== oldHead - ) { - searchStart = prevMatchedInternal._next; - } else { - searchStart = oldHead._next; - prevSearch = oldHead; - } - + // // We need to search for a matching internal for this VNode. We'll start + // // at the first unmatched Internal and search all of its siblings. When we + // // find a match, we'll remove it from the list of unmatched Internals and + // // add to the new list of children internals, whose tail is + // // prevMatchedInternal + // // + // // TODO: Measure if starting search from prevMatchedInternal._next is worth it. + // // Let's start our search at the node where our previous match left off. + // // We do this to optimize for the more common case of holes over keyed + // // shuffles + // let searchStart; + // if ( + // prevMatchedInternal && + // prevMatchedInternal._next && + // prevMatchedInternal._next !== oldHead + // ) { + // searchStart = prevMatchedInternal._next; + // } else { + // searchStart = oldHead._next; + // prevSearch = oldHead; + // } + + // /** @type {Internal} */ + // let search = searchStart; + + // while (search) { + // if ( + // search.flags & typeFlag && + // search.type === type && + // search.key == key + // ) { + // // Match found! + // moved = true; + // matchedInternal = search; + + // // Let's update our list of unmatched nodes to remove the new matchedInternal. + // // TODO: Better explain this: Temporarily keep the old next pointer + // // around for tracking null placeholders. Particularly examine the + // // test "should support moving Fragments between beginning and end" + // prevSearch._tempNext = prevSearch._next; + // prevSearch._next = matchedInternal._next; + + // break; + // } + + // // No match found. Let's move our pointers to the next node in our + // // search. + // prevSearch = search; + + // // If our current node we are searching has a _next node, then let's + // // continue from there. If it doesn't, let's loop back around to the + // // start of the list of unmatched nodes (i.e. oldHead). + // search = search._next ? search._next : oldHead._next; + // if (search === searchStart) { + // // However, it's possible that oldHead was the start of our search. If + // // so, we can stop searching. No match was found. + // break; + // } + // } + + /* Keyed search */ /** @type {Internal} */ - let search = searchStart; - - while (search) { - const flags = search.flags; - - // Match found! - if (flags & typeFlag && search.type === type && search.key == key) { + let search; + if (!keyMap) { + keyMap = {}; + typeMap = new Map(); + search = oldHead; + while (search) { + if (search.key) { + keyMap[search.key] = search; + } else if (!typeMap.has(search.type)) { + typeMap.set(search.type, [search]); + } else { + typeMap.get(search.type).push(search); + } + search = search._next; + } + } + if (key == null) { + search = typeMap.get(type); + if (search && search.length) { moved = true; - matchedInternal = search; - - // Let's update our list of unmatched nodes to remove the new matchedInternal. - // TODO: Better explain this: Temporarily keep the old next pointer - // around for tracking null placeholders. Particularly examine the - // test "should support moving Fragments between beginning and end" - prevSearch._tempNext = prevSearch._next; - prevSearch._next = matchedInternal._next; - - break; + matchedInternal = search.shift(); } - - // No match found. Let's move our pointers to the next node in our - // search. - prevSearch = search; - - // If our current node we are searching has a _next node, then let's - // continue from there. If it doesn't, let's loop back around to the - // start of the list of unmatched nodes (i.e. oldHead). - search = search._next ? search._next : oldHead._next; - if (search === searchStart) { - // However, it's possible that oldHead was the start of our search. If - // so, we can stop searching. No match was found. - break; + } else { + search = keyMap[key]; + if (search && search.type == type) { + moved = true; + keyMap[search.key] = null; + matchedInternal = search; } } } @@ -257,11 +298,37 @@ function findMatches(internal, children, parentInternal) { if (prevMatchedInternal) prevMatchedInternal._next = null; // Step 2. Walk over the unused children and unmount: - unmountUnusedChildren(oldHead); + // unmountUnusedChildren(oldHead); + if (keyMap) { + unmountUnusedKeyedChildren(keyMap, typeMap); + } else if (oldHead) { + unmountUnusedChildren(oldHead); + } return moved; } +/** + * + * @param {Record} keyMap + * @param {Map} typeMap + */ +function unmountUnusedKeyedChildren(keyMap, typeMap) { + let key, internal; + for (key in keyMap) { + internal = keyMap[key]; + if (internal) { + unmount(internal, internal, 0); + } + } + + for (key of typeMap.values()) { + for (internal of key) { + unmount(internal, internal, 0); + } + } +} + /** * @param {Internal} internal */ From 72550c460da96107ee8dfeb12229a3d09a352a24 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 12 Feb 2023 01:17:20 -0800 Subject: [PATCH 58/71] Use map instead of object for keys --- src/diff/children.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 240fed0454..ef6e1f5cc8 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -89,7 +89,7 @@ function findMatches(internal, children, parentInternal) { /** @type {Internal} The last matched internal */ let prevMatchedInternal; - /** @type {Record} */ + /** @type {Map} */ let keyMap; /** @type {Map} */ let typeMap; @@ -242,12 +242,12 @@ function findMatches(internal, children, parentInternal) { /** @type {Internal} */ let search; if (!keyMap) { - keyMap = {}; + keyMap = new Map(); typeMap = new Map(); search = oldHead; while (search) { if (search.key) { - keyMap[search.key] = search; + keyMap.set(search.key, search); } else if (!typeMap.has(search.type)) { typeMap.set(search.type, [search]); } else { @@ -263,10 +263,10 @@ function findMatches(internal, children, parentInternal) { matchedInternal = search.shift(); } } else { - search = keyMap[key]; + search = keyMap.get(key); if (search && search.type == type) { moved = true; - keyMap[search.key] = null; + keyMap.delete(key); matchedInternal = search; } } @@ -309,17 +309,13 @@ function findMatches(internal, children, parentInternal) { } /** - * - * @param {Record} keyMap + * @param {Map} keyMap * @param {Map} typeMap */ function unmountUnusedKeyedChildren(keyMap, typeMap) { let key, internal; - for (key in keyMap) { - internal = keyMap[key]; - if (internal) { - unmount(internal, internal, 0); - } + for (internal of keyMap.values()) { + unmount(internal, internal, 0); } for (key of typeMap.values()) { From 869a20bc43810d41de79b9d688fc92daa6c73efe Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 12 Feb 2023 13:31:55 -0800 Subject: [PATCH 59/71] Use a single map for types and keys --- src/diff/children.js | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index ef6e1f5cc8..cac96043c9 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -89,10 +89,8 @@ function findMatches(internal, children, parentInternal) { /** @type {Internal} The last matched internal */ let prevMatchedInternal; - /** @type {Map} */ + /** @type {Map} */ let keyMap; - /** @type {Map} */ - let typeMap; /** * @type {Internal} The previously searched internal, aka the old previous @@ -243,21 +241,20 @@ function findMatches(internal, children, parentInternal) { let search; if (!keyMap) { keyMap = new Map(); - typeMap = new Map(); search = oldHead; while (search) { if (search.key) { keyMap.set(search.key, search); - } else if (!typeMap.has(search.type)) { - typeMap.set(search.type, [search]); + } else if (!keyMap.has(search.type)) { + keyMap.set(search.type, [search]); } else { - typeMap.get(search.type).push(search); + keyMap.get(search.type).push(search); } search = search._next; } } if (key == null) { - search = typeMap.get(type); + search = keyMap.get(type); if (search && search.length) { moved = true; matchedInternal = search.shift(); @@ -300,7 +297,7 @@ function findMatches(internal, children, parentInternal) { // Step 2. Walk over the unused children and unmount: // unmountUnusedChildren(oldHead); if (keyMap) { - unmountUnusedKeyedChildren(keyMap, typeMap); + unmountUnusedKeyedChildren(keyMap); } else if (oldHead) { unmountUnusedChildren(oldHead); } @@ -309,17 +306,15 @@ function findMatches(internal, children, parentInternal) { } /** - * @param {Map} keyMap - * @param {Map} typeMap + * @param {Map} keyMap */ -function unmountUnusedKeyedChildren(keyMap, typeMap) { - let key, internal; - for (internal of keyMap.values()) { - unmount(internal, internal, 0); - } - - for (key of typeMap.values()) { - for (internal of key) { +function unmountUnusedKeyedChildren(keyMap) { + for (let internal of keyMap.values()) { + if (Array.isArray(internal)) { + for (let i of internal) { + unmount(i, i, 0); + } + } else { unmount(internal, internal, 0); } } From 5de56b05b7b22a895cdcf40fdd230230d4878239 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 12 Feb 2023 23:47:28 -0800 Subject: [PATCH 60/71] Remove LIS algo --- src/diff/children.js | 104 ++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 61 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index cac96043c9..26c5b50da2 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -47,26 +47,14 @@ export function patchChildren(parentInternal, children, parentDom) { // `_tempNext` to hold the old next pointers we are able to simultaneously // iterate over the new VNodes, iterate over the old Internal list, and update // _next pointers to the new Internals. - let moved = findMatches(parentInternal._child, children, parentInternal); - - // Step 3. Find the longest increasing subsequence - // - // In this step, `_tempNext` will hold the previous Internal in the longest - // increasing subsequence containing the current Internal. - // - // - [ ] TODO: Explore trying to do this without an array, maybe next - // pointers? Or maybe reuse the array - let lisHead = null; - if (parentInternal._child && moved) { - lisHead = runLIS(parentInternal._child, parentDom); - } + findMatches(parentInternal._child, children, parentInternal); // Step 5. Walk forwards over the newly-assigned _next properties, inserting // Internals that require insertion. We track the next dom sibling Internals // should be inserted before by walking over the LIS (using _tempNext) at the // same time if (parentInternal._child) { - insertionLoop(parentInternal._child, children, parentDom, lisHead); + insertionLoop(parentInternal._child, children, parentDom); } } @@ -74,11 +62,8 @@ export function patchChildren(parentInternal, children, parentDom) { * @param {Internal} internal * @param {ComponentChildren[]} children * @param {Internal} parentInternal - * @returns {boolean} */ function findMatches(internal, children, parentInternal) { - let moved = false; - /** @type {Internal} */ // let internal = parentInternal._child; parentInternal._child = null; @@ -92,6 +77,9 @@ function findMatches(internal, children, parentInternal) { /** @type {Map} */ let keyMap; + /** @type {number} Tracks the index of the last Internal we decided was "in-place" and did not need insertion */ + let lastPlacedIndex = 0; + /** * @type {Internal} The previously searched internal, aka the old previous * Internal of prevMatchedInternal. We store this outside of the loop so we @@ -208,7 +196,6 @@ function findMatches(internal, children, parentInternal) { // search.key == key // ) { // // Match found! - // moved = true; // matchedInternal = search; // // Let's update our list of unmatched nodes to remove the new matchedInternal. @@ -256,13 +243,11 @@ function findMatches(internal, children, parentInternal) { if (key == null) { search = keyMap.get(type); if (search && search.length) { - moved = true; matchedInternal = search.shift(); } } else { search = keyMap.get(key); if (search && search.type == type) { - moved = true; keyMap.delete(key); matchedInternal = search; } @@ -272,6 +257,19 @@ function findMatches(internal, children, parentInternal) { // No match, create a new Internal: if (!matchedInternal) { matchedInternal = createInternal(normalizedVNode, parentInternal); + } else if (matchedInternal._index < lastPlacedIndex) { + // If the matched internal has moved such that it is now after the last + // internal we determined was "in-place", mark it for insertion to move it + // into the correct place + matchedInternal.flags |= INSERT_INTERNAL; + } else { + // If the matched internal's oldIndex is greater the index of the last + // internal we determined was "in-place", make this internal the new + // "in-place" internal. Doing this (only moving the index forward when + // matching internals old index is grater) better accommodates more + // scenarios such as unmounting Internals at the beginning and middle of + // lists + lastPlacedIndex = matchedInternal._index; } // Put matched or new internal into the new list of children @@ -301,8 +299,6 @@ function findMatches(internal, children, parentInternal) { } else if (oldHead) { unmountUnusedChildren(oldHead); } - - return moved; } /** @@ -435,21 +431,8 @@ function runLIS(internal, parentDom) { * @param {Internal} internal * @param {ComponentChildren[]} children * @param {PreactElement} parentDom - * @param {Internal} lisHead */ -function insertionLoop(internal, children, parentDom, lisHead) { - /** @type {Internal} The next in-place Internal whose DOM previous Internals should be inserted before */ - let lisNode = lisHead; - /** @type {PreactElement | null} The DOM element of the next LIS internal */ - let nextDomSibling; - - // If lisHead is non-null, then we have a LIS sequence of in-place Internal - // we can use to determine our next DOM sibling - if (lisHead) { - nextDomSibling = - lisNode.flags & TYPE_DOM ? lisNode.data : getFirstDom(lisNode._child); - } - +function insertionLoop(internal, children, parentDom) { let index = 0; while (internal) { let vnode = children[index]; @@ -457,32 +440,15 @@ function insertionLoop(internal, children, parentDom, lisHead) { vnode = children[++index]; } - // If lisHead is non-null, then we have a LIS sequence of in-place - // Internals we can use to determine our next DOM sibling. If this internal - // is the current internal in our LIS in-place sequence, then let's go to - // the next Internal in the sequence and use it's DOM node as our new - // nextSibling - if (lisHead && internal === lisNode) { - lisNode = lisNode._tempNext; - if (lisNode) { - nextDomSibling = - lisNode.flags & TYPE_DOM ? lisNode.data : getFirstDom(lisNode._child); - } else { - nextDomSibling = getDomSibling(internal); - } - } - if (internal._index === -1) { - let mountNextDomSibling = lisHead - ? nextDomSibling - : getDomSibling(internal); - mount(internal, parentDom, mountNextDomSibling); + let nextDomSibling = getDomSibling(internal); + mount(internal, parentDom, nextDomSibling); if (internal.flags & TYPE_DOM) { // If we are mounting a component, it's DOM children will get inserted // into the DOM in mountChildren. If we are mounting a DOM node, then // it's children will be mounted into itself and we need to insert this // DOM in place. - insert(internal, parentDom, mountNextDomSibling); + insert(internal, parentDom, nextDomSibling); } } else if ( (internal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) === @@ -495,12 +461,28 @@ function insertionLoop(internal, children, parentDom, lisHead) { Array.isArray(vnode) ? createElement(Fragment, null, vnode) : vnode, parentDom ); + // if (internal._index < lastPlacedIndex) { + // if ((internal.flags & INSERT_INTERNAL) !== INSERT_INTERNAL) { + // console.log( + // '======================= EXPECTED INSERTION', + // internal.flags & INSERT_INTERNAL, + // INSERT_INTERNAL, + // internal.key + // ); + // } + // let sibling = internal._next; + // while (sibling && sibling._index < lastPlacedIndex) { + // sibling = sibling._next; + // } + // insert(internal, parentDom, sibling ? getFirstDom(sibling) : null); + // } else { + // if ((internal.flags & INSERT_INTERNAL) !== 0) { + // console.log('======================= DID NOT EXPECT INSERTION'); + // } + // lastPlacedIndex = internal._index; + // } if (internal.flags & INSERT_INTERNAL) { - insert( - internal, - parentDom, - lisHead ? nextDomSibling : getDomSibling(internal) - ); + insert(internal, parentDom, getDomSibling(internal)); } } From 4e9b5e50c3e41a9c12ce36b808a6027419ae46c2 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 17 Feb 2023 14:35:26 -0800 Subject: [PATCH 61/71] Removed unused code, namely LIS algo and keyed loop search --- src/diff/children.js | 216 ++----------------------------------------- 1 file changed, 10 insertions(+), 206 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 26c5b50da2..dc2ce289ff 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -1,21 +1,18 @@ import { applyRef } from './refs'; -import { createElement, Fragment, normalizeToVNode } from '../create-element'; +import { createElement, Fragment } from '../create-element'; import { TYPE_COMPONENT, TYPE_TEXT, MODE_HYDRATE, MODE_SUSPENDED, - EMPTY_ARR, TYPE_DOM, - UNDEFINED, TYPE_ELEMENT, - INSERT_INTERNAL, - TYPE_ROOT + INSERT_INTERNAL } from '../constants'; import { mount } from './mount'; import { patch } from './patch'; import { unmount } from './unmount'; -import { createInternal, getDomSibling, getFirstDom } from '../tree'; +import { createInternal, getDomSibling } from '../tree'; /** * Scenarios: @@ -59,39 +56,26 @@ export function patchChildren(parentInternal, children, parentDom) { } /** - * @param {Internal} internal + * @param {Internal | null} internal * @param {ComponentChildren[]} children * @param {Internal} parentInternal */ function findMatches(internal, children, parentInternal) { /** @type {Internal} */ - // let internal = parentInternal._child; parentInternal._child = null; - /** @type {Internal} The start of the list of unmatched Internals */ + /** @type {Internal | null} The start of the list of unmatched Internals */ let oldHead = internal; - /** @type {Internal} The last matched internal */ + /** @type {Internal | undefined} The last matched internal */ let prevMatchedInternal; - /** @type {Map} */ + /** @type {Map | undefined} */ let keyMap; /** @type {number} Tracks the index of the last Internal we decided was "in-place" and did not need insertion */ let lastPlacedIndex = 0; - /** - * @type {Internal} The previously searched internal, aka the old previous - * Internal of prevMatchedInternal. We store this outside of the loop so we - * can begin our search with prevMatchedInternal._next and have a previous - * Internal to update if prevMatchedInternal._next matches. In other words, it - * allows us to resume our search in the middle of the list of unused - * Internals. We initialize it to oldHead since the first time we enter the - * search loop, we just attempted to match oldHead so oldHead is the previous - * search. - */ - let prevSearch = oldHead; - for (let index = 0; index < children.length; index++) { const vnode = children[index]; @@ -141,7 +125,7 @@ function findMatches(internal, children, parentInternal) { key = normalizedVNode.key; } - /** @type {Internal?} */ + /** @type {Internal | undefined} */ let matchedInternal; if (key == null && internal && index < internal._index) { @@ -164,65 +148,6 @@ function findMatches(internal, children, parentInternal) { matchedInternal = oldHead; oldHead = oldHead._next; } else if (oldHead) { - // // We need to search for a matching internal for this VNode. We'll start - // // at the first unmatched Internal and search all of its siblings. When we - // // find a match, we'll remove it from the list of unmatched Internals and - // // add to the new list of children internals, whose tail is - // // prevMatchedInternal - // // - // // TODO: Measure if starting search from prevMatchedInternal._next is worth it. - // // Let's start our search at the node where our previous match left off. - // // We do this to optimize for the more common case of holes over keyed - // // shuffles - // let searchStart; - // if ( - // prevMatchedInternal && - // prevMatchedInternal._next && - // prevMatchedInternal._next !== oldHead - // ) { - // searchStart = prevMatchedInternal._next; - // } else { - // searchStart = oldHead._next; - // prevSearch = oldHead; - // } - - // /** @type {Internal} */ - // let search = searchStart; - - // while (search) { - // if ( - // search.flags & typeFlag && - // search.type === type && - // search.key == key - // ) { - // // Match found! - // matchedInternal = search; - - // // Let's update our list of unmatched nodes to remove the new matchedInternal. - // // TODO: Better explain this: Temporarily keep the old next pointer - // // around for tracking null placeholders. Particularly examine the - // // test "should support moving Fragments between beginning and end" - // prevSearch._tempNext = prevSearch._next; - // prevSearch._next = matchedInternal._next; - - // break; - // } - - // // No match found. Let's move our pointers to the next node in our - // // search. - // prevSearch = search; - - // // If our current node we are searching has a _next node, then let's - // // continue from there. If it doesn't, let's loop back around to the - // // start of the list of unmatched nodes (i.e. oldHead). - // search = search._next ? search._next : oldHead._next; - // if (search === searchStart) { - // // However, it's possible that oldHead was the start of our search. If - // // so, we can stop searching. No match was found. - // break; - // } - // } - /* Keyed search */ /** @type {Internal} */ let search; @@ -317,7 +242,7 @@ function unmountUnusedKeyedChildren(keyMap) { } /** - * @param {Internal} internal + * @param {Internal | null} internal */ function unmountUnusedChildren(internal) { while (internal) { @@ -327,108 +252,7 @@ function unmountUnusedChildren(internal) { } /** - * @param {Internal} internal - * @param {PreactElement} parentDom - * @returns {Internal} - */ -function runLIS(internal, parentDom) { - // let internal = prevInternal; - /** @type {Internal[]} */ - const wipLIS = []; - - while (internal) { - // Skip over Root nodes whose parentDOM is different from the current - // parentDOM (aka Portals). Don't mark them for insertion since the - // recursive calls to mountChildren/patchChildren will handle - // mounting/inserting any DOM nodes under the root node. - // - // If a root node's parentDOM is the same as the current parentDOM then - // treat it as an unkeyed fragment and prepare it for moving or insertion - // if necessary. - // - // TODO: Consider the case where a root node has the same parent, goes - // into a different parent, a new node is inserted before the portal, and - // then the portal goes back to the original parent. Do we correctly - // insert the portal into the right place? Currently yes, because the - // beginning of patch calls insert whenever parentDom changes. Could we - // move that logic here? - // - // TODO: We do the props._parentDom !== parentDom in a couple places. - // Could we do this check once and cache the result in a flag? - if (internal.flags & TYPE_ROOT && internal.props._parentDom !== parentDom) { - internal = internal._next; - continue; - } - - // Mark all internals as requiring insertion. We will clear this flag for - // internals on longest decreasing subsequence - internal.flags |= INSERT_INTERNAL; - - // Skip over newly mounted internals. They will be mounted in place. - if (internal._index === -1) { - internal = internal._next; - continue; - } - - if (wipLIS.length == 0) { - wipLIS.push(internal); - internal = internal._next; - continue; - } - - let ldsTail = wipLIS[wipLIS.length - 1]; - if (ldsTail._index < internal._index) { - internal._tempNext = ldsTail; - wipLIS.push(internal); - } else { - // Search for position in wipLIS where node should go. It should replace - // the first node where node > wip[i] (though keep in mind, we are - // iterating over the list backwards). Example: - // ``` - // wipLIS = [4,3,1], node = 2. - // Node should replace 1: [4,3,2] - // ``` - let i = wipLIS.length; - // TODO: Binary search? - while (--i >= 0 && wipLIS[i]._index > internal._index) {} - - wipLIS[i + 1] = internal; - let prevLIS = i < 0 ? null : wipLIS[i]; - internal._tempNext = prevLIS; - } - - internal = internal._next; - } - - // Step 4. Mark internals in longest increasing subsequence and reverse the - // the longest increasing subsequence linked list. Before this step, _tempNext - // is actual the **previous** Internal in the longest increasing subsequence. - // - // After this step, _tempNext becomes the **next** Internal in the longest - // increasing subsequence. - /** @type {Internal | null} */ - let lisNode = wipLIS.length ? wipLIS[wipLIS.length - 1] : null; - let lisHead = lisNode; - let nextLIS = null; - while (lisNode) { - // This node is on the longest decreasing subsequence so clear INSERT_NODE flag - lisNode.flags &= ~INSERT_INTERNAL; - - // Reverse the _tempNext LIS linked list - internal = lisNode._tempNext; - lisNode._tempNext = nextLIS; - nextLIS = lisNode; - lisNode = internal; - - // Track the head of the linked list - if (lisNode) lisHead = lisNode; - } - - return lisHead; -} - -/** - * @param {Internal} internal + * @param {Internal | null} internal * @param {ComponentChildren[]} children * @param {PreactElement} parentDom */ @@ -461,26 +285,6 @@ function insertionLoop(internal, children, parentDom) { Array.isArray(vnode) ? createElement(Fragment, null, vnode) : vnode, parentDom ); - // if (internal._index < lastPlacedIndex) { - // if ((internal.flags & INSERT_INTERNAL) !== INSERT_INTERNAL) { - // console.log( - // '======================= EXPECTED INSERTION', - // internal.flags & INSERT_INTERNAL, - // INSERT_INTERNAL, - // internal.key - // ); - // } - // let sibling = internal._next; - // while (sibling && sibling._index < lastPlacedIndex) { - // sibling = sibling._next; - // } - // insert(internal, parentDom, sibling ? getFirstDom(sibling) : null); - // } else { - // if ((internal.flags & INSERT_INTERNAL) !== 0) { - // console.log('======================= DID NOT EXPECT INSERTION'); - // } - // lastPlacedIndex = internal._index; - // } if (internal.flags & INSERT_INTERNAL) { insert(internal, parentDom, getDomSibling(internal)); } From a9f0d59cceee11bd47c1f8dbd15b78ea931cbca0 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 17 Feb 2023 14:42:21 -0800 Subject: [PATCH 62/71] Inline insertionLoop into patchChildren Reduces recursion level by 1 function --- jsconfig.json | 2 +- src/diff/children.js | 112 ++++++++++++++++++++----------------------- 2 files changed, 52 insertions(+), 62 deletions(-) diff --git a/jsconfig.json b/jsconfig.json index cd6a2a5b3c..70fe82097a 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -10,7 +10,7 @@ "preact/*": ["./*"] }, "reactNamespace": "createElement", - "target": "es5" + "target": "es2015" }, "exclude": ["node_modules", "dist", "demo"] } diff --git a/src/diff/children.js b/src/diff/children.js index dc2ce289ff..9564149c17 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -29,11 +29,11 @@ import { createInternal, getDomSibling } from '../tree'; /** * Update an internal with new children. - * @param {Internal} parentInternal The internal whose children should be patched + * @param {Internal} internal The internal whose children should be patched * @param {ComponentChildren[]} children The new children, represented as VNodes * @param {PreactElement} parentDom The element into which this subtree is rendered */ -export function patchChildren(parentInternal, children, parentDom) { +export function patchChildren(internal, children, parentDom) { // Step 1. Find matches and set up _next pointers. All unused internals are at // attached to oldHead. // @@ -44,14 +44,57 @@ export function patchChildren(parentInternal, children, parentDom) { // `_tempNext` to hold the old next pointers we are able to simultaneously // iterate over the new VNodes, iterate over the old Internal list, and update // _next pointers to the new Internals. - findMatches(parentInternal._child, children, parentInternal); + findMatches(internal._child, children, internal); // Step 5. Walk forwards over the newly-assigned _next properties, inserting // Internals that require insertion. We track the next dom sibling Internals // should be inserted before by walking over the LIS (using _tempNext) at the // same time - if (parentInternal._child) { - insertionLoop(parentInternal._child, children, parentDom); + let index = 0; + internal = internal._child; + while (internal) { + let vnode = children[index]; + while (vnode == null || vnode === true || vnode === false) { + vnode = children[++index]; + } + + if (internal._index === -1) { + let nextDomSibling = getDomSibling(internal); + mount(internal, parentDom, nextDomSibling); + if (internal.flags & TYPE_DOM) { + // If we are mounting a component, it's DOM children will get inserted + // into the DOM in mountChildren. If we are mounting a DOM node, then + // it's children will be mounted into itself and we need to insert this + // DOM in place. + insert(internal, parentDom, nextDomSibling); + } + } else if ( + (internal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) === + (MODE_HYDRATE | MODE_SUSPENDED) + ) { + mount(internal, parentDom, internal.data); + } else { + patch( + internal, + Array.isArray(vnode) ? createElement(Fragment, null, vnode) : vnode, + parentDom + ); + if (internal.flags & INSERT_INTERNAL) { + insert(internal, parentDom, getDomSibling(internal)); + } + } + + let oldRef = internal._prevRef; + if (internal.ref != oldRef) { + if (oldRef) applyRef(oldRef, null, internal); + if (internal.ref) + applyRef(internal.ref, internal._component || internal.data, internal); + } + + internal.flags &= ~INSERT_INTERNAL; + internal._tempNext = null; + internal._index = index++; + internal = internal._next; } } @@ -106,12 +149,12 @@ function findMatches(internal, children, parentInternal) { let typeFlag = 0; let key; /** @type {VNode | string} */ - let normalizedVNode; + let normalizedVNode = ''; // text VNodes (strings, numbers, bigints, etc): if (typeof vnode !== 'object') { typeFlag = TYPE_TEXT; - normalizedVNode = '' + vnode; + normalizedVNode += vnode; } else { // TODO: Investigate avoiding this VNode allocation (and the one below in // the call to `patch`) by passing through the raw VNode type and handling @@ -208,7 +251,7 @@ function findMatches(internal, children, parentInternal) { if (internal && internal._index == index) { // Move forward our tracker for null placeholders - internal = internal._tempNext || internal._next; + internal = internal._next; } } @@ -251,59 +294,6 @@ function unmountUnusedChildren(internal) { } } -/** - * @param {Internal | null} internal - * @param {ComponentChildren[]} children - * @param {PreactElement} parentDom - */ -function insertionLoop(internal, children, parentDom) { - let index = 0; - while (internal) { - let vnode = children[index]; - while (vnode == null || vnode === true || vnode === false) { - vnode = children[++index]; - } - - if (internal._index === -1) { - let nextDomSibling = getDomSibling(internal); - mount(internal, parentDom, nextDomSibling); - if (internal.flags & TYPE_DOM) { - // If we are mounting a component, it's DOM children will get inserted - // into the DOM in mountChildren. If we are mounting a DOM node, then - // it's children will be mounted into itself and we need to insert this - // DOM in place. - insert(internal, parentDom, nextDomSibling); - } - } else if ( - (internal.flags & (MODE_HYDRATE | MODE_SUSPENDED)) === - (MODE_HYDRATE | MODE_SUSPENDED) - ) { - mount(internal, parentDom, internal.data); - } else { - patch( - internal, - Array.isArray(vnode) ? createElement(Fragment, null, vnode) : vnode, - parentDom - ); - if (internal.flags & INSERT_INTERNAL) { - insert(internal, parentDom, getDomSibling(internal)); - } - } - - let oldRef = internal._prevRef; - if (internal.ref != oldRef) { - if (oldRef) applyRef(oldRef, null, internal); - if (internal.ref) - applyRef(internal.ref, internal._component || internal.data, internal); - } - - internal.flags &= ~INSERT_INTERNAL; - internal._tempNext = null; - internal._index = index++; - internal = internal._next; - } -} - /** * @param {import('../internal').Internal} internal * @param {import('../internal').PreactNode} parentDom From 51053fb136a3fde4839c08ccfa10f44f7976d7dc Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 17 Feb 2023 14:54:08 -0800 Subject: [PATCH 63/71] Remove _tempNext pointer --- src/diff/children.js | 23 +++++------------------ src/internal.d.ts | 5 ----- src/tree.js | 2 -- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 9564149c17..23a8931d0a 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -34,22 +34,11 @@ import { createInternal, getDomSibling } from '../tree'; * @param {PreactElement} parentDom The element into which this subtree is rendered */ export function patchChildren(internal, children, parentDom) { - // Step 1. Find matches and set up _next pointers. All unused internals are at - // attached to oldHead. - // - // In this step, _tempNext will hold the old next pointer for an internal. - // This algorithm changes `_next` when finding matching internals. This change - // breaks our null placeholder detection logic which compares the old internal - // at a particular index with the new VNode at that index. By using - // `_tempNext` to hold the old next pointers we are able to simultaneously - // iterate over the new VNodes, iterate over the old Internal list, and update - // _next pointers to the new Internals. + // Find matches and set up _next pointers. Unmount unused/unclaimed Internal findMatches(internal._child, children, internal); - // Step 5. Walk forwards over the newly-assigned _next properties, inserting - // Internals that require insertion. We track the next dom sibling Internals - // should be inserted before by walking over the LIS (using _tempNext) at the - // same time + // Walk forwards over the newly-assigned _next properties, inserting Internals + // that require insertion. let index = 0; internal = internal._child; while (internal) { @@ -92,7 +81,6 @@ export function patchChildren(internal, children, parentDom) { } internal.flags &= ~INSERT_INTERNAL; - internal._tempNext = null; internal._index = index++; internal = internal._next; } @@ -256,12 +244,11 @@ function findMatches(internal, children, parentInternal) { } // Ensure the last node of the last matched internal has a null _next pointer. - // Its possible that it still points to it's old sibling at the end of Step 1, + // Its possible that it still points to it's old sibling at the end of this loop, // so we'll manually clear it here. if (prevMatchedInternal) prevMatchedInternal._next = null; - // Step 2. Walk over the unused children and unmount: - // unmountUnusedChildren(oldHead); + // Walk over the unused children and unmount: if (keyMap) { unmountUnusedKeyedChildren(keyMap); } else if (oldHead) { diff --git a/src/internal.d.ts b/src/internal.d.ts index e4b5a2c161..3902b89457 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -152,11 +152,6 @@ export interface Internal

                                                                    { /** next sibling Internal node */ _next: Internal | null; _index: number; - /** - * A temporary pointer to another Internal, used for different purposes in - * the patching children algorithm. See comments there for its different uses - * */ - _tempNext: Internal | null; /** most recent vnode ID */ _vnodeId: number; /** diff --git a/src/tree.js b/src/tree.js index bd092acec9..bffad31101 100644 --- a/src/tree.js +++ b/src/tree.js @@ -97,7 +97,6 @@ export function createInternal(vnode, parentInternal) { // _parent: parentInternal, // _child: null, // _next: null, - // _tempNext: null, // _index: -1, // _vnodeId: vnodeId, // _component: null, @@ -135,7 +134,6 @@ class Internal { this._parent = parentInternal; this._child = null; this._next = null; - this._tempNext = null; this._index = -1; this._vnodeId = vnodeId; this._component = null; From 38bc3f64ad36c0e3a5ca124492450db162f3cc5e Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 17 Feb 2023 15:36:36 -0800 Subject: [PATCH 64/71] Combine oldHead and internal variables in findMatches --- src/diff/children.js | 42 ++++++++++++------------------------------ 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 23a8931d0a..1b34d7cbdc 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -92,12 +92,8 @@ export function patchChildren(internal, children, parentDom) { * @param {Internal} parentInternal */ function findMatches(internal, children, parentInternal) { - /** @type {Internal} */ parentInternal._child = null; - /** @type {Internal | null} The start of the list of unmatched Internals */ - let oldHead = internal; - /** @type {Internal | undefined} The last matched internal */ let prevMatchedInternal; @@ -120,14 +116,6 @@ function findMatches(internal, children, parentInternal) { // searches for matching internals unmount(internal, internal, 0); - // If this internal is the first unmatched internal, then bump our - // pointer to the next node so our search will skip over this internal. - // - // TODO: What if this node is not the first unmatched Internal (and so - // remains in the search array) and shares the type with another - // Internal that is it matches? Do we have a test for this? - if (oldHead == internal) oldHead = oldHead._next; - internal = internal._next; } continue; @@ -138,6 +126,8 @@ function findMatches(internal, children, parentInternal) { let key; /** @type {VNode | string} */ let normalizedVNode = ''; + /** @type {Internal | undefined} */ + let matchedInternal; // text VNodes (strings, numbers, bigints, etc): if (typeof vnode !== 'object') { @@ -156,9 +146,6 @@ function findMatches(internal, children, parentInternal) { key = normalizedVNode.key; } - /** @type {Internal | undefined} */ - let matchedInternal; - if (key == null && internal && index < internal._index) { // If we are doing an unkeyed diff, and the old index of the current // internal in the old list of children is greater than the current VNode @@ -167,24 +154,24 @@ function findMatches(internal, children, parentInternal) { // internal to mount this VNode. } else if ( !keyMap && - oldHead && - (oldHead.flags & typeFlag) !== 0 && - oldHead.type === type && - oldHead.key == key + internal && + internal.flags & typeFlag && + internal.type === type && + internal.key == key ) { // Fast path checking if this current vnode matches the first unused // Internal. By doing this we can avoid the search loop and setting the // move flag, which allows us to skip the LDS algorithm if no Internals // moved - matchedInternal = oldHead; - oldHead = oldHead._next; - } else if (oldHead) { + matchedInternal = internal; + internal = internal._next; + } else if (internal) { /* Keyed search */ /** @type {Internal} */ let search; if (!keyMap) { keyMap = new Map(); - search = oldHead; + search = internal; while (search) { if (search.key) { keyMap.set(search.key, search); @@ -236,11 +223,6 @@ function findMatches(internal, children, parentInternal) { // TODO: Consider detecting if an internal is of TYPE_ROOT, whether or not // it is a PORTAL, and setting a flag as such to use in getDomSibling and // getFirstDom - - if (internal && internal._index == index) { - // Move forward our tracker for null placeholders - internal = internal._next; - } } // Ensure the last node of the last matched internal has a null _next pointer. @@ -251,8 +233,8 @@ function findMatches(internal, children, parentInternal) { // Walk over the unused children and unmount: if (keyMap) { unmountUnusedKeyedChildren(keyMap); - } else if (oldHead) { - unmountUnusedChildren(oldHead); + } else if (internal) { + unmountUnusedChildren(internal); } } From a5310ad567edadaaad63858aa4757a3510e18845 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 17 Feb 2023 15:39:13 -0800 Subject: [PATCH 65/71] Reuse vnode variable and remove normalizedVNode variable --- src/diff/children.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 1b34d7cbdc..46993d4eb7 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -104,7 +104,7 @@ function findMatches(internal, children, parentInternal) { let lastPlacedIndex = 0; for (let index = 0; index < children.length; index++) { - const vnode = children[index]; + let vnode = children[index]; // holes get accounted for in the index property: if (vnode == null || vnode === true || vnode === false) { @@ -124,26 +124,24 @@ function findMatches(internal, children, parentInternal) { let type = null; let typeFlag = 0; let key; - /** @type {VNode | string} */ - let normalizedVNode = ''; /** @type {Internal | undefined} */ let matchedInternal; // text VNodes (strings, numbers, bigints, etc): if (typeof vnode !== 'object') { typeFlag = TYPE_TEXT; - normalizedVNode += vnode; + vnode = '' + vnode; } else { // TODO: Investigate avoiding this VNode allocation (and the one below in // the call to `patch`) by passing through the raw VNode type and handling // nested arrays directly in mount, patch, createInternal, etc. - normalizedVNode = Array.isArray(vnode) + vnode = Array.isArray(vnode) ? createElement(Fragment, null, vnode) : vnode; - type = normalizedVNode.type; + type = vnode.type; typeFlag = typeof type === 'function' ? TYPE_COMPONENT : TYPE_ELEMENT; - key = normalizedVNode.key; + key = vnode.key; } if (key == null && internal && index < internal._index) { @@ -199,7 +197,7 @@ function findMatches(internal, children, parentInternal) { // No match, create a new Internal: if (!matchedInternal) { - matchedInternal = createInternal(normalizedVNode, parentInternal); + matchedInternal = createInternal(vnode, parentInternal); } else if (matchedInternal._index < lastPlacedIndex) { // If the matched internal has moved such that it is now after the last // internal we determined was "in-place", mark it for insertion to move it From d00f85cc1f0377783a0de7fea1ea2bdcf86e900c Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 17 Feb 2023 16:06:00 -0800 Subject: [PATCH 66/71] Move building the key map into its own function to save some bytes I think this saves bytes cuz the implementation is another "loop over internals" and putting it into its own function gives the core internal variable the same minified name as other functions that also loop over internals. --- src/diff/children.js | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index 46993d4eb7..4e48e60266 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -165,21 +165,10 @@ function findMatches(internal, children, parentInternal) { internal = internal._next; } else if (internal) { /* Keyed search */ - /** @type {Internal} */ + /** @type {any} */ let search; if (!keyMap) { - keyMap = new Map(); - search = internal; - while (search) { - if (search.key) { - keyMap.set(search.key, search); - } else if (!keyMap.has(search.type)) { - keyMap.set(search.type, [search]); - } else { - keyMap.get(search.type).push(search); - } - search = search._next; - } + keyMap = buildMap(internal); } if (key == null) { search = keyMap.get(type); @@ -236,6 +225,26 @@ function findMatches(internal, children, parentInternal) { } } +/** + * @param {Internal | null} internal + * @returns {Map} + */ +function buildMap(internal) { + let keyMap = new Map(); + while (internal) { + if (internal.key) { + keyMap.set(internal.key, internal); + } else if (!keyMap.has(internal.type)) { + keyMap.set(internal.type, [internal]); + } else { + keyMap.get(internal.type).push(internal); + } + internal = internal._next; + } + + return keyMap; +} + /** * @param {Map} keyMap */ From 9e7cac28ca57f0f9116e5d7e020fb86b6e969c32 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 17 Feb 2023 17:08:33 -0800 Subject: [PATCH 67/71] Fix dom operation tests Lots of tests changed cuz we unmount before doing insertions now --- jsconfig.json | 2 +- package.json | 3 + test/browser/createContext.test.js | 2 +- test/browser/fragments.test.js | 209 ++++++++++++++++------------- test/browser/keys.test.js | 58 +++++--- test/browser/portals.test.js | 8 +- 6 files changed, 168 insertions(+), 114 deletions(-) diff --git a/jsconfig.json b/jsconfig.json index 70fe82097a..ccd3f47e1d 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -3,13 +3,13 @@ "baseUrl": ".", "checkJs": true, "jsx": "react", + "jsxFactory": "createElement", "lib": ["dom", "es5"], "moduleResolution": "node", "paths": { "preact": ["."], "preact/*": ["./*"] }, - "reactNamespace": "createElement", "target": "es2015" }, "exclude": ["node_modules", "dist", "demo"] diff --git a/package.json b/package.json index 25ffa353bb..d2e68489f2 100644 --- a/package.json +++ b/package.json @@ -318,5 +318,8 @@ }, "dependencies": { "pretty-format": "^27.5.1" + }, + "volta": { + "node": "18.14.0" } } diff --git a/test/browser/createContext.test.js b/test/browser/createContext.test.js index 0502e2b557..d7f94ad3b5 100644 --- a/test/browser/createContext.test.js +++ b/test/browser/createContext.test.js @@ -881,8 +881,8 @@ describe('createContext', () => { expect(events).to.deep.equal([ 'render 0', 'mount 0', - 'render 1', 'unmount 0', + 'render 1', 'mount 1' ]); }); diff --git a/test/browser/fragments.test.js b/test/browser/fragments.test.js index fdaf6d5be3..981f9c05f7 100644 --- a/test/browser/fragments.test.js +++ b/test/browser/fragments.test.js @@ -282,9 +282,9 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal(div([div(1), span(2), span(2)])); expectDomLogToBe([ + '1.remove()', '

                                                                    .insertBefore(#text, Null)', - '
                                                                    122.insertBefore(
                                                                    1, 1)', - '1.remove()' + '
                                                                    22.insertBefore(
                                                                    1, 2)' ]); }); @@ -404,9 +404,9 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); expect(scratch.innerHTML).to.equal('
                                                                    Hello
                                                                    '); expectDomLogToBe([ + '
                                                                    Hello.remove()', '
                                                                    .insertBefore(#text, Null)', - '
                                                                    Hello.insertBefore(
                                                                    Hello,
                                                                    Hello)', - '
                                                                    Hello.remove()' + '
                                                                    .insertBefore(
                                                                    Hello, Null)' ]); clearLog(); @@ -415,10 +415,10 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); expect(scratch.innerHTML).to.equal('
                                                                    Hello
                                                                    '); expectDomLogToBe([ + '
                                                                    Hello.remove()', '
                                                                    .insertBefore(#text, Null)', // Re-append the Stateful DOM since it has been re-parented - '
                                                                    Hello.insertBefore(
                                                                    Hello,
                                                                    Hello)', - '
                                                                    Hello.remove()' + '
                                                                    .insertBefore(
                                                                    Hello, Null)' ]); }); @@ -443,9 +443,9 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); expect(scratch.innerHTML).to.equal('
                                                                    Hello
                                                                    '); expectDomLogToBe([ + '
                                                                    Hello.remove()', '
                                                                    .insertBefore(#text, Null)', - '
                                                                    Hello.insertBefore(
                                                                    Hello,
                                                                    Hello)', - '
                                                                    Hello.remove()' + '
                                                                    .insertBefore(
                                                                    Hello, Null)' ]); clearLog(); @@ -454,9 +454,9 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); expect(scratch.innerHTML).to.equal('
                                                                    Hello
                                                                    '); expectDomLogToBe([ + '
                                                                    Hello.remove()', '
                                                                    .insertBefore(#text, Null)', - '
                                                                    Hello.insertBefore(
                                                                    Hello,
                                                                    Hello)', - '
                                                                    Hello.remove()' + '
                                                                    .insertBefore(
                                                                    Hello, Null)' ]); }); @@ -734,10 +734,10 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal(htmlForFalse); expectDomLogToBe( [ - '
                                                                    barHellobeep.insertBefore(
                                                                    bar,
                                                                    beep)', - '
                                                                    Hellobarbeep.insertBefore(
                                                                    Hello, Null)', + '
                                                                    barHellobeep.insertBefore(
                                                                    bar, Null)', + '
                                                                    Hellobeepbar.insertBefore(
                                                                    Hello, Null)', // TODO: does one operation too many here - '
                                                                    barbeepHello.insertBefore(
                                                                    bar, Null)' + '
                                                                    beepbarHello.insertBefore(
                                                                    bar, Null)' ], 'rendering true to false' ); @@ -787,12 +787,12 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); // Component should not have updated (empty op log) expect(scratch.innerHTML).to.equal(html); expectDomLogToBe([ + '1.remove()', + '
                                                                    Hello.remove()', '.insertBefore(#text, Null)', - '
                                                                    1Hello2.insertBefore(1, 1)', + '
                                                                    2.insertBefore(1, 2)', '
                                                                    .insertBefore(#text, Null)', - '
                                                                    11Hello2.insertBefore(
                                                                    Hello, 1)', - '1.remove()', - '
                                                                    Hello.remove()' + '
                                                                    12.insertBefore(
                                                                    Hello, 2)' ]); clearLog(); @@ -801,12 +801,12 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); // Component should not have updated (empty op log) expect(scratch.innerHTML).to.equal(html); expectDomLogToBe([ + '1.remove()', + '
                                                                    Hello.remove()', '.insertBefore(#text, Null)', - '
                                                                    1Hello2.insertBefore(1, 1)', + '
                                                                    2.insertBefore(1, 2)', '
                                                                    .insertBefore(#text, Null)', - '
                                                                    11Hello2.insertBefore(
                                                                    Hello, 1)', - '1.remove()', - '
                                                                    Hello.remove()' + '
                                                                    12.insertBefore(
                                                                    Hello, 2)' ]); }); @@ -869,9 +869,9 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal('foobar'); expectDomLogToBe([ - '
                                                                    spamfoobar.insertBefore(#text, #text)', '#text.remove()', - '#text.remove()' + '#text.remove()', + '
                                                                    foo.insertBefore(#text, Null)' ]); }); @@ -1032,6 +1032,7 @@ describe('Fragment', () => { }); it('should reorder Fragment children', () => { + /** @type {() => void} */ let updateState; class App extends Component { @@ -1324,9 +1325,16 @@ describe('Fragment', () => { htmlForFalse, 'rendering from true to false' ); + // A perfect algo can do this in two moves + // expectDomLogToBe([ + // '
                                                                      012345.insertBefore(
                                                                    1. 4,
                                                                    2. 0)', + // '
                                                                        401235.insertBefore(
                                                                      1. 5,
                                                                      2. 0)' + // ]); expectDomLogToBe([ - '
                                                                          012345.insertBefore(
                                                                        1. 4,
                                                                        2. 0)', - '
                                                                            401235.insertBefore(
                                                                          1. 5,
                                                                          2. 0)' + '
                                                                              012345.insertBefore(
                                                                            1. 0, Null)', + '
                                                                                123450.insertBefore(
                                                                              1. 1, Null)', + '
                                                                                  234501.insertBefore(
                                                                                1. 2, Null)', + '
                                                                                    345012.insertBefore(
                                                                                  1. 3, Null)' ]); clearLog(); @@ -1376,14 +1384,14 @@ describe('Fragment', () => { 'rendering from true to false' ); expectDomLogToBe([ + // Remove 1 & 2 (replaced with null) + '
                                                                                  2. 0.remove()', + '
                                                                                  3. 1.remove()', // Mount 3 & 4 '
                                                                                  4. .insertBefore(#text, Null)', - '
                                                                                      0122.insertBefore(
                                                                                    1. 3, Null)', + '
                                                                                        22.insertBefore(
                                                                                      1. 3, Null)', '
                                                                                      2. .insertBefore(#text, Null)', - '
                                                                                          01223.insertBefore(
                                                                                        1. 4, Null)', - // Remove 1 & 2 (replaced with null) - '
                                                                                        2. 0.remove()', - '
                                                                                        3. 1.remove()' + '
                                                                                            223.insertBefore(
                                                                                          1. 4, Null)' ]); clearLog(); @@ -1393,14 +1401,14 @@ describe('Fragment', () => { 'rendering from false to true' ); expectDomLogToBe([ + // Remove 3 & 4 (replaced by null) + '
                                                                                          2. 3.remove()', + '
                                                                                          3. 4.remove()', // Insert 0 and 1 '
                                                                                          4. .insertBefore(#text, Null)', - '
                                                                                              2234.insertBefore(
                                                                                            1. 0,
                                                                                            2. 2)', + '
                                                                                                22.insertBefore(
                                                                                              1. 0,
                                                                                              2. 2)', '
                                                                                              3. .insertBefore(#text, Null)', - '
                                                                                                  02234.insertBefore(
                                                                                                1. 1,
                                                                                                2. 2)', - // Remove 3 & 4 (replaced by null) - '
                                                                                                3. 3.remove()', - '
                                                                                                4. 4.remove()' + '
                                                                                                    022.insertBefore(
                                                                                                  1. 1,
                                                                                                  2. 2)' ]); }); @@ -1447,14 +1455,14 @@ describe('Fragment', () => { 'rendering from true to false' ); expectDomLogToBe([ + // Remove 1 & 2 (replaced with null) + '
                                                                                                  3. 0.remove()', + '
                                                                                                  4. 1.remove()', // Mount 4 & 5 '
                                                                                                  5. .insertBefore(#text, Null)', - '
                                                                                                      0123.insertBefore(
                                                                                                    1. 4, Null)', + '
                                                                                                        23.insertBefore(
                                                                                                      1. 4, Null)', '
                                                                                                      2. .insertBefore(#text, Null)', - '
                                                                                                          01234.insertBefore(
                                                                                                        1. 5, Null)', - // Remove 1 & 2 (replaced with null) - '
                                                                                                        2. 0.remove()', - '
                                                                                                        3. 1.remove()' + '
                                                                                                            234.insertBefore(
                                                                                                          1. 5, Null)' ]); clearLog(); @@ -1464,14 +1472,14 @@ describe('Fragment', () => { 'rendering from false to true' ); expectDomLogToBe([ + // Remove 4 & 5 (replaced by null) + '
                                                                                                          2. 4.remove()', + '
                                                                                                          3. 5.remove()', // Insert 0 and 1 back into the DOM '
                                                                                                          4. .insertBefore(#text, Null)', - '
                                                                                                              2345.insertBefore(
                                                                                                            1. 0,
                                                                                                            2. 2)', + '
                                                                                                                23.insertBefore(
                                                                                                              1. 0,
                                                                                                              2. 2)', '
                                                                                                              3. .insertBefore(#text, Null)', - '
                                                                                                                  02345.insertBefore(
                                                                                                                1. 1,
                                                                                                                2. 2)', - // Remove 4 & 5 (replaced by null) - '
                                                                                                                3. 4.remove()', - '
                                                                                                                4. 5.remove()' + '
                                                                                                                    023.insertBefore(
                                                                                                                  1. 1,
                                                                                                                  2. 2)' ]); }); @@ -1525,11 +1533,11 @@ describe('Fragment', () => { ); expectDomLogToBe( [ + '
                                                                                                                    boop.remove()', // TODO: this operation is too much - '
                                                                                                                    barHellobeepboop.insertBefore(
                                                                                                                    bar,
                                                                                                                    beep)', - '
                                                                                                                    Hellobarbeepboop.insertBefore(
                                                                                                                    Hello,
                                                                                                                    boop)', - '
                                                                                                                    barbeepHelloboop.insertBefore(
                                                                                                                    bar,
                                                                                                                    boop)', - '
                                                                                                                    boop.remove()' + '
                                                                                                                    barHellobeep.insertBefore(
                                                                                                                    bar, Null)', + '
                                                                                                                    Hellobeepbar.insertBefore(
                                                                                                                    Hello, Null)', + '
                                                                                                                    beepbarHello.insertBefore(
                                                                                                                    bar, Null)' ], 'rendering from true to false' ); @@ -1554,6 +1562,7 @@ describe('Fragment', () => { }); it('should swap nested fragments correctly', () => { + /** @type {() => void} */ let swap; class App extends Component { constructor(props) { @@ -1670,9 +1679,9 @@ describe('Fragment', () => { ); expectDomLogToBe( [ - '
                                                                                                                    barHellobeepbeepbeep.insertBefore(
                                                                                                                    bar,
                                                                                                                    beep)', - '
                                                                                                                    Hellobarbeepbeepbeep.insertBefore(
                                                                                                                    Hello, Null)', - '
                                                                                                                    barbeepbeepbeepHello.insertBefore(
                                                                                                                    bar, Null)' + '
                                                                                                                    barHellobeepbeepbeep.insertBefore(
                                                                                                                    bar, Null)', + '
                                                                                                                    Hellobeepbeepbeepbar.insertBefore(
                                                                                                                    Hello, Null)', + '
                                                                                                                    beepbeepbeepbarHello.insertBefore(
                                                                                                                    bar, Null)' ], 'rendering from true to false' ); @@ -1686,10 +1695,16 @@ describe('Fragment', () => { 'rendering from false to true' ); expectDomLogToBe( + // [ + // '
                                                                                                                    beepbeepbeepHellofoo.insertBefore(
                                                                                                                    Hello, Null)', + // '
                                                                                                                    beepbeepbeepfooHello.insertBefore(
                                                                                                                    foo,
                                                                                                                    beep)', + // '
                                                                                                                    foobeepbeepbeepHello.insertBefore(
                                                                                                                    Hello,
                                                                                                                    beep)' + // ], [ '
                                                                                                                    beepbeepbeepHellofoo.insertBefore(
                                                                                                                    Hello, Null)', - '
                                                                                                                    beepbeepbeepfooHello.insertBefore(
                                                                                                                    foo,
                                                                                                                    beep)', - '
                                                                                                                    foobeepbeepbeepHello.insertBefore(
                                                                                                                    Hello,
                                                                                                                    beep)' + '
                                                                                                                    boopbeepbeepfooHello.insertBefore(
                                                                                                                    boop, Null)', + '
                                                                                                                    boopbeepfooHelloboop.insertBefore(
                                                                                                                    boop, Null)', + '
                                                                                                                    boopfooHelloboopboop.insertBefore(
                                                                                                                    boop, Null)' ], 'rendering from false to true' ); @@ -1697,7 +1712,7 @@ describe('Fragment', () => { it('should correctly append children with siblings', () => { /** - * @type {(props: { values: Array}) => JSX.Element} + * @type {(props: { values: Array}) => preact.VNode} */ const Foo = ({ values }) => (
                                                                                                                      @@ -1778,12 +1793,12 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal(htmlForFalse); expectDomLogToBe( [ + '
                                                                                                                      2.remove()', + '#text.remove()', '
                                                                                                                      .insertBefore(#text, Null)', - '
                                                                                                                      1.insertBefore(
                                                                                                                      3, #text)', + '
                                                                                                                      .insertBefore(
                                                                                                                      3, Null)', '
                                                                                                                      .insertBefore(#text, Null)', - '
                                                                                                                      31.insertBefore(
                                                                                                                      4, #text)', - '#text.remove()', - '
                                                                                                                      2.remove()' + '
                                                                                                                      3.insertBefore(
                                                                                                                      4, Null)' ], 'rendering from true to false' ); @@ -1794,9 +1809,9 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal(htmlForTrue); expectDomLogToBe( [ - '
                                                                                                                      34.insertBefore(#text,
                                                                                                                      3)', - '
                                                                                                                      4.remove()', '
                                                                                                                      3.remove()', + '
                                                                                                                      4.remove()', + '
                                                                                                                      .insertBefore(#text, Null)', '
                                                                                                                      .insertBefore(#text, Null)', '
                                                                                                                      1.insertBefore(
                                                                                                                      2, Null)' ], @@ -1980,6 +1995,7 @@ describe('Fragment', () => { it('should properly render Fragments whose last child is a component returning null', () => { let Noop = () => null; + /** @type {() => void} */ let update; class App extends Component { constructor(props) { @@ -2025,6 +2041,7 @@ describe('Fragment', () => { }); it('should replace node in-between children', () => { + /** @type {() => void} */ let update; class SetState extends Component { constructor(props) { @@ -2058,13 +2075,14 @@ describe('Fragment', () => { `
                                                                                                                      A
                                                                                                                      B2
                                                                                                                      C
                                                                                                                      ` ); expectDomLogToBe([ + '
                                                                                                                      B1.remove()', '
                                                                                                                      .insertBefore(#text, Null)', - '
                                                                                                                      AB1C.insertBefore(
                                                                                                                      B2,
                                                                                                                      B1)', - '
                                                                                                                      B1.remove()' + '
                                                                                                                      AC.insertBefore(
                                                                                                                      B2,
                                                                                                                      C)' ]); }); it('should replace Fragment in-between children', () => { + /** @type {() => void} */ let update; class SetState extends Component { constructor(props) { @@ -2108,16 +2126,17 @@ describe('Fragment', () => { div([div('A'), section('B3'), section('B4'), div('C')]) ); expectDomLogToBe([ + '
                                                                                                                      B1.remove()', + '
                                                                                                                      B2.remove()', '
                                                                                                                      .insertBefore(#text, Null)', - '
                                                                                                                      AB1B2C.insertBefore(
                                                                                                                      B3,
                                                                                                                      B1)', + '
                                                                                                                      AC.insertBefore(
                                                                                                                      B3,
                                                                                                                      C)', '
                                                                                                                      .insertBefore(#text, Null)', - '
                                                                                                                      AB3B1B2C.insertBefore(
                                                                                                                      B4,
                                                                                                                      B1)', - '
                                                                                                                      B2.remove()', - '
                                                                                                                      B1.remove()' + '
                                                                                                                      AB3C.insertBefore(
                                                                                                                      B4,
                                                                                                                      C)' ]); }); it('should insert in-between children', () => { + /** @type {() => void} */ let update; class SetState extends Component { constructor(props) { @@ -2155,6 +2174,7 @@ describe('Fragment', () => { }); it('should insert in-between Fragments', () => { + /** @type {() => void} */ let update; class SetState extends Component { constructor(props) { @@ -2194,6 +2214,7 @@ describe('Fragment', () => { }); it('should insert in-between null children', () => { + /** @type {() => void} */ let update; class SetState extends Component { constructor(props) { @@ -2233,6 +2254,7 @@ describe('Fragment', () => { }); it('should insert Fragment in-between null children', () => { + /** @type {() => void} */ let update; class SetState extends Component { constructor(props) { @@ -2279,6 +2301,7 @@ describe('Fragment', () => { }); it('should insert in-between nested null children', () => { + /** @type {() => void} */ let update; class SetState extends Component { constructor(props) { @@ -2322,6 +2345,7 @@ describe('Fragment', () => { }); it('should insert Fragment in-between nested null children', () => { + /** @type {() => void} */ let update; class SetState extends Component { constructor(props) { @@ -2372,6 +2396,7 @@ describe('Fragment', () => { }); it('should update at correct place', () => { + /** @type {() => void} */ let updateA; class A extends Component { constructor(props) { @@ -2426,13 +2451,14 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.eql(`
                                                                                                                      A2
                                                                                                                      C
                                                                                                                      `); expectDomLogToBe([ + '
                                                                                                                      A.remove()', '.insertBefore(#text, Null)', - '
                                                                                                                      AC.insertBefore(A2,
                                                                                                                      A)', - '
                                                                                                                      A.remove()' + '
                                                                                                                      C.insertBefore(A2,
                                                                                                                      C)' ]); }); it('should update Fragment at correct place', () => { + /** @type {() => void} */ let updateA; class A extends Component { constructor(props) { @@ -2493,12 +2519,12 @@ describe('Fragment', () => { `
                                                                                                                      A3A4
                                                                                                                      C
                                                                                                                      ` ); expectDomLogToBe([ + '
                                                                                                                      A1.remove()', + '
                                                                                                                      A2.remove()', '.insertBefore(#text, Null)', - '
                                                                                                                      A1A2C.insertBefore(A3,
                                                                                                                      A1)', + '
                                                                                                                      C.insertBefore(A3,
                                                                                                                      C)', '.insertBefore(#text, Null)', - '
                                                                                                                      A3A1A2C.insertBefore(A4,
                                                                                                                      A1)', - '
                                                                                                                      A2.remove()', - '
                                                                                                                      A1.remove()' + '
                                                                                                                      A3C.insertBefore(A4,
                                                                                                                      C)' ]); }); @@ -2574,9 +2600,9 @@ describe('Fragment', () => { 'updateA' ); expectDomLogToBe([ + '
                                                                                                                      A.remove()', '.insertBefore(#text, Null)', - '
                                                                                                                      ABC.insertBefore(A2,
                                                                                                                      A)', - '
                                                                                                                      A.remove()' + '
                                                                                                                      BC.insertBefore(A2,
                                                                                                                      B)' ]); }); @@ -2631,12 +2657,12 @@ describe('Fragment', () => { 'updateA' ); expectDomLogToBe([ + '
                                                                                                                      A1.remove()', + '
                                                                                                                      A2.remove()', '.insertBefore(#text, Null)', - '
                                                                                                                      A1A2.insertBefore(A3,
                                                                                                                      A1)', + '
                                                                                                                      .insertBefore(A3, Null)', '.insertBefore(#text, Null)', - '
                                                                                                                      A3A1A2.insertBefore(A4,
                                                                                                                      A1)', - '
                                                                                                                      A2.remove()', - '
                                                                                                                      A1.remove()' + '
                                                                                                                      A3.insertBefore(A4, Null)' ]); clearLog(); @@ -2654,6 +2680,7 @@ describe('Fragment', () => { }); it('should properly place conditional elements around strictly equal vnodes', () => { + /** @type {() => void} */ let set; const Children = () => ( @@ -2705,9 +2732,9 @@ describe('Fragment', () => { rerender(); expect(scratch.innerHTML).to.equal(top); expectDomLogToBe([ + '
                                                                                                                      bottom panel.remove()', '
                                                                                                                      .insertBefore(#text, Null)', - '
                                                                                                                      NavigationContentbottom panel.insertBefore(
                                                                                                                      top panel,
                                                                                                                      Navigation)', - '
                                                                                                                      bottom panel.remove()' + '
                                                                                                                      NavigationContent.insertBefore(
                                                                                                                      top panel,
                                                                                                                      Navigation)' ]); clearLog(); @@ -2715,9 +2742,9 @@ describe('Fragment', () => { rerender(); expect(scratch.innerHTML).to.equal(bottom); expectDomLogToBe([ + '
                                                                                                                      top panel.remove()', '
                                                                                                                      .insertBefore(#text, Null)', - '
                                                                                                                      top panelNavigationContent.insertBefore(
                                                                                                                      bottom panel, Null)', - '
                                                                                                                      top panel.remove()' + '
                                                                                                                      NavigationContent.insertBefore(
                                                                                                                      bottom panel, Null)' ]); clearLog(); @@ -2725,9 +2752,9 @@ describe('Fragment', () => { rerender(); expect(scratch.innerHTML).to.equal(top); expectDomLogToBe([ + '
                                                                                                                      bottom panel.remove()', '
                                                                                                                      .insertBefore(#text, Null)', - '
                                                                                                                      NavigationContentbottom panel.insertBefore(
                                                                                                                      top panel,
                                                                                                                      Navigation)', - '
                                                                                                                      bottom panel.remove()' + '
                                                                                                                      NavigationContent.insertBefore(
                                                                                                                      top panel,
                                                                                                                      Navigation)' ]); }); @@ -2856,10 +2883,10 @@ describe('Fragment', () => { div([div(1), div(4), div('A'), div('B')]) ); expectDomLogToBe([ - '
                                                                                                                      .insertBefore(#text, Null)', - '
                                                                                                                      123AB.insertBefore(
                                                                                                                      4,
                                                                                                                      2)', '
                                                                                                                      2.remove()', - '
                                                                                                                      3.remove()' + '
                                                                                                                      3.remove()', + '
                                                                                                                      .insertBefore(#text, Null)', + '
                                                                                                                      1AB.insertBefore(
                                                                                                                      4,
                                                                                                                      A)' ]); }); @@ -2904,11 +2931,11 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal(div([span(1), div('A'), div('B')])); expectDomLogToBe([ - '.insertBefore(#text, Null)', - '
                                                                                                                      123AB.insertBefore(1,
                                                                                                                      1)', + '
                                                                                                                      1.remove()', '
                                                                                                                      2.remove()', '
                                                                                                                      3.remove()', - '
                                                                                                                      1.remove()' + '.insertBefore(#text, Null)', + '
                                                                                                                      AB.insertBefore(1,
                                                                                                                      A)' ]); }); }); diff --git a/test/browser/keys.test.js b/test/browser/keys.test.js index 7564c8b168..3c8dffe3ca 100644 --- a/test/browser/keys.test.js +++ b/test/browser/keys.test.js @@ -297,7 +297,13 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('abcd'); - expect(getLog()).to.deep.equal(['
                                                                                                                        bcda.insertBefore(
                                                                                                                      1. a,
                                                                                                                      2. b)']); + // A perfect algorithm would do this in one move. Our algorithm is a compromise of size vs common case perf + // expect(getLog()).to.deep.equal(['
                                                                                                                          bcda.insertBefore(
                                                                                                                        1. a,
                                                                                                                        2. b)']); + expect(getLog()).to.deep.equal([ + '
                                                                                                                            bcda.insertBefore(
                                                                                                                          1. b, Null)', + '
                                                                                                                              cdab.insertBefore(
                                                                                                                            1. c, Null)', + '
                                                                                                                                dabc.insertBefore(
                                                                                                                              1. d, Null)' + ]); }); it('should move multiple keyed children to the beginning', () => { @@ -312,9 +318,15 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('abcde'); + // A perfect algorithm would do this in two moves. Our algorithm is a compromise of size vs common case perf + // expect(getLog()).to.deep.equal([ + // '
                                                                                                                                  cdeab.insertBefore(
                                                                                                                                1. a,
                                                                                                                                2. c)', + // '
                                                                                                                                    acdeb.insertBefore(
                                                                                                                                  1. b,
                                                                                                                                  2. c)' + // ]); expect(getLog()).to.deep.equal([ - '
                                                                                                                                      cdeab.insertBefore(
                                                                                                                                    1. a,
                                                                                                                                    2. c)', - '
                                                                                                                                        acdeb.insertBefore(
                                                                                                                                      1. b,
                                                                                                                                      2. c)' + '
                                                                                                                                          cdeab.insertBefore(
                                                                                                                                        1. c, Null)', + '
                                                                                                                                            deabc.insertBefore(
                                                                                                                                          1. d, Null)', + '
                                                                                                                                              eabcd.insertBefore(
                                                                                                                                            1. e, Null)' ]); }); @@ -327,7 +339,7 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('ba'); - expect(getLog()).to.deep.equal(['
                                                                                                                                                ab.insertBefore(
                                                                                                                                              1. b,
                                                                                                                                              2. a)']); + expect(getLog()).to.deep.equal(['
                                                                                                                                                  ab.insertBefore(
                                                                                                                                                1. a, Null)']); }); it('should swap existing keyed children in the middle of a list efficiently', () => { @@ -343,7 +355,7 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('acbd', 'initial swap'); expect(getLog()).to.deep.equal( - ['
                                                                                                                                                    abcd.insertBefore(
                                                                                                                                                  1. c,
                                                                                                                                                  2. b)'], + ['
                                                                                                                                                      abcd.insertBefore(
                                                                                                                                                    1. b,
                                                                                                                                                    2. d)'], 'initial swap' ); @@ -354,7 +366,7 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('abcd', 'swap back'); expect(getLog()).to.deep.equal( - ['
                                                                                                                                                        acbd.insertBefore(
                                                                                                                                                      1. b,
                                                                                                                                                      2. c)'], + ['
                                                                                                                                                          acbd.insertBefore(
                                                                                                                                                        1. c,
                                                                                                                                                        2. d)'], 'swap back' ); }); @@ -383,7 +395,13 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('abcd', 'move to beginning'); expect(getLog()).to.deep.equal( - ['
                                                                                                                                                            bcda.insertBefore(
                                                                                                                                                          1. a,
                                                                                                                                                          2. b)'], + // A perfect algorithm would do this in one move. Our algorithm is a compromise of size vs common case perf + // ['
                                                                                                                                                              bcda.insertBefore(
                                                                                                                                                            1. a,
                                                                                                                                                            2. b)'], + [ + '
                                                                                                                                                                bcda.insertBefore(
                                                                                                                                                              1. b, Null)', + '
                                                                                                                                                                  cdab.insertBefore(
                                                                                                                                                                1. c, Null)', + '
                                                                                                                                                                    dabc.insertBefore(
                                                                                                                                                                  1. d, Null)' + ], 'move to beginning' ); }); @@ -400,7 +418,13 @@ describe('keys', () => { render(, scratch); expect(scratch.textContent).to.equal('aebcdf'); - expect(getLog()).to.deep.equal(['
                                                                                                                                                                      abcdef.insertBefore(
                                                                                                                                                                    1. e,
                                                                                                                                                                    2. b)']); + // A perfect algorithm would do this in one move. Our algorithm is a compromise of size vs common case perf + // expect(getLog()).to.deep.equal(['
                                                                                                                                                                        abcdef.insertBefore(
                                                                                                                                                                      1. e,
                                                                                                                                                                      2. b)']); + expect(getLog()).to.deep.equal([ + '
                                                                                                                                                                          abcdef.insertBefore(
                                                                                                                                                                        1. b,
                                                                                                                                                                        2. f)', + '
                                                                                                                                                                            acdebf.insertBefore(
                                                                                                                                                                          1. c,
                                                                                                                                                                          2. f)', + '
                                                                                                                                                                              adebcf.insertBefore(
                                                                                                                                                                            1. d,
                                                                                                                                                                            2. f)' + ]); }); it('should move keyed children to the end on longer list', () => { @@ -431,15 +455,15 @@ describe('keys', () => { expect(scratch.textContent).to.equal(values.join('')); // expect(getLog()).to.have.lengthOf(9); expect(getLog()).to.deep.equal([ - '
                                                                                                                                                                                abcdefghij.insertBefore(
                                                                                                                                                                              1. j,
                                                                                                                                                                              2. a)', - '
                                                                                                                                                                                  jabcdefghi.insertBefore(
                                                                                                                                                                                1. i,
                                                                                                                                                                                2. a)', - '
                                                                                                                                                                                    jiabcdefgh.insertBefore(
                                                                                                                                                                                  1. h,
                                                                                                                                                                                  2. a)', - '
                                                                                                                                                                                      jihabcdefg.insertBefore(
                                                                                                                                                                                    1. g,
                                                                                                                                                                                    2. a)', - '
                                                                                                                                                                                        jihgabcdef.insertBefore(
                                                                                                                                                                                      1. f,
                                                                                                                                                                                      2. a)', - '
                                                                                                                                                                                          jihgfabcde.insertBefore(
                                                                                                                                                                                        1. e,
                                                                                                                                                                                        2. a)', - '
                                                                                                                                                                                            jihgfeabcd.insertBefore(
                                                                                                                                                                                          1. d,
                                                                                                                                                                                          2. a)', - '
                                                                                                                                                                                              jihgfedabc.insertBefore(
                                                                                                                                                                                            1. c,
                                                                                                                                                                                            2. a)', - '
                                                                                                                                                                                                jihgfedcab.insertBefore(
                                                                                                                                                                                              1. b,
                                                                                                                                                                                              2. a)' + '
                                                                                                                                                                                                  abcdefghij.insertBefore(
                                                                                                                                                                                                1. i, Null)', + '
                                                                                                                                                                                                    abcdefghji.insertBefore(
                                                                                                                                                                                                  1. h, Null)', + '
                                                                                                                                                                                                      abcdefgjih.insertBefore(
                                                                                                                                                                                                    1. g, Null)', + '
                                                                                                                                                                                                        abcdefjihg.insertBefore(
                                                                                                                                                                                                      1. f, Null)', + '
                                                                                                                                                                                                          abcdejihgf.insertBefore(
                                                                                                                                                                                                        1. e, Null)', + '
                                                                                                                                                                                                            abcdjihgfe.insertBefore(
                                                                                                                                                                                                          1. d, Null)', + '
                                                                                                                                                                                                              abcjihgfed.insertBefore(
                                                                                                                                                                                                            1. c, Null)', + '
                                                                                                                                                                                                                abjihgfedc.insertBefore(
                                                                                                                                                                                                              1. b, Null)', + '
                                                                                                                                                                                                                  ajihgfedcb.insertBefore(
                                                                                                                                                                                                                1. a, Null)' ]); }); diff --git a/test/browser/portals.test.js b/test/browser/portals.test.js index d6f4231cd2..ffbb1baeb8 100644 --- a/test/browser/portals.test.js +++ b/test/browser/portals.test.js @@ -104,12 +104,12 @@ describe('Portal', () => { const log = getLog().filter(t => !/#text/.test(t)); expect(log).to.deep.equal([ - '
                                                                                                                                                                                                                  .insertBefore(

                                                                                                                                                                                                                  A, )', + '.remove()', + '
                                                                                                                                                                                                                  .insertBefore(

                                                                                                                                                                                                                  A, )', '.insertBefore(

                                                                                                                                                                                                                  B, Null)', - '
                                                                                                                                                                                                                  A.insertBefore(

                                                                                                                                                                                                                  C, )', + '
                                                                                                                                                                                                                  A.insertBefore(

                                                                                                                                                                                                                  C, )', 'B.insertBefore(

                                                                                                                                                                                                                  D, Null)', - '
                                                                                                                                                                                                                  AC.insertBefore(
                                                                                                                                                                                                                  E, )', - '.remove()' + '
                                                                                                                                                                                                                  AC.insertBefore(
                                                                                                                                                                                                                  E, )' ]); }); From 2b042c0fca6d8b68e0799d4e17f5179f196496d6 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 17 Feb 2023 17:20:29 -0800 Subject: [PATCH 68/71] Skip over internals that haven't been mounted yet when searching for DOM nodes Fixes the portal test "should insert a portal before new siblings when changing container to match siblings" --- src/tree.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tree.js b/src/tree.js index bffad31101..cb53501139 100644 --- a/src/tree.js +++ b/src/tree.js @@ -186,7 +186,7 @@ export function getDomSibling(internal, childIndex) { */ export function getFirstDom(internal) { while (internal) { - if (internal.flags & INSERT_INTERNAL) { + if (internal.flags & INSERT_INTERNAL || internal._index == -1) { internal = internal._next; continue; } From ef08d9b2efffb3c889958656a44f40a082d2698d Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 6 Mar 2023 15:58:33 -0800 Subject: [PATCH 69/71] [debug] Remove WeakMap support check and improve options typing --- debug/src/component-stack.js | 4 +++- debug/src/debug.js | 17 +++++++---------- src/internal.d.ts | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/debug/src/component-stack.js b/debug/src/component-stack.js index bcba04d60f..c1ab7e0b11 100644 --- a/debug/src/component-stack.js +++ b/debug/src/component-stack.js @@ -1,4 +1,6 @@ -import { options, Fragment } from 'preact'; +import { options as rawOptions, Fragment } from 'preact'; + +const options = /** @type {import('../../src/internal').Options} */ (rawOptions); /** * Get human readable name of the component/dom node diff --git a/debug/src/debug.js b/debug/src/debug.js index 61ecb46ae5..b32ce71728 100644 --- a/debug/src/debug.js +++ b/debug/src/debug.js @@ -1,5 +1,5 @@ import { checkPropTypes } from './check-props'; -import { options, Component } from 'preact'; +import { options as rawOptions, Component } from 'preact'; import { ELEMENT_NODE, DOCUMENT_NODE, @@ -13,7 +13,7 @@ import { } from './component-stack'; import { IS_NON_DIMENSIONAL } from '../../compat/src/util'; -const isWeakMapSupported = typeof WeakMap == 'function'; +const options = /** @type {import('../../src/internal').Options} */ (rawOptions); function getClosestDomNodeParent(parent) { if (!parent) return {}; @@ -35,13 +35,11 @@ export function initDebug() { let oldCatchError = options._catchError; let oldRoot = options._root; let oldHook = options._hook; - const warnedComponents = !isWeakMapSupported - ? null - : { - useEffect: new WeakMap(), - useLayoutEffect: new WeakMap(), - lazyPropTypes: new WeakMap() - }; + const warnedComponents = { + useEffect: new WeakMap(), + useLayoutEffect: new WeakMap(), + lazyPropTypes: new WeakMap() + }; const deprecations = []; options._catchError = (error, vnode, oldVNode) => { @@ -244,7 +242,6 @@ export function initDebug() { if (typeof internal.type == 'function' && internal.type.propTypes) { if ( internal.type.displayName === 'Lazy' && - warnedComponents && !warnedComponents.lazyPropTypes.has(internal.type) ) { const m = diff --git a/src/internal.d.ts b/src/internal.d.ts index 3902b89457..7b1037e229 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -31,7 +31,7 @@ export interface Options extends preact.Options { parent: Element | Document | ShadowRoot | DocumentFragment ): void; /** Attach a hook that is invoked before a vnode is diffed. */ - _diff?(internal: Internal, vnode?: VNode | string): void; + _diff?(internal: Internal, vnode: VNode | string | null): void; /** Attach a hook that is invoked after a tree was mounted or was updated. */ _commit?(internal: Internal, commitQueue: CommitQueue): void; /** Attach a hook that is invoked before a vnode has rendered. */ From 77cb172313e80c65e9514c9a2b0f9a682a41ca2f Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 6 Mar 2023 16:08:28 -0800 Subject: [PATCH 70/71] Improve typing inference on debug option hooks --- debug/src/debug.js | 59 +++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/debug/src/debug.js b/debug/src/debug.js index b32ce71728..2d6011fd28 100644 --- a/debug/src/debug.js +++ b/debug/src/debug.js @@ -42,15 +42,25 @@ export function initDebug() { }; const deprecations = []; - options._catchError = (error, vnode, oldVNode) => { - let component = vnode && vnode._component; + options._catchError = catchErrorHook; + options._root = rootHook; + options._diff = diffHook; + options._hook = hookHook; + options.vnode = vnodeHook; + options.diffed = diffedHook; + + /** @type {typeof options["_catchError"]} */ + function catchErrorHook(error, internal) { + let component = internal && internal._component; if (component && typeof error.then == 'function') { const promise = error; error = new Error( - `Missing Suspense. The throwing component was: ${getDisplayName(vnode)}` + `Missing Suspense. The throwing component was: ${getDisplayName( + internal + )}` ); - let parent = vnode; + let parent = internal; for (; parent; parent = parent._parent) { if (parent._component && parent._component._childDidSuspend) { error = promise; @@ -66,7 +76,7 @@ export function initDebug() { } try { - oldCatchError(error, vnode, oldVNode); + oldCatchError(error, internal); // when an error was handled by an ErrorBoundary we will nontheless emit an error // event on the window object. This is to make up for react compatibility in dev mode @@ -79,9 +89,10 @@ export function initDebug() { } catch (e) { throw e; } - }; + } - options._root = (vnode, parentNode) => { + /** @type {typeof options["_root"]} */ + function rootHook(vnode, parentNode) { if (!parentNode) { throw new Error( 'Undefined parent passed to render(), this is the second argument.\n' + @@ -108,9 +119,10 @@ export function initDebug() { } if (oldRoot) oldRoot(vnode, parentNode); - }; + } - options._diff = (internal, vnode) => { + /** @type {typeof options["_diff"]} */ + function diffHook(internal, vnode) { if (vnode === null || typeof vnode !== 'object') return; // Check if the user passed plain objects as children. Note that we cannot // move this check into `options.vnode` because components can receive @@ -276,15 +288,16 @@ export function initDebug() { } if (oldBeforeDiff) oldBeforeDiff(internal, vnode); - }; + } - options._hook = (internal, index, type) => { + /** @type {typeof options["_hook"]} */ + function hookHook(internal, index, type) { if (!internal || !hooksAllowed) { throw new Error('Hook can only be invoked from render methods.'); } if (oldHook) oldHook(internal, index, type); - }; + } // Ideally we'd want to print a warning once per component, but we // don't have access to the vnode that triggered it here. As a @@ -324,7 +337,8 @@ export function initDebug() { // https://esbench.com/bench/6021ebd7d9c27600a7bfdba3 const deprecatedProto = Object.create({}, deprecatedAttributes); - options.vnode = vnode => { + /** @type {typeof options["vnode"]} */ + function vnodeHook(vnode) { const props = vnode.props; if (props != null && ('__source' in props || '__self' in props)) { Object.defineProperties(props, debugProps); @@ -335,17 +349,18 @@ export function initDebug() { // eslint-disable-next-line vnode.__proto__ = deprecatedProto; if (oldVnode) oldVnode(vnode); - }; + } - options.diffed = vnode => { + /** @type {typeof options["diffed"]} */ + function diffedHook(internal) { hooksAllowed = false; - if (oldDiffed) oldDiffed(vnode); + if (oldDiffed) oldDiffed(internal); - if (vnode._children != null) { + if (internal._children != null) { const keys = []; - for (let i = 0; i < vnode._children.length; i++) { - const child = vnode._children[i]; + for (let i = 0; i < internal._children.length; i++) { + const child = internal._children[i]; if (!child || child.key == null) continue; const key = child.key; @@ -354,8 +369,8 @@ export function initDebug() { 'Following component has two or more children with the ' + `same key attribute: "${key}". This may cause glitches and misbehavior ` + 'in rendering process. Component: \n\n' + - serializeVNode(vnode) + - `\n\n${getOwnerStack(vnode)}` + serializeVNode(internal) + + `\n\n${getOwnerStack(internal)}` ); // Break early to not spam the console @@ -365,7 +380,7 @@ export function initDebug() { keys.push(key); } } - }; + } } const setState = Component.prototype.setState; From 1e505c5a9fc211e4bcee7671829f14452393c9dd Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 6 Mar 2023 16:37:37 -0800 Subject: [PATCH 71/71] Move markup validations to their own file --- debug/src/component-stack.js | 17 ++++++----- debug/src/debug.js | 52 +++++--------------------------- debug/src/validateMarkup.js | 58 ++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 52 deletions(-) create mode 100644 debug/src/validateMarkup.js diff --git a/debug/src/component-stack.js b/debug/src/component-stack.js index c1ab7e0b11..0eef9ebbe7 100644 --- a/debug/src/component-stack.js +++ b/debug/src/component-stack.js @@ -4,17 +4,18 @@ const options = /** @type {import('../../src/internal').Options} */ (rawOptions) /** * Get human readable name of the component/dom node - * @param {import('./internal').VNode} vnode - * @param {import('./internal').VNode} vnode + * @param {import('../../src/internal').ComponentChild} vnode * @returns {string} */ export function getDisplayName(vnode) { - if (vnode.type === Fragment) { - return 'Fragment'; - } else if (typeof vnode.type == 'function') { - return vnode.type.displayName || vnode.type.name; - } else if (typeof vnode.type == 'string') { - return vnode.type; + if (vnode != null && typeof vnode === 'object') { + if (vnode.type === Fragment) { + return 'Fragment'; + } else if (typeof vnode.type == 'function') { + return vnode.type.displayName || vnode.type.name; + } else if (typeof vnode.type == 'string') { + return vnode.type; + } } return '#text'; diff --git a/debug/src/debug.js b/debug/src/debug.js index 2d6011fd28..0aa0573ccd 100644 --- a/debug/src/debug.js +++ b/debug/src/debug.js @@ -12,17 +12,10 @@ import { getDisplayName } from './component-stack'; import { IS_NON_DIMENSIONAL } from '../../compat/src/util'; +import { validateTableMarkup } from './validateMarkup'; const options = /** @type {import('../../src/internal').Options} */ (rawOptions); -function getClosestDomNodeParent(parent) { - if (!parent) return {}; - if (typeof parent.type == 'function') { - return getClosestDomNodeParent(parent._parent); - } - return parent; -} - export function initDebug() { setupComponentStack(); @@ -123,7 +116,12 @@ export function initDebug() { /** @type {typeof options["_diff"]} */ function diffHook(internal, vnode) { - if (vnode === null || typeof vnode !== 'object') return; + if (vnode === null || typeof vnode !== 'object') { + // TODO: This isn't correct. We need these checks to run on mount + oldBeforeDiff(internal, vnode); + return; + } + // Check if the user passed plain objects as children. Note that we cannot // move this check into `options.vnode` because components can receive // children in any shape they want (e.g. @@ -163,44 +161,10 @@ export function initDebug() { ); } - let parentVNode = getClosestDomNodeParent(parent); + validateTableMarkup(internal); hooksAllowed = true; - if ( - (type === 'thead' || type === 'tfoot' || type === 'tbody') && - parentVNode.type !== 'table' - ) { - console.error( - 'Improper nesting of table. Your should have a parent.' + - serializeVNode(internal) + - `\n\n${getOwnerStack(internal)}` - ); - } else if ( - type === 'tr' && - parentVNode.type !== 'thead' && - parentVNode.type !== 'tfoot' && - parentVNode.type !== 'tbody' && - parentVNode.type !== 'table' - ) { - console.error( - 'Improper nesting of table. Your should have a parent.' + - serializeVNode(internal) + - `\n\n${getOwnerStack(internal)}` - ); - } else if (type === 'td' && parentVNode.type !== 'tr') { - console.error( - 'Improper nesting of table. Your parent.' + - serializeVNode(internal) + - `\n\n${getOwnerStack(internal)}` - ); - } else if (type === 'th' && parentVNode.type !== 'tr') { - console.error( - 'Improper nesting of table. Your .' + - serializeVNode(internal) + - `\n\n${getOwnerStack(internal)}` - ); - } let isCompatNode = '$$typeof' in vnode; if ( internal.ref !== undefined && diff --git a/debug/src/validateMarkup.js b/debug/src/validateMarkup.js new file mode 100644 index 0000000000..7040ab5ec2 --- /dev/null +++ b/debug/src/validateMarkup.js @@ -0,0 +1,58 @@ +import { getOwnerStack } from './component-stack'; +import { serializeVNode } from './debug'; + +/** + * @param {import('./internal').Internal} parent + * @returns {import('./internal').Internal | null} + */ +function getClosestDomNodeParent(parent) { + if (!parent) return null; + if (typeof parent.type == 'function') { + return getClosestDomNodeParent(parent._parent); + } + return parent; +} + +/** + * @param {import('./internal').Internal} internal + * @return {void} + */ +export function validateTableMarkup(internal) { + const { type, _parent: parent } = internal; + const parentDomInternal = getClosestDomNodeParent(parent); + + if ( + (type === 'thead' || type === 'tfoot' || type === 'tbody') && + parentDomInternal.type !== 'table' + ) { + console.error( + 'Improper nesting of table. Your should have a
                                                                                                                                                                                                                  should have a
                                                                                                                                                                                                                  should have a
                                                                                                                                                                                                                  parent.' + + serializeVNode(internal) + + `\n\n${getOwnerStack(internal)}` + ); + } else if ( + type === 'tr' && + parentDomInternal.type !== 'thead' && + parentDomInternal.type !== 'tfoot' && + parentDomInternal.type !== 'tbody' && + parentDomInternal.type !== 'table' + ) { + console.error( + 'Improper nesting of table. Your should have a parent.' + + serializeVNode(internal) + + `\n\n${getOwnerStack(internal)}` + ); + } else if (type === 'td' && parentDomInternal.type !== 'tr') { + console.error( + 'Improper nesting of table. Your parent.' + + serializeVNode(internal) + + `\n\n${getOwnerStack(internal)}` + ); + } else if (type === 'th' && parentDomInternal.type !== 'tr') { + console.error( + 'Improper nesting of table. Your .' + + serializeVNode(internal) + + `\n\n${getOwnerStack(internal)}` + ); + } +}
                                                                                                                                                                                                                  should have a
                                                                                                                                                                                                                  should have a