From 15183da071ed40308f6206d4940c5da2188dc5d0 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Tue, 14 Dec 2021 07:24:34 +0100 Subject: [PATCH 01/17] chore: move all the diffing APIs in hooks.ts --- .../src/3rdparty/snabbdom/snabbdom.ts | 214 -------------- .../@lwc/engine-core/src/framework/hooks.ts | 278 +++++++++++++++++- packages/@lwc/engine-core/src/framework/vm.ts | 58 +--- 3 files changed, 269 insertions(+), 281 deletions(-) delete mode 100644 packages/@lwc/engine-core/src/3rdparty/snabbdom/snabbdom.ts diff --git a/packages/@lwc/engine-core/src/3rdparty/snabbdom/snabbdom.ts b/packages/@lwc/engine-core/src/3rdparty/snabbdom/snabbdom.ts deleted file mode 100644 index fc72a2d962..0000000000 --- a/packages/@lwc/engine-core/src/3rdparty/snabbdom/snabbdom.ts +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (c) 2018, salesforce.com, inc. - * All rights reserved. - * SPDX-License-Identifier: MIT - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT - */ -/** -@license -Copyright (c) 2015 Simon Friis Vindum. -This code may only be used under the MIT License found at -https://github.com/snabbdom/snabbdom/blob/master/LICENSE -Code distributed by Snabbdom as part of the Snabbdom project at -https://github.com/snabbdom/snabbdom/ -*/ - -import { VNode, VNodes, Key } from './types'; - -function isUndef(s: any): s is undefined { - return s === undefined; -} - -interface KeyToIndexMap { - [key: string]: number; -} - -function sameVnode(vnode1: VNode, vnode2: VNode): boolean { - return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; -} - -function isVNode(vnode: any): vnode is VNode { - return vnode != null; -} - -function createKeyToOldIdx(children: VNodes, beginIdx: number, endIdx: number): KeyToIndexMap { - const map: KeyToIndexMap = {}; - let j: number, key: Key | undefined, ch; - // TODO [#1637]: simplify this by assuming that all vnodes has keys - for (j = beginIdx; j <= endIdx; ++j) { - ch = children[j]; - if (isVNode(ch)) { - key = ch.key; - if (key !== undefined) { - map[key] = j; - } - } - } - return map; -} - -function addVnodes( - parentElm: Node, - before: Node | null, - vnodes: VNodes, - startIdx: number, - endIdx: number -) { - for (; startIdx <= endIdx; ++startIdx) { - const ch = vnodes[startIdx]; - if (isVNode(ch)) { - ch.hook.create(ch); - ch.hook.insert(ch, parentElm, before); - } - } -} - -function removeVnodes(parentElm: Node, vnodes: VNodes, startIdx: number, endIdx: number): void { - for (; startIdx <= endIdx; ++startIdx) { - const ch = vnodes[startIdx]; - // text nodes do not have logic associated to them - if (isVNode(ch)) { - ch.hook.remove(ch, parentElm); - } - } -} - -export function updateDynamicChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes) { - let oldStartIdx = 0; - let newStartIdx = 0; - let oldEndIdx = oldCh.length - 1; - let oldStartVnode = oldCh[0]; - let oldEndVnode = oldCh[oldEndIdx]; - const newChEnd = newCh.length - 1; - let newEndIdx = newChEnd; - let newStartVnode = newCh[0]; - let newEndVnode = newCh[newEndIdx]; - let oldKeyToIdx: any; - let idxInOld: number; - let elmToMove: VNode | null | undefined; - let before: any; - while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { - if (!isVNode(oldStartVnode)) { - oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left - } else if (!isVNode(oldEndVnode)) { - oldEndVnode = oldCh[--oldEndIdx]; - } else if (!isVNode(newStartVnode)) { - newStartVnode = newCh[++newStartIdx]; - } else if (!isVNode(newEndVnode)) { - newEndVnode = newCh[--newEndIdx]; - } else if (sameVnode(oldStartVnode, newStartVnode)) { - patchVnode(oldStartVnode, newStartVnode); - oldStartVnode = oldCh[++oldStartIdx]; - newStartVnode = newCh[++newStartIdx]; - } else if (sameVnode(oldEndVnode, newEndVnode)) { - patchVnode(oldEndVnode, newEndVnode); - oldEndVnode = oldCh[--oldEndIdx]; - newEndVnode = newCh[--newEndIdx]; - } else if (sameVnode(oldStartVnode, newEndVnode)) { - // Vnode moved right - patchVnode(oldStartVnode, newEndVnode); - newEndVnode.hook.move( - oldStartVnode, - parentElm, - oldEndVnode.owner.renderer.nextSibling(oldEndVnode.elm!) - ); - oldStartVnode = oldCh[++oldStartIdx]; - newEndVnode = newCh[--newEndIdx]; - } else if (sameVnode(oldEndVnode, newStartVnode)) { - // Vnode moved left - patchVnode(oldEndVnode, newStartVnode); - newStartVnode.hook.move(oldEndVnode, parentElm, oldStartVnode.elm!); - oldEndVnode = oldCh[--oldEndIdx]; - newStartVnode = newCh[++newStartIdx]; - } else { - if (oldKeyToIdx === undefined) { - oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); - } - idxInOld = oldKeyToIdx[newStartVnode.key!]; - if (isUndef(idxInOld)) { - // New element - newStartVnode.hook.create(newStartVnode); - newStartVnode.hook.insert(newStartVnode, parentElm, oldStartVnode.elm!); - newStartVnode = newCh[++newStartIdx]; - } else { - elmToMove = oldCh[idxInOld]; - if (isVNode(elmToMove)) { - if (elmToMove.sel !== newStartVnode.sel) { - // New element - newStartVnode.hook.create(newStartVnode); - newStartVnode.hook.insert(newStartVnode, parentElm, oldStartVnode.elm!); - } else { - patchVnode(elmToMove, newStartVnode); - oldCh[idxInOld] = undefined as any; - newStartVnode.hook.move(elmToMove, parentElm, oldStartVnode.elm!); - } - } - newStartVnode = newCh[++newStartIdx]; - } - } - } - if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { - if (oldStartIdx > oldEndIdx) { - // There's some cases in which the sub array of vnodes to be inserted is followed by null(s) and an - // already processed vnode, in such cases the vnodes to be inserted should be before that processed vnode. - let i = newEndIdx; - let n; - do { - n = newCh[++i]; - } while (!isVNode(n) && i < newChEnd); - before = isVNode(n) ? n.elm : null; - addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx); - } else { - removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); - } - } -} - -export function updateStaticChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes) { - const oldChLength = oldCh.length; - const newChLength = newCh.length; - - if (oldChLength === 0) { - // the old list is empty, we can directly insert anything new - addVnodes(parentElm, null, newCh, 0, newChLength); - return; - } - if (newChLength === 0) { - // the old list is nonempty and the new list is empty so we can directly remove all old nodes - // this is the case in which the dynamic children of an if-directive should be removed - removeVnodes(parentElm, oldCh, 0, oldChLength); - return; - } - // if the old list is not empty, the new list MUST have the same - // amount of nodes, that's why we call this static children - let referenceElm: Node | null = null; - for (let i = newChLength - 1; i >= 0; i -= 1) { - const vnode = newCh[i]; - const oldVNode = oldCh[i]; - if (vnode !== oldVNode) { - if (isVNode(oldVNode)) { - if (isVNode(vnode)) { - // both vnodes must be equivalent, and se just need to patch them - patchVnode(oldVNode, vnode); - referenceElm = vnode.elm!; - } else { - // removing the old vnode since the new one is null - oldVNode.hook.remove(oldVNode, parentElm); - } - } else if (isVNode(vnode)) { - // this condition is unnecessary - vnode.hook.create(vnode); - // insert the new node one since the old one is null - vnode.hook.insert(vnode, parentElm, referenceElm); - referenceElm = vnode.elm!; - } - } - } -} - -function patchVnode(oldVnode: VNode, vnode: VNode) { - if (oldVnode !== vnode) { - vnode.elm = oldVnode.elm; - vnode.hook.update(oldVnode, vnode); - } -} diff --git a/packages/@lwc/engine-core/src/framework/hooks.ts b/packages/@lwc/engine-core/src/framework/hooks.ts index 5388cdf7c9..a75a1edf4a 100644 --- a/packages/@lwc/engine-core/src/framework/hooks.ts +++ b/packages/@lwc/engine-core/src/framework/hooks.ts @@ -4,17 +4,20 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { ArrayFilter, ArrayJoin, assert, isArray, isNull, isUndefined, keys } from '@lwc/shared'; -import { EmptyArray, parseStyleText } from './utils'; import { - createVM, - allocateInSlot, - getAssociatedVMIfPresent, - VM, - ShadowMode, - RenderMode, -} from './vm'; -import { VNode, VCustomElement, VElement, VNodes } from '../3rdparty/snabbdom/types'; + ArrayFilter, + ArrayJoin, + ArrayPush, + assert, + create, + isArray, + isNull, + isUndefined, + keys, +} from '@lwc/shared'; +import { EmptyArray, parseStyleText } from './utils'; +import { createVM, getAssociatedVMIfPresent, VM, ShadowMode, RenderMode } from './vm'; +import { VNode, VCustomElement, VElement, VNodes, Key } from '../3rdparty/snabbdom/types'; import modEvents from './modules/events'; import modAttrs from './modules/attrs'; import modProps from './modules/props'; @@ -22,10 +25,10 @@ import modComputedClassName from './modules/computed-class-attr'; import modComputedStyle from './modules/computed-style-attr'; import modStaticClassName from './modules/static-class-attr'; import modStaticStyle from './modules/static-style-attr'; -import { updateDynamicChildren, updateStaticChildren } from '../3rdparty/snabbdom/snabbdom'; import { patchElementWithRestrictions, unlockDomMutation, lockDomMutation } from './restrictions'; import { getComponentInternalDef } from './def'; import { logError } from '../shared/logger'; +import { markComponentAsDirty } from './component'; function observeElementChildNodes(elm: Element) { (elm as any).$domManual$ = true; @@ -447,6 +450,257 @@ export function removeElmHook(vnode: VElement) { } } +interface KeyToIndexMap { + [key: string]: number; +} + +function sameVnode(vnode1: VNode, vnode2: VNode): boolean { + return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; +} + +function isVNode(vnode: any): vnode is VNode { + return vnode != null; +} + +function createKeyToOldIdx(children: VNodes, beginIdx: number, endIdx: number): KeyToIndexMap { + const map: KeyToIndexMap = {}; + let j: number, key: Key | undefined, ch; + // TODO [#1637]: simplify this by assuming that all vnodes has keys + for (j = beginIdx; j <= endIdx; ++j) { + ch = children[j]; + if (isVNode(ch)) { + key = ch.key; + if (key !== undefined) { + map[key] = j; + } + } + } + return map; +} + +function addVnodes( + parentElm: Node, + before: Node | null, + vnodes: VNodes, + startIdx: number, + endIdx: number +) { + for (; startIdx <= endIdx; ++startIdx) { + const ch = vnodes[startIdx]; + if (isVNode(ch)) { + ch.hook.create(ch); + ch.hook.insert(ch, parentElm, before); + } + } +} + +function removeVnodes(parentElm: Node, vnodes: VNodes, startIdx: number, endIdx: number): void { + for (; startIdx <= endIdx; ++startIdx) { + const ch = vnodes[startIdx]; + // text nodes do not have logic associated to them + if (isVNode(ch)) { + ch.hook.remove(ch, parentElm); + } + } +} + +function updateDynamicChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes) { + let oldStartIdx = 0; + let newStartIdx = 0; + let oldEndIdx = oldCh.length - 1; + let oldStartVnode = oldCh[0]; + let oldEndVnode = oldCh[oldEndIdx]; + const newChEnd = newCh.length - 1; + let newEndIdx = newChEnd; + let newStartVnode = newCh[0]; + let newEndVnode = newCh[newEndIdx]; + let oldKeyToIdx: any; + let idxInOld: number; + let elmToMove: VNode | null | undefined; + let before: any; + while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { + if (!isVNode(oldStartVnode)) { + oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left + } else if (!isVNode(oldEndVnode)) { + oldEndVnode = oldCh[--oldEndIdx]; + } else if (!isVNode(newStartVnode)) { + newStartVnode = newCh[++newStartIdx]; + } else if (!isVNode(newEndVnode)) { + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldStartVnode, newStartVnode)) { + patchVnode(oldStartVnode, newStartVnode); + oldStartVnode = oldCh[++oldStartIdx]; + newStartVnode = newCh[++newStartIdx]; + } else if (sameVnode(oldEndVnode, newEndVnode)) { + patchVnode(oldEndVnode, newEndVnode); + oldEndVnode = oldCh[--oldEndIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldStartVnode, newEndVnode)) { + // Vnode moved right + patchVnode(oldStartVnode, newEndVnode); + newEndVnode.hook.move( + oldStartVnode, + parentElm, + oldEndVnode.owner.renderer.nextSibling(oldEndVnode.elm!) + ); + oldStartVnode = oldCh[++oldStartIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldEndVnode, newStartVnode)) { + // Vnode moved left + patchVnode(oldEndVnode, newStartVnode); + newStartVnode.hook.move(oldEndVnode, parentElm, oldStartVnode.elm!); + oldEndVnode = oldCh[--oldEndIdx]; + newStartVnode = newCh[++newStartIdx]; + } else { + if (oldKeyToIdx === undefined) { + oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); + } + idxInOld = oldKeyToIdx[newStartVnode.key!]; + if (isUndefined(idxInOld)) { + // New element + newStartVnode.hook.create(newStartVnode); + newStartVnode.hook.insert(newStartVnode, parentElm, oldStartVnode.elm!); + newStartVnode = newCh[++newStartIdx]; + } else { + elmToMove = oldCh[idxInOld]; + if (isVNode(elmToMove)) { + if (elmToMove.sel !== newStartVnode.sel) { + // New element + newStartVnode.hook.create(newStartVnode); + newStartVnode.hook.insert(newStartVnode, parentElm, oldStartVnode.elm!); + } else { + patchVnode(elmToMove, newStartVnode); + oldCh[idxInOld] = undefined as any; + newStartVnode.hook.move(elmToMove, parentElm, oldStartVnode.elm!); + } + } + newStartVnode = newCh[++newStartIdx]; + } + } + } + if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { + if (oldStartIdx > oldEndIdx) { + // There's some cases in which the sub array of vnodes to be inserted is followed by null(s) and an + // already processed vnode, in such cases the vnodes to be inserted should be before that processed vnode. + let i = newEndIdx; + let n; + do { + n = newCh[++i]; + } while (!isVNode(n) && i < newChEnd); + before = isVNode(n) ? n.elm : null; + addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx); + } else { + removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); + } + } +} + +function updateStaticChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes) { + const oldChLength = oldCh.length; + const newChLength = newCh.length; + + if (oldChLength === 0) { + // the old list is empty, we can directly insert anything new + addVnodes(parentElm, null, newCh, 0, newChLength); + return; + } + if (newChLength === 0) { + // the old list is nonempty and the new list is empty so we can directly remove all old nodes + // this is the case in which the dynamic children of an if-directive should be removed + removeVnodes(parentElm, oldCh, 0, oldChLength); + return; + } + // if the old list is not empty, the new list MUST have the same + // amount of nodes, that's why we call this static children + let referenceElm: Node | null = null; + for (let i = newChLength - 1; i >= 0; i -= 1) { + const vnode = newCh[i]; + const oldVNode = oldCh[i]; + if (vnode !== oldVNode) { + if (isVNode(oldVNode)) { + if (isVNode(vnode)) { + // both vnodes must be equivalent, and se just need to patch them + patchVnode(oldVNode, vnode); + referenceElm = vnode.elm!; + } else { + // removing the old vnode since the new one is null + oldVNode.hook.remove(oldVNode, parentElm); + } + } else if (isVNode(vnode)) { + // this condition is unnecessary + vnode.hook.create(vnode); + // insert the new node one since the old one is null + vnode.hook.insert(vnode, parentElm, referenceElm); + referenceElm = vnode.elm!; + } + } + } +} + +export function patchChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes): void { + if (hasDynamicChildren(newCh)) { + updateDynamicChildren(parentElm, oldCh, newCh); + } else { + updateStaticChildren(parentElm, oldCh, newCh); + } +} + +function patchVnode(oldVnode: VNode, vnode: VNode) { + if (oldVnode !== vnode) { + vnode.elm = oldVnode.elm; + vnode.hook.update(oldVnode, vnode); + } +} + +// slow path routine +// NOTE: we should probably more this routine to the synthetic shadow folder +// and get the allocation to be cached by in the elm instead of in the VM +function allocateInSlot(vm: VM, children: VNodes) { + const { cmpSlots: oldSlots } = vm; + const cmpSlots = (vm.cmpSlots = create(null)); + for (let i = 0, len = children.length; i < len; i += 1) { + const vnode = children[i]; + if (isNull(vnode)) { + continue; + } + const { data } = vnode; + const slotName = (data.attrs?.slot ?? '') as string; + const vnodes = (cmpSlots[slotName] = cmpSlots[slotName] || []); + // re-keying the vnodes is necessary to avoid conflicts with default content for the slot + // which might have similar keys. Each vnode will always have a key that + // starts with a numeric character from compiler. In this case, we add a unique + // notation for slotted vnodes keys, e.g.: `@foo:1:1` + if (!isUndefined(vnode.key)) { + vnode.key = `@${slotName}:${vnode.key}`; + } + ArrayPush.call(vnodes, vnode); + } + if (!vm.isDirty) { + // We need to determine if the old allocation is really different from the new one + // and mark the vm as dirty + const oldKeys = keys(oldSlots); + if (oldKeys.length !== keys(cmpSlots).length) { + markComponentAsDirty(vm); + return; + } + for (let i = 0, len = oldKeys.length; i < len; i += 1) { + const key = oldKeys[i]; + if (isUndefined(cmpSlots[key]) || oldSlots[key].length !== cmpSlots[key].length) { + markComponentAsDirty(vm); + return; + } + const oldVNodes = oldSlots[key]; + const vnodes = cmpSlots[key]; + for (let j = 0, a = cmpSlots[key].length; j < a; j += 1) { + if (oldVNodes[j] !== vnodes[j]) { + markComponentAsDirty(vm); + return; + } + } + } + } +} + // Using a WeakMap instead of a WeakSet because this one works in IE11 :( const FromIteration: WeakMap = new WeakMap(); @@ -456,6 +710,6 @@ export function markAsDynamicChildren(children: VNodes) { FromIteration.set(children, 1); } -export function hasDynamicChildren(children: VNodes): boolean { +function hasDynamicChildren(children: VNodes): boolean { return FromIteration.has(children); } diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index 6bef3c62a4..93e8a64738 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -18,7 +18,6 @@ import { isObject, isTrue, isUndefined, - keys, } from '@lwc/shared'; import { renderComponent, markComponentAsDirty, getTemplateReactiveObserver } from './component'; import { addCallbackToNextTick, EmptyArray, EmptyObject } from './utils'; @@ -34,14 +33,13 @@ import { logGlobalOperationEnd, logGlobalOperationStart, } from './profiler'; -import { hasDynamicChildren, hydrateChildrenHook } from './hooks'; +import { hydrateChildrenHook, patchChildren } from './hooks'; import { ReactiveObserver } from './mutation-tracker'; import { connectWireAdapters, disconnectWireAdapters, installWireAdapters } from './wiring'; import { AccessorReactiveObserver } from './decorators/api'; import { Renderer, HostNode, HostElement } from './renderer'; import { removeActiveVM } from './hot-swaps'; -import { updateDynamicChildren, updateStaticChildren } from '../3rdparty/snabbdom/snabbdom'; import { VNodes, VCustomElement, VNode } from '../3rdparty/snabbdom/types'; import { addErrorComponentStack } from '../shared/error'; @@ -447,7 +445,6 @@ function patchShadowRoot(vm: VM, newCh: VNodes) { // patch function mutates vnodes by adding the element reference, // however, if patching fails it contains partial changes. if (oldCh !== newCh) { - const fn = hasDynamicChildren(newCh) ? updateDynamicChildren : updateStaticChildren; runWithBoundaryProtection( vm, vm, @@ -457,8 +454,8 @@ function patchShadowRoot(vm: VM, newCh: VNodes) { }, () => { // job - const elementToRenderTo = getRenderRoot(vm); - fn(elementToRenderTo, oldCh, newCh); + const renderRoot = getRenderRoot(vm); + patchChildren(renderRoot, oldCh, newCh); }, () => { // post @@ -696,55 +693,6 @@ function getErrorBoundaryVM(vm: VM): VM | undefined { } } -// slow path routine -// NOTE: we should probably more this routine to the synthetic shadow folder -// and get the allocation to be cached by in the elm instead of in the VM -export function allocateInSlot(vm: VM, children: VNodes) { - const { cmpSlots: oldSlots } = vm; - const cmpSlots = (vm.cmpSlots = create(null)); - for (let i = 0, len = children.length; i < len; i += 1) { - const vnode = children[i]; - if (isNull(vnode)) { - continue; - } - const { data } = vnode; - const slotName = ((data.attrs && data.attrs.slot) || '') as string; - const vnodes: VNodes = (cmpSlots[slotName] = cmpSlots[slotName] || []); - // re-keying the vnodes is necessary to avoid conflicts with default content for the slot - // which might have similar keys. Each vnode will always have a key that - // starts with a numeric character from compiler. In this case, we add a unique - // notation for slotted vnodes keys, e.g.: `@foo:1:1` - if (!isUndefined(vnode.key)) { - vnode.key = `@${slotName}:${vnode.key}`; - } - ArrayPush.call(vnodes, vnode); - } - if (isFalse(vm.isDirty)) { - // We need to determine if the old allocation is really different from the new one - // and mark the vm as dirty - const oldKeys = keys(oldSlots); - if (oldKeys.length !== keys(cmpSlots).length) { - markComponentAsDirty(vm); - return; - } - for (let i = 0, len = oldKeys.length; i < len; i += 1) { - const key = oldKeys[i]; - if (isUndefined(cmpSlots[key]) || oldSlots[key].length !== cmpSlots[key].length) { - markComponentAsDirty(vm); - return; - } - const oldVNodes = oldSlots[key]; - const vnodes = cmpSlots[key]; - for (let j = 0, a = cmpSlots[key].length; j < a; j += 1) { - if (oldVNodes[j] !== vnodes[j]) { - markComponentAsDirty(vm); - return; - } - } - } - } -} - export function runWithBoundaryProtection( vm: VM, owner: VM | null, From 1e8f7e9379aa9264873ea570ffab1f7873b52b0d Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Tue, 14 Dec 2021 08:09:21 +0100 Subject: [PATCH 02/17] chore: remove duplicate patchChildren --- packages/@lwc/engine-core/src/framework/api.ts | 6 +++--- packages/@lwc/engine-core/src/framework/hooks.ts | 9 --------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/api.ts b/packages/@lwc/engine-core/src/framework/api.ts index 8073283d4d..1c9099e5cf 100644 --- a/packages/@lwc/engine-core/src/framework/api.ts +++ b/packages/@lwc/engine-core/src/framework/api.ts @@ -66,7 +66,7 @@ import { updateElmHook, createCustomElmHook, updateCustomElmHook, - updateChildrenHook, + patchChildren, allocateChildrenHook, markAsDynamicChildren, hydrateChildrenHook, @@ -174,7 +174,7 @@ const ElementHook: Hooks = { }, update: (oldVnode, vnode) => { updateElmHook(oldVnode, vnode); - updateChildrenHook(oldVnode, vnode); + patchChildren(vnode.elm!, oldVnode.children, vnode.children); }, insert: (vnode, parentNode, referenceNode) => { insertNodeHook(vnode, parentNode, referenceNode); @@ -259,7 +259,7 @@ const CustomElementHook: Hooks = { } // in fallback mode, the children will be always empty, so, nothing // will happen, but in native, it does allocate the light dom - updateChildrenHook(oldVnode, vnode); + patchChildren(vnode.elm!, oldVnode.children, vnode.children); if (vm) { if (process.env.NODE_ENV !== 'production') { assert.isTrue( diff --git a/packages/@lwc/engine-core/src/framework/hooks.ts b/packages/@lwc/engine-core/src/framework/hooks.ts index a75a1edf4a..83765ac543 100644 --- a/packages/@lwc/engine-core/src/framework/hooks.ts +++ b/packages/@lwc/engine-core/src/framework/hooks.ts @@ -161,15 +161,6 @@ export function updateElmHook(oldVnode: VElement, vnode: VElement) { modComputedStyle.update(oldVnode, vnode); } -export function updateChildrenHook(oldVnode: VElement, vnode: VElement) { - const { elm, children } = vnode; - if (hasDynamicChildren(children)) { - updateDynamicChildren(elm!, oldVnode.children, children); - } else { - updateStaticChildren(elm!, oldVnode.children, children); - } -} - export function allocateChildrenHook(vnode: VCustomElement, vm: VM) { // A component with slots will re-render because: // 1- There is a change of the internal state. From 7b96e6fc2ab2c65a13fbc2f2b7eac8e5f2392ddc Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Tue, 14 Dec 2021 08:22:40 +0100 Subject: [PATCH 03/17] chore: move all the hooks to hooks --- .../@lwc/engine-core/src/framework/api.ts | 318 +---------------- .../@lwc/engine-core/src/framework/hooks.ts | 327 +++++++++++++++++- 2 files changed, 320 insertions(+), 325 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/api.ts b/packages/@lwc/engine-core/src/framework/api.ts index 1c9099e5cf..6e10e29fd4 100644 --- a/packages/@lwc/engine-core/src/framework/api.ts +++ b/packages/@lwc/engine-core/src/framework/api.ts @@ -19,335 +19,35 @@ import { isString, isTrue, isUndefined, - KEY__SHADOW_RESOLVER, StringReplace, toString, } from '@lwc/shared'; -import { logError, logWarn } from '../shared/logger'; +import { logError } from '../shared/logger'; import { invokeEventListener } from './invoker'; import { getVMBeingRendered } from './template'; import { EmptyArray, EmptyObject } from './utils'; -import { - appendVM, - getAssociatedVMIfPresent, - getAssociatedVM, - removeVM, - rerenderVM, - runConnectedCallback, - ShadowMode, - SlotSet, - VM, - VMState, - getRenderRoot, - createVM, - hydrateVM, - RenderMode, -} from './vm'; +import { ShadowMode, SlotSet, VM, RenderMode } from './vm'; import { VNode, VNodes, VElement, VText, - Hooks, VCustomElement, VComment, VElementData, } from '../3rdparty/snabbdom/types'; import { LightningElementConstructor } from './base-lightning-element'; import { - createViewModelHook, - fallbackElmHook, - removeElmHook, - createChildrenHook, - updateNodeHook, - insertNodeHook, - removeNodeHook, - createElmHook, - updateElmHook, - createCustomElmHook, - updateCustomElmHook, - patchChildren, - allocateChildrenHook, markAsDynamicChildren, - hydrateChildrenHook, - hydrateElmHook, - LWCDOMMode, + TextHook, + CommentHook, + ElementHook, + CustomElementHook, } from './hooks'; -import { getComponentInternalDef, isComponentConstructor } from './def'; -import { getUpgradableConstructor } from './upgradable-element'; +import { isComponentConstructor } from './def'; -const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; const SymbolIterator: typeof Symbol.iterator = Symbol.iterator; -const TextHook: Hooks = { - create: (vnode) => { - const { owner } = vnode; - const { renderer } = owner; - - const elm = renderer.createText(vnode.text!); - linkNodeToShadow(elm, owner); - vnode.elm = elm; - }, - update: updateNodeHook, - insert: insertNodeHook, - move: insertNodeHook, // same as insert for text nodes - remove: removeNodeHook, - hydrate: (vNode: VNode, node: Node) => { - if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line lwc-internal/no-global-node - if (node.nodeType !== Node.TEXT_NODE) { - logError('Hydration mismatch: incorrect node type received', vNode.owner); - assert.fail('Hydration mismatch: incorrect node type received.'); - } - - if (node.nodeValue !== vNode.text) { - logWarn( - 'Hydration mismatch: text values do not match, will recover from the difference', - vNode.owner - ); - } - } - - // always set the text value to the one from the vnode. - node.nodeValue = vNode.text ?? null; - vNode.elm = node; - }, -}; - -const CommentHook: Hooks = { - create: (vnode) => { - const { owner, text } = vnode; - const { renderer } = owner; - - const elm = renderer.createComment(text); - linkNodeToShadow(elm, owner); - vnode.elm = elm; - }, - update: updateNodeHook, - insert: insertNodeHook, - move: insertNodeHook, // same as insert for text nodes - remove: removeNodeHook, - hydrate: (vNode: VNode, node: Node) => { - if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line lwc-internal/no-global-node - if (node.nodeType !== Node.COMMENT_NODE) { - logError('Hydration mismatch: incorrect node type received', vNode.owner); - assert.fail('Hydration mismatch: incorrect node type received.'); - } - - if (node.nodeValue !== vNode.text) { - logWarn( - 'Hydration mismatch: comment values do not match, will recover from the difference', - vNode.owner - ); - } - } - - // always set the text value to the one from the vnode. - node.nodeValue = vNode.text ?? null; - vNode.elm = node; - }, -}; - -// insert is called after update, which is used somewhere else (via a module) -// to mark the vm as inserted, that means we cannot use update as the main channel -// to rehydrate when dirty, because sometimes the element is not inserted just yet, -// which breaks some invariants. For that reason, we have the following for any -// Custom Element that is inserted via a template. -const ElementHook: Hooks = { - create: (vnode) => { - const { - sel, - owner, - data: { svg }, - } = vnode; - const { renderer } = owner; - - const namespace = isTrue(svg) ? SVG_NAMESPACE : undefined; - const elm = renderer.createElement(sel, namespace); - - linkNodeToShadow(elm, owner); - fallbackElmHook(elm, vnode); - vnode.elm = elm; - - createElmHook(vnode); - }, - update: (oldVnode, vnode) => { - updateElmHook(oldVnode, vnode); - patchChildren(vnode.elm!, oldVnode.children, vnode.children); - }, - insert: (vnode, parentNode, referenceNode) => { - insertNodeHook(vnode, parentNode, referenceNode); - createChildrenHook(vnode); - }, - move: (vnode, parentNode, referenceNode) => { - insertNodeHook(vnode, parentNode, referenceNode); - }, - remove: (vnode, parentNode) => { - removeNodeHook(vnode, parentNode); - removeElmHook(vnode); - }, - hydrate: (vnode, node) => { - const elm = node as Element; - vnode.elm = elm; - - const { context } = vnode.data; - const isDomManual = Boolean( - !isUndefined(context) && - !isUndefined(context.lwc) && - context.lwc.dom === LWCDOMMode.manual - ); - - if (isDomManual) { - // it may be that this element has lwc:inner-html, we need to diff and in case are the same, - // remove the innerHTML from props so it reuses the existing dom elements. - const { props } = vnode.data; - if (!isUndefined(props) && !isUndefined(props.innerHTML)) { - if (elm.innerHTML === props.innerHTML) { - delete props.innerHTML; - } else { - logWarn( - `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: innerHTML values do not match for element, will recover from the difference`, - vnode.owner - ); - } - } - } - - hydrateElmHook(vnode); - - if (!isDomManual) { - hydrateChildrenHook(vnode.elm.childNodes, vnode.children, vnode.owner); - } - }, -}; - -const CustomElementHook: Hooks = { - create: (vnode) => { - const { sel, owner } = vnode; - const { renderer } = owner; - const UpgradableConstructor = getUpgradableConstructor(sel, renderer); - /** - * Note: if the upgradable constructor does not expect, or throw when we new it - * with a callback as the first argument, we could implement a more advanced - * mechanism that only passes that argument if the constructor is known to be - * an upgradable custom element. - */ - const elm = new UpgradableConstructor((elm: HTMLElement) => { - // the custom element from the registry is expecting an upgrade callback - createViewModelHook(elm, vnode); - }); - - linkNodeToShadow(elm, owner); - vnode.elm = elm; - - const vm = getAssociatedVMIfPresent(elm); - if (vm) { - allocateChildrenHook(vnode, vm); - } else if (vnode.ctor !== UpgradableConstructor) { - throw new TypeError(`Incorrect Component Constructor`); - } - createCustomElmHook(vnode); - }, - update: (oldVnode, vnode) => { - updateCustomElmHook(oldVnode, vnode); - const vm = getAssociatedVMIfPresent(vnode.elm); - if (vm) { - // in fallback mode, the allocation will always set children to - // empty and delegate the real allocation to the slot elements - allocateChildrenHook(vnode, vm); - } - // in fallback mode, the children will be always empty, so, nothing - // will happen, but in native, it does allocate the light dom - patchChildren(vnode.elm!, oldVnode.children, vnode.children); - if (vm) { - if (process.env.NODE_ENV !== 'production') { - assert.isTrue( - isArray(vnode.children), - `Invalid vnode for a custom element, it must have children defined.` - ); - } - // this will probably update the shadowRoot, but only if the vm is in a dirty state - // this is important to preserve the top to bottom synchronous rendering phase. - rerenderVM(vm); - } - }, - insert: (vnode, parentNode, referenceNode) => { - insertNodeHook(vnode, parentNode, referenceNode); - const vm = getAssociatedVMIfPresent(vnode.elm); - if (vm) { - if (process.env.NODE_ENV !== 'production') { - assert.isTrue(vm.state === VMState.created, `${vm} cannot be recycled.`); - } - runConnectedCallback(vm); - } - createChildrenHook(vnode); - if (vm) { - appendVM(vm); - } - }, - move: (vnode, parentNode, referenceNode) => { - insertNodeHook(vnode, parentNode, referenceNode); - }, - remove: (vnode, parentNode) => { - removeNodeHook(vnode, parentNode); - const vm = getAssociatedVMIfPresent(vnode.elm); - if (vm) { - // for custom elements we don't have to go recursively because the removeVM routine - // will take care of disconnecting any child VM attached to its shadow as well. - removeVM(vm); - } - }, - hydrate: (vnode, elm) => { - // the element is created, but the vm is not - const { sel, mode, ctor, owner } = vnode; - - const def = getComponentInternalDef(ctor); - createVM(elm, def, { - mode, - owner, - tagName: sel, - renderer: owner.renderer, - }); - - vnode.elm = elm as Element; - - const vm = getAssociatedVM(elm); - allocateChildrenHook(vnode, vm); - - hydrateElmHook(vnode); - - // Insert hook section: - if (process.env.NODE_ENV !== 'production') { - assert.isTrue(vm.state === VMState.created, `${vm} cannot be recycled.`); - } - runConnectedCallback(vm); - - if (vm.renderMode !== RenderMode.Light) { - // VM is not rendering in Light DOM, we can proceed and hydrate the slotted content. - // Note: for Light DOM, this is handled while hydrating the VM - hydrateChildrenHook(vnode.elm.childNodes, vnode.children, vm); - } - - hydrateVM(vm); - }, -}; - -function linkNodeToShadow(elm: Node, owner: VM) { - const { renderer, renderMode, shadowMode } = owner; - - // TODO [#1164]: this should eventually be done by the polyfill directly - if (renderer.isSyntheticShadowDefined) { - if (shadowMode === ShadowMode.Synthetic || renderMode === RenderMode.Light) { - (elm as any)[KEY__SHADOW_RESOLVER] = getRenderRoot(owner)[KEY__SHADOW_RESOLVER]; - } - } -} - -function addVNodeToChildLWC(vnode: VCustomElement) { - ArrayPush.call(getVMBeingRendered()!.velements, vnode); -} - // [h]tml node function h(sel: string, data: VElementData, children: VNodes): VElement { const vmBeingRendered = getVMBeingRendered()!; @@ -521,7 +221,9 @@ function c( owner: vmBeingRendered, mode: 'open', // TODO [#1294]: this should be defined in Ctor }; - addVNodeToChildLWC(vnode); + + ArrayPush.call(vmBeingRendered.velements, vnode); + return vnode; } diff --git a/packages/@lwc/engine-core/src/framework/hooks.ts b/packages/@lwc/engine-core/src/framework/hooks.ts index 83765ac543..77c23e6740 100644 --- a/packages/@lwc/engine-core/src/framework/hooks.ts +++ b/packages/@lwc/engine-core/src/framework/hooks.ts @@ -12,12 +12,37 @@ import { create, isArray, isNull, + isTrue, isUndefined, keys, + KEY__SHADOW_RESOLVER, } from '@lwc/shared'; import { EmptyArray, parseStyleText } from './utils'; -import { createVM, getAssociatedVMIfPresent, VM, ShadowMode, RenderMode } from './vm'; -import { VNode, VCustomElement, VElement, VNodes, Key } from '../3rdparty/snabbdom/types'; +import { + createVM, + getAssociatedVMIfPresent, + VM, + ShadowMode, + RenderMode, + rerenderVM, + VMState, + runConnectedCallback, + appendVM, + removeVM, + getAssociatedVM, + hydrateVM, + getRenderRoot, +} from './vm'; +import { + VNode, + VCustomElement, + VElement, + VNodes, + Key, + Hooks, + VText, + VComment, +} from '../3rdparty/snabbdom/types'; import modEvents from './modules/events'; import modAttrs from './modules/attrs'; import modProps from './modules/props'; @@ -27,8 +52,276 @@ import modStaticClassName from './modules/static-class-attr'; import modStaticStyle from './modules/static-style-attr'; import { patchElementWithRestrictions, unlockDomMutation, lockDomMutation } from './restrictions'; import { getComponentInternalDef } from './def'; -import { logError } from '../shared/logger'; +import { logError, logWarn } from '../shared/logger'; import { markComponentAsDirty } from './component'; +import { getUpgradableConstructor } from './upgradable-element'; + +const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; + +export const TextHook: Hooks = { + create: (vnode) => { + const { owner } = vnode; + const { renderer } = owner; + + const elm = renderer.createText(vnode.text!); + linkNodeToShadow(elm, owner); + vnode.elm = elm; + }, + update: updateNodeHook, + insert: insertNodeHook, + move: insertNodeHook, // same as insert for text nodes + remove: removeNodeHook, + hydrate: (vNode: VNode, node: Node) => { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line lwc-internal/no-global-node + if (node.nodeType !== Node.TEXT_NODE) { + logError('Hydration mismatch: incorrect node type received', vNode.owner); + assert.fail('Hydration mismatch: incorrect node type received.'); + } + + if (node.nodeValue !== vNode.text) { + logWarn( + 'Hydration mismatch: text values do not match, will recover from the difference', + vNode.owner + ); + } + } + + // always set the text value to the one from the vnode. + node.nodeValue = vNode.text ?? null; + vNode.elm = node; + }, +}; + +export const CommentHook: Hooks = { + create: (vnode) => { + const { owner, text } = vnode; + const { renderer } = owner; + + const elm = renderer.createComment(text); + linkNodeToShadow(elm, owner); + vnode.elm = elm; + }, + update: updateNodeHook, + insert: insertNodeHook, + move: insertNodeHook, // same as insert for text nodes + remove: removeNodeHook, + hydrate: (vNode: VNode, node: Node) => { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line lwc-internal/no-global-node + if (node.nodeType !== Node.COMMENT_NODE) { + logError('Hydration mismatch: incorrect node type received', vNode.owner); + assert.fail('Hydration mismatch: incorrect node type received.'); + } + + if (node.nodeValue !== vNode.text) { + logWarn( + 'Hydration mismatch: comment values do not match, will recover from the difference', + vNode.owner + ); + } + } + + // always set the text value to the one from the vnode. + node.nodeValue = vNode.text ?? null; + vNode.elm = node; + }, +}; + +// insert is called after update, which is used somewhere else (via a module) +// to mark the vm as inserted, that means we cannot use update as the main channel +// to rehydrate when dirty, because sometimes the element is not inserted just yet, +// which breaks some invariants. For that reason, we have the following for any +// Custom Element that is inserted via a template. +export const ElementHook: Hooks = { + create: (vnode) => { + const { + sel, + owner, + data: { svg }, + } = vnode; + const { renderer } = owner; + + const namespace = isTrue(svg) ? SVG_NAMESPACE : undefined; + const elm = renderer.createElement(sel, namespace); + + linkNodeToShadow(elm, owner); + fallbackElmHook(elm, vnode); + vnode.elm = elm; + + createElmHook(vnode); + }, + update: (oldVnode, vnode) => { + updateElmHook(oldVnode, vnode); + patchChildren(vnode.elm!, oldVnode.children, vnode.children); + }, + insert: (vnode, parentNode, referenceNode) => { + insertNodeHook(vnode, parentNode, referenceNode); + createChildrenHook(vnode); + }, + move: (vnode, parentNode, referenceNode) => { + insertNodeHook(vnode, parentNode, referenceNode); + }, + remove: (vnode, parentNode) => { + removeNodeHook(vnode, parentNode); + removeElmHook(vnode); + }, + hydrate: (vnode, node) => { + const elm = node as Element; + vnode.elm = elm; + + const { context } = vnode.data; + const isDomManual = Boolean( + !isUndefined(context) && + !isUndefined(context.lwc) && + context.lwc.dom === LWCDOMMode.manual + ); + + if (isDomManual) { + // it may be that this element has lwc:inner-html, we need to diff and in case are the same, + // remove the innerHTML from props so it reuses the existing dom elements. + const { props } = vnode.data; + if (!isUndefined(props) && !isUndefined(props.innerHTML)) { + if (elm.innerHTML === props.innerHTML) { + delete props.innerHTML; + } else { + logWarn( + `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: innerHTML values do not match for element, will recover from the difference`, + vnode.owner + ); + } + } + } + + hydrateElmHook(vnode); + + if (!isDomManual) { + hydrateChildrenHook(vnode.elm.childNodes, vnode.children, vnode.owner); + } + }, +}; + +export const CustomElementHook: Hooks = { + create: (vnode) => { + const { sel, owner } = vnode; + const { renderer } = owner; + const UpgradableConstructor = getUpgradableConstructor(sel, renderer); + /** + * Note: if the upgradable constructor does not expect, or throw when we new it + * with a callback as the first argument, we could implement a more advanced + * mechanism that only passes that argument if the constructor is known to be + * an upgradable custom element. + */ + const elm = new UpgradableConstructor((elm: HTMLElement) => { + // the custom element from the registry is expecting an upgrade callback + createViewModelHook(elm, vnode); + }); + + linkNodeToShadow(elm, owner); + vnode.elm = elm; + + const vm = getAssociatedVMIfPresent(elm); + if (vm) { + allocateChildrenHook(vnode, vm); + } else if (vnode.ctor !== UpgradableConstructor) { + throw new TypeError(`Incorrect Component Constructor`); + } + createCustomElmHook(vnode); + }, + update: (oldVnode, vnode) => { + updateCustomElmHook(oldVnode, vnode); + const vm = getAssociatedVMIfPresent(vnode.elm); + if (vm) { + // in fallback mode, the allocation will always set children to + // empty and delegate the real allocation to the slot elements + allocateChildrenHook(vnode, vm); + } + // in fallback mode, the children will be always empty, so, nothing + // will happen, but in native, it does allocate the light dom + patchChildren(vnode.elm!, oldVnode.children, vnode.children); + if (vm) { + if (process.env.NODE_ENV !== 'production') { + assert.isTrue( + isArray(vnode.children), + `Invalid vnode for a custom element, it must have children defined.` + ); + } + // this will probably update the shadowRoot, but only if the vm is in a dirty state + // this is important to preserve the top to bottom synchronous rendering phase. + rerenderVM(vm); + } + }, + insert: (vnode, parentNode, referenceNode) => { + insertNodeHook(vnode, parentNode, referenceNode); + const vm = getAssociatedVMIfPresent(vnode.elm); + if (vm) { + if (process.env.NODE_ENV !== 'production') { + assert.isTrue(vm.state === VMState.created, `${vm} cannot be recycled.`); + } + runConnectedCallback(vm); + } + createChildrenHook(vnode); + if (vm) { + appendVM(vm); + } + }, + move: (vnode, parentNode, referenceNode) => { + insertNodeHook(vnode, parentNode, referenceNode); + }, + remove: (vnode, parentNode) => { + removeNodeHook(vnode, parentNode); + const vm = getAssociatedVMIfPresent(vnode.elm); + if (vm) { + // for custom elements we don't have to go recursively because the removeVM routine + // will take care of disconnecting any child VM attached to its shadow as well. + removeVM(vm); + } + }, + hydrate: (vnode, elm) => { + // the element is created, but the vm is not + const { sel, mode, ctor, owner } = vnode; + + const def = getComponentInternalDef(ctor); + createVM(elm, def, { + mode, + owner, + tagName: sel, + renderer: owner.renderer, + }); + + vnode.elm = elm as Element; + + const vm = getAssociatedVM(elm); + allocateChildrenHook(vnode, vm); + + hydrateElmHook(vnode); + + // Insert hook section: + if (process.env.NODE_ENV !== 'production') { + assert.isTrue(vm.state === VMState.created, `${vm} cannot be recycled.`); + } + runConnectedCallback(vm); + + if (vm.renderMode !== RenderMode.Light) { + // VM is not rendering in Light DOM, we can proceed and hydrate the slotted content. + // Note: for Light DOM, this is handled while hydrating the VM + hydrateChildrenHook(vnode.elm.childNodes, vnode.children, vm); + } + + hydrateVM(vm); + }, +}; + +function linkNodeToShadow(elm: Node, owner: VM) { + const { renderer, renderMode, shadowMode } = owner; + + // TODO [#1164]: this should eventually be done by the polyfill directly + if (renderer.isSyntheticShadowDefined) { + if (shadowMode === ShadowMode.Synthetic || renderMode === RenderMode.Light) { + (elm as any)[KEY__SHADOW_RESOLVER] = getRenderRoot(owner)[KEY__SHADOW_RESOLVER]; + } + } +} function observeElementChildNodes(elm: Element) { (elm as any).$domManual$ = true; @@ -47,7 +340,7 @@ function setScopeTokenClassIfNecessary(elm: Element, owner: VM) { } } -export function updateNodeHook(oldVnode: VNode, vnode: VNode) { +function updateNodeHook(oldVnode: VNode, vnode: VNode) { const { elm, text, @@ -65,7 +358,7 @@ export function updateNodeHook(oldVnode: VNode, vnode: VNode) { } } -export function insertNodeHook(vnode: VNode, parentNode: Node, referenceNode: Node | null) { +function insertNodeHook(vnode: VNode, parentNode: Node, referenceNode: Node | null) { const { renderer } = vnode.owner; if (process.env.NODE_ENV !== 'production') { @@ -77,7 +370,7 @@ export function insertNodeHook(vnode: VNode, parentNode: Node, referenceNode: No } } -export function removeNodeHook(vnode: VNode, parentNode: Node) { +function removeNodeHook(vnode: VNode, parentNode: Node) { const { renderer } = vnode.owner; if (process.env.NODE_ENV !== 'production') { @@ -89,7 +382,7 @@ export function removeNodeHook(vnode: VNode, parentNode: Node) { } } -export function createElmHook(vnode: VElement) { +function createElmHook(vnode: VElement) { modEvents.create(vnode); // Attrs need to be applied to element before props // IE11 will wipe out value on radio inputs if value @@ -102,11 +395,11 @@ export function createElmHook(vnode: VElement) { modComputedStyle.create(vnode); } -export const enum LWCDOMMode { +const enum LWCDOMMode { manual = 'manual', } -export function hydrateElmHook(vnode: VElement) { +function hydrateElmHook(vnode: VElement) { modEvents.create(vnode); // Attrs are already on the element. // modAttrs.create(vnode); @@ -118,7 +411,7 @@ export function hydrateElmHook(vnode: VElement) { // modComputedStyle.create(vnode); } -export function fallbackElmHook(elm: Element, vnode: VElement) { +function fallbackElmHook(elm: Element, vnode: VElement) { const { owner } = vnode; setScopeTokenClassIfNecessary(elm, owner); if (owner.shadowMode === ShadowMode.Synthetic) { @@ -151,7 +444,7 @@ export function fallbackElmHook(elm: Element, vnode: VElement) { } } -export function updateElmHook(oldVnode: VElement, vnode: VElement) { +function updateElmHook(oldVnode: VElement, vnode: VElement) { // Attrs need to be applied to element before props // IE11 will wipe out value on radio inputs if value // is set before type=radio. @@ -161,7 +454,7 @@ export function updateElmHook(oldVnode: VElement, vnode: VElement) { modComputedStyle.update(oldVnode, vnode); } -export function allocateChildrenHook(vnode: VCustomElement, vm: VM) { +function allocateChildrenHook(vnode: VCustomElement, vm: VM) { // A component with slots will re-render because: // 1- There is a change of the internal state. // 2- There is a change on the external api (ex: slots) @@ -187,7 +480,7 @@ export function allocateChildrenHook(vnode: VCustomElement, vm: VM) { } } -export function createViewModelHook(elm: HTMLElement, vnode: VCustomElement) { +function createViewModelHook(elm: HTMLElement, vnode: VCustomElement) { if (!isUndefined(getAssociatedVMIfPresent(elm))) { // There is a possibility that a custom element is registered under tagName, // in which case, the initialization is already carry on, and there is nothing else @@ -217,7 +510,7 @@ export function createViewModelHook(elm: HTMLElement, vnode: VCustomElement) { } } -export function createCustomElmHook(vnode: VCustomElement) { +function createCustomElmHook(vnode: VCustomElement) { modEvents.create(vnode); // Attrs need to be applied to element before props // IE11 will wipe out value on radio inputs if value @@ -230,7 +523,7 @@ export function createCustomElmHook(vnode: VCustomElement) { modComputedStyle.create(vnode); } -export function createChildrenHook(vnode: VElement) { +function createChildrenHook(vnode: VElement) { const { elm, children } = vnode; for (let j = 0; j < children.length; ++j) { const ch = children[j]; @@ -418,7 +711,7 @@ export function hydrateChildrenHook(elmChildren: NodeListOf, children } } -export function updateCustomElmHook(oldVnode: VCustomElement, vnode: VCustomElement) { +function updateCustomElmHook(oldVnode: VCustomElement, vnode: VCustomElement) { // Attrs need to be applied to element before props // IE11 will wipe out value on radio inputs if value // is set before type=radio. @@ -428,7 +721,7 @@ export function updateCustomElmHook(oldVnode: VCustomElement, vnode: VCustomElem modComputedStyle.update(oldVnode, vnode); } -export function removeElmHook(vnode: VElement) { +function removeElmHook(vnode: VElement) { // this method only needs to search on child vnodes from template // to trigger the remove hook just in case some of those children // are custom elements. From 6f01f12b20eb2e2de67e5b39e4c379b5a1394f28 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Wed, 15 Dec 2021 06:51:56 +0100 Subject: [PATCH 04/17] refactor: add type to each VNode --- .../src/3rdparty/snabbdom/types.ts | 30 ++++++++++++++----- .../@lwc/engine-core/src/framework/api.ts | 5 ++++ .../@lwc/engine-core/src/framework/hooks.ts | 6 ++-- .../src/framework/modules/attrs.ts | 8 ++--- .../framework/modules/computed-class-attr.ts | 8 ++--- .../framework/modules/computed-style-attr.ts | 8 ++--- .../src/framework/modules/events.ts | 12 ++++---- .../src/framework/modules/props.ts | 8 ++--- .../framework/modules/static-class-attr.ts | 4 +-- .../framework/modules/static-style-attr.ts | 4 +-- 10 files changed, 57 insertions(+), 36 deletions(-) diff --git a/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts b/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts index 242d47d259..edc67fd731 100644 --- a/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts +++ b/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts @@ -17,21 +17,30 @@ import { VM } from '../../framework/vm'; export type Key = string | number; +export const enum VNodeType { + Text, + Comment, + Element, + CustomElement, +} + +export type VNode = VText | VComment | VElement | VCustomElement; +export type VParentElement = VElement | VCustomElement; export type VNodes = Array; -export interface VNode { +export interface VBaseNode { sel: string | undefined; data: VNodeData; children: VNodes | undefined; elm: Node | undefined; - parentElm?: Element; text: string | undefined; key: Key | undefined; hook: Hooks; owner: VM; + type: VNodeType; } -export interface VElement extends VNode { +export interface VBaseElement extends VBaseNode { sel: string; data: VElementData; children: VNodes; @@ -40,26 +49,31 @@ export interface VElement extends VNode { key: Key; } -export interface VCustomElement extends VElement { +export interface VElement extends VBaseElement { + type: VNodeType.Element; +} + +export interface VCustomElement extends VBaseElement { mode: 'closed' | 'open'; ctor: any; // copy of the last allocated children. aChildren?: VNodes; + type: VNodeType.CustomElement; } -export interface VText extends VNode { +export interface VText extends VBaseNode { sel: undefined; children: undefined; elm: Node | undefined; text: string; - key: undefined; + type: VNodeType.Text; } -export interface VComment extends VNode { +export interface VComment extends VBaseNode { sel: undefined; children: undefined; text: string; - key: undefined; + type: VNodeType.Comment; } export interface VNodeData { diff --git a/packages/@lwc/engine-core/src/framework/api.ts b/packages/@lwc/engine-core/src/framework/api.ts index 6e10e29fd4..09030a0333 100644 --- a/packages/@lwc/engine-core/src/framework/api.ts +++ b/packages/@lwc/engine-core/src/framework/api.ts @@ -35,6 +35,7 @@ import { VCustomElement, VComment, VElementData, + VNodeType, } from '../3rdparty/snabbdom/types'; import { LightningElementConstructor } from './base-lightning-element'; import { @@ -94,6 +95,7 @@ function h(sel: string, data: VElementData, children: VNodes): VElement { const { key } = data; return { + type: VNodeType.Element, sel, data, children, @@ -209,6 +211,7 @@ function c( const { key } = data; let text, elm; const vnode: VCustomElement = { + type: VNodeType.CustomElement, sel, data, children, @@ -343,6 +346,7 @@ function t(text: string): VText { const data = EmptyObject; let sel, children, key, elm; return { + type: VNodeType.Text, sel, data, children, @@ -360,6 +364,7 @@ function co(text: string): VComment { const data = EmptyObject; let sel, children, key, elm; return { + type: VNodeType.Comment, sel, data, children, diff --git a/packages/@lwc/engine-core/src/framework/hooks.ts b/packages/@lwc/engine-core/src/framework/hooks.ts index 77c23e6740..6e36919d4d 100644 --- a/packages/@lwc/engine-core/src/framework/hooks.ts +++ b/packages/@lwc/engine-core/src/framework/hooks.ts @@ -42,6 +42,8 @@ import { Hooks, VText, VComment, + VParentElement, + VBaseElement, } from '../3rdparty/snabbdom/types'; import modEvents from './modules/events'; import modAttrs from './modules/attrs'; @@ -399,7 +401,7 @@ const enum LWCDOMMode { manual = 'manual', } -function hydrateElmHook(vnode: VElement) { +function hydrateElmHook(vnode: VBaseElement) { modEvents.create(vnode); // Attrs are already on the element. // modAttrs.create(vnode); @@ -523,7 +525,7 @@ function createCustomElmHook(vnode: VCustomElement) { modComputedStyle.create(vnode); } -function createChildrenHook(vnode: VElement) { +function createChildrenHook(vnode: VParentElement) { const { elm, children } = vnode; for (let j = 0; j < children.length; ++j) { const ch = children[j]; diff --git a/packages/@lwc/engine-core/src/framework/modules/attrs.ts b/packages/@lwc/engine-core/src/framework/modules/attrs.ts index 2699bdc2be..0e564bbf5d 100644 --- a/packages/@lwc/engine-core/src/framework/modules/attrs.ts +++ b/packages/@lwc/engine-core/src/framework/modules/attrs.ts @@ -7,13 +7,13 @@ import { assert, isNull, isUndefined, keys, StringCharCodeAt } from '@lwc/shared'; import { unlockAttribute, lockAttribute } from '../attributes'; import { EmptyObject } from '../utils'; -import { VElement } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../../3rdparty/snabbdom/types'; const xlinkNS = 'http://www.w3.org/1999/xlink'; const xmlNS = 'http://www.w3.org/XML/1998/namespace'; const ColonCharCode = 58; -function updateAttrs(oldVnode: VElement, vnode: VElement) { +function updateAttrs(oldVnode: VBaseElement, vnode: VBaseElement) { const { data: { attrs }, owner: { renderer }, @@ -66,9 +66,9 @@ function updateAttrs(oldVnode: VElement, vnode: VElement) { } } -const emptyVNode = { data: {} } as VElement; +const emptyVNode = { data: {} } as VBaseElement; export default { - create: (vnode: VElement) => updateAttrs(emptyVNode, vnode), + create: (vnode: VBaseElement) => updateAttrs(emptyVNode, vnode), update: updateAttrs, }; diff --git a/packages/@lwc/engine-core/src/framework/modules/computed-class-attr.ts b/packages/@lwc/engine-core/src/framework/modules/computed-class-attr.ts index 615a029943..c44cc9c094 100644 --- a/packages/@lwc/engine-core/src/framework/modules/computed-class-attr.ts +++ b/packages/@lwc/engine-core/src/framework/modules/computed-class-attr.ts @@ -6,7 +6,7 @@ */ import { create, freeze, isString, isUndefined, StringCharCodeAt, StringSlice } from '@lwc/shared'; import { EmptyObject, SPACE_CHAR } from '../utils'; -import { VElement } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../../3rdparty/snabbdom/types'; const classNameToClassMap = create(null); @@ -46,7 +46,7 @@ function getMapFromClassName(className: string | undefined): Record updateClassAttribute(emptyVNode, vnode), + create: (vnode: VBaseElement) => updateClassAttribute(emptyVNode, vnode), update: updateClassAttribute, }; diff --git a/packages/@lwc/engine-core/src/framework/modules/computed-style-attr.ts b/packages/@lwc/engine-core/src/framework/modules/computed-style-attr.ts index 35196b8bb9..e6c5989c28 100644 --- a/packages/@lwc/engine-core/src/framework/modules/computed-style-attr.ts +++ b/packages/@lwc/engine-core/src/framework/modules/computed-style-attr.ts @@ -5,10 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { isString } from '@lwc/shared'; -import { VNode } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../../3rdparty/snabbdom/types'; // The style property is a string when defined via an expression in the template. -function updateStyleAttribute(oldVnode: VNode, vnode: VNode) { +function updateStyleAttribute(oldVnode: VBaseElement, vnode: VBaseElement) { const { elm, data: { style: newStyle }, @@ -26,9 +26,9 @@ function updateStyleAttribute(oldVnode: VNode, vnode: VNode) { } } -const emptyVNode = { data: {} } as VNode; +const emptyVNode = { data: {} } as VBaseElement; export default { - create: (vnode: VNode) => updateStyleAttribute(emptyVNode, vnode), + create: (vnode: VBaseElement) => updateStyleAttribute(emptyVNode, vnode), update: updateStyleAttribute, }; diff --git a/packages/@lwc/engine-core/src/framework/modules/events.ts b/packages/@lwc/engine-core/src/framework/modules/events.ts index 89564f7042..3762364a36 100644 --- a/packages/@lwc/engine-core/src/framework/modules/events.ts +++ b/packages/@lwc/engine-core/src/framework/modules/events.ts @@ -5,9 +5,9 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { isUndefined } from '@lwc/shared'; -import { VNode } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../../3rdparty/snabbdom/types'; -function handleEvent(event: Event, vnode: VNode) { +function handleEvent(event: Event, vnode: VBaseElement) { const { type } = event; const { data: { on }, @@ -20,16 +20,16 @@ function handleEvent(event: Event, vnode: VNode) { } interface VNodeEventListener extends EventListener { - vnode?: VNode; + vnode?: VBaseElement; } -interface InteractiveVNode extends VNode { +interface InteractiveVNode extends VBaseElement { listener: VNodeEventListener | undefined; } function createListener(): EventListener { return function handler(event: Event) { - handleEvent(event, (handler as VNodeEventListener).vnode as VNode); + handleEvent(event, (handler as VNodeEventListener).vnode as VBaseElement); }; } @@ -42,7 +42,7 @@ function updateAllEventListeners(oldVnode: InteractiveVNode, vnode: InteractiveV } } -function createAllEventListeners(vnode: VNode) { +function createAllEventListeners(vnode: VBaseElement) { const { elm, data: { on }, diff --git a/packages/@lwc/engine-core/src/framework/modules/props.ts b/packages/@lwc/engine-core/src/framework/modules/props.ts index 228782e3b6..cb797609bd 100644 --- a/packages/@lwc/engine-core/src/framework/modules/props.ts +++ b/packages/@lwc/engine-core/src/framework/modules/props.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { assert, isUndefined, keys } from '@lwc/shared'; -import { VElement } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../../3rdparty/snabbdom/types'; function isLiveBindingProp(sel: string, key: string): boolean { // For properties with live bindings, we read values from the DOM element @@ -13,7 +13,7 @@ function isLiveBindingProp(sel: string, key: string): boolean { return sel === 'input' && (key === 'value' || key === 'checked'); } -function update(oldVnode: VElement, vnode: VElement) { +function update(oldVnode: VBaseElement, vnode: VBaseElement) { const props = vnode.data.props; if (isUndefined(props)) { @@ -54,9 +54,9 @@ function update(oldVnode: VElement, vnode: VElement) { } } -const emptyVNode = { data: {} } as VElement; +const emptyVNode = { data: {} } as VBaseElement; export default { - create: (vnode: VElement) => update(emptyVNode, vnode), + create: (vnode: VBaseElement) => update(emptyVNode, vnode), update, }; diff --git a/packages/@lwc/engine-core/src/framework/modules/static-class-attr.ts b/packages/@lwc/engine-core/src/framework/modules/static-class-attr.ts index 2faf5840fc..6d513875f7 100644 --- a/packages/@lwc/engine-core/src/framework/modules/static-class-attr.ts +++ b/packages/@lwc/engine-core/src/framework/modules/static-class-attr.ts @@ -5,12 +5,12 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { isUndefined } from '@lwc/shared'; -import { VNode } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../../3rdparty/snabbdom/types'; // The HTML class property becomes the vnode.data.classMap object when defined as a string in the template. // The compiler takes care of transforming the inline classnames into an object. It's faster to set the // different classnames properties individually instead of via a string. -function createClassAttribute(vnode: VNode) { +function createClassAttribute(vnode: VBaseElement) { const { elm, data: { classMap }, diff --git a/packages/@lwc/engine-core/src/framework/modules/static-style-attr.ts b/packages/@lwc/engine-core/src/framework/modules/static-style-attr.ts index 0094738db8..b0e4739e49 100644 --- a/packages/@lwc/engine-core/src/framework/modules/static-style-attr.ts +++ b/packages/@lwc/engine-core/src/framework/modules/static-style-attr.ts @@ -5,12 +5,12 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { isUndefined } from '@lwc/shared'; -import { VNode } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../../3rdparty/snabbdom/types'; // The HTML style property becomes the vnode.data.styleDecls object when defined as a string in the template. // The compiler takes care of transforming the inline style into an object. It's faster to set the // different style properties individually instead of via a string. -function createStyleAttribute(vnode: VNode) { +function createStyleAttribute(vnode: VBaseElement) { const { elm, data: { styleDecls }, From 273b43f71f58fbf5d1115150dd86fdd2e7b2c7e7 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Wed, 15 Dec 2021 07:36:27 +0100 Subject: [PATCH 05/17] chore: rework diffing modules --- .../@lwc/engine-core/src/framework/hooks.ts | 68 ++++++++++--------- .../src/framework/modules/attrs.ts | 15 ++-- .../framework/modules/computed-class-attr.ts | 24 +++---- .../framework/modules/computed-style-attr.ts | 18 ++--- .../src/framework/modules/events.ts | 16 +---- .../src/framework/modules/props.ts | 15 ++-- .../framework/modules/static-class-attr.ts | 6 +- .../framework/modules/static-style-attr.ts | 6 +- 8 files changed, 65 insertions(+), 103 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/hooks.ts b/packages/@lwc/engine-core/src/framework/hooks.ts index 6e36919d4d..68d495570e 100644 --- a/packages/@lwc/engine-core/src/framework/hooks.ts +++ b/packages/@lwc/engine-core/src/framework/hooks.ts @@ -45,13 +45,15 @@ import { VParentElement, VBaseElement, } from '../3rdparty/snabbdom/types'; -import modEvents from './modules/events'; -import modAttrs from './modules/attrs'; -import modProps from './modules/props'; -import modComputedClassName from './modules/computed-class-attr'; -import modComputedStyle from './modules/computed-style-attr'; -import modStaticClassName from './modules/static-class-attr'; -import modStaticStyle from './modules/static-style-attr'; + +import { applyEventListeners } from './modules/events'; +import { patchAttributes } from './modules/attrs'; +import { patchProps } from './modules/props'; +import { patchClassAttribute } from './modules/computed-class-attr'; +import { patchStyleAttribute } from './modules/computed-style-attr'; +import { applyStaticClassAttribute } from './modules/static-class-attr'; +import { applyStaticStyleAttribute } from './modules/static-style-attr'; + import { patchElementWithRestrictions, unlockDomMutation, lockDomMutation } from './restrictions'; import { getComponentInternalDef } from './def'; import { logError, logWarn } from '../shared/logger'; @@ -385,16 +387,16 @@ function removeNodeHook(vnode: VNode, parentNode: Node) { } function createElmHook(vnode: VElement) { - modEvents.create(vnode); + applyEventListeners(vnode); // Attrs need to be applied to element before props // IE11 will wipe out value on radio inputs if value // is set before type=radio. - modAttrs.create(vnode); - modProps.create(vnode); - modStaticClassName.create(vnode); - modStaticStyle.create(vnode); - modComputedClassName.create(vnode); - modComputedStyle.create(vnode); + patchAttributes(null, vnode); + patchProps(null, vnode); + applyStaticClassAttribute(vnode); + applyStaticStyleAttribute(vnode); + patchClassAttribute(null, vnode); + patchStyleAttribute(null, vnode); } const enum LWCDOMMode { @@ -402,13 +404,13 @@ const enum LWCDOMMode { } function hydrateElmHook(vnode: VBaseElement) { - modEvents.create(vnode); + applyEventListeners(vnode); // Attrs are already on the element. // modAttrs.create(vnode); - modProps.create(vnode); + patchProps(null, vnode); // Already set. - // modStaticClassName.create(vnode); - // modStaticStyle.create(vnode); + // applyStaticClassAttribute(vnode); + // applyStaticStyleAttribute(vnode); // modComputedClassName.create(vnode); // modComputedStyle.create(vnode); } @@ -450,10 +452,10 @@ function updateElmHook(oldVnode: VElement, vnode: VElement) { // Attrs need to be applied to element before props // IE11 will wipe out value on radio inputs if value // is set before type=radio. - modAttrs.update(oldVnode, vnode); - modProps.update(oldVnode, vnode); - modComputedClassName.update(oldVnode, vnode); - modComputedStyle.update(oldVnode, vnode); + patchAttributes(oldVnode, vnode); + patchProps(oldVnode, vnode); + patchClassAttribute(oldVnode, vnode); + patchStyleAttribute(oldVnode, vnode); } function allocateChildrenHook(vnode: VCustomElement, vm: VM) { @@ -513,16 +515,16 @@ function createViewModelHook(elm: HTMLElement, vnode: VCustomElement) { } function createCustomElmHook(vnode: VCustomElement) { - modEvents.create(vnode); + applyEventListeners(vnode); // Attrs need to be applied to element before props // IE11 will wipe out value on radio inputs if value // is set before type=radio. - modAttrs.create(vnode); - modProps.create(vnode); - modStaticClassName.create(vnode); - modStaticStyle.create(vnode); - modComputedClassName.create(vnode); - modComputedStyle.create(vnode); + patchAttributes(null, vnode); + patchProps(null, vnode); + applyStaticClassAttribute(vnode); + applyStaticStyleAttribute(vnode); + patchClassAttribute(null, vnode); + patchStyleAttribute(null, vnode); } function createChildrenHook(vnode: VParentElement) { @@ -717,10 +719,10 @@ function updateCustomElmHook(oldVnode: VCustomElement, vnode: VCustomElement) { // Attrs need to be applied to element before props // IE11 will wipe out value on radio inputs if value // is set before type=radio. - modAttrs.update(oldVnode, vnode); - modProps.update(oldVnode, vnode); - modComputedClassName.update(oldVnode, vnode); - modComputedStyle.update(oldVnode, vnode); + patchAttributes(oldVnode, vnode); + patchProps(oldVnode, vnode); + patchClassAttribute(oldVnode, vnode); + patchStyleAttribute(oldVnode, vnode); } function removeElmHook(vnode: VElement) { diff --git a/packages/@lwc/engine-core/src/framework/modules/attrs.ts b/packages/@lwc/engine-core/src/framework/modules/attrs.ts index 0e564bbf5d..2f2ab7a613 100644 --- a/packages/@lwc/engine-core/src/framework/modules/attrs.ts +++ b/packages/@lwc/engine-core/src/framework/modules/attrs.ts @@ -13,7 +13,7 @@ const xlinkNS = 'http://www.w3.org/1999/xlink'; const xmlNS = 'http://www.w3.org/XML/1998/namespace'; const ColonCharCode = 58; -function updateAttrs(oldVnode: VBaseElement, vnode: VBaseElement) { +export function patchAttributes(oldVnode: VBaseElement | null, vnode: VBaseElement) { const { data: { attrs }, owner: { renderer }, @@ -22,9 +22,9 @@ function updateAttrs(oldVnode: VBaseElement, vnode: VBaseElement) { if (isUndefined(attrs)) { return; } - let { - data: { attrs: oldAttrs }, - } = oldVnode; + + let oldAttrs = isNull(oldVnode) ? undefined : oldVnode.data.attrs; + if (oldAttrs === attrs) { return; } @@ -65,10 +65,3 @@ function updateAttrs(oldVnode: VBaseElement, vnode: VBaseElement) { } } } - -const emptyVNode = { data: {} } as VBaseElement; - -export default { - create: (vnode: VBaseElement) => updateAttrs(emptyVNode, vnode), - update: updateAttrs, -}; diff --git a/packages/@lwc/engine-core/src/framework/modules/computed-class-attr.ts b/packages/@lwc/engine-core/src/framework/modules/computed-class-attr.ts index c44cc9c094..570b1dd09d 100644 --- a/packages/@lwc/engine-core/src/framework/modules/computed-class-attr.ts +++ b/packages/@lwc/engine-core/src/framework/modules/computed-class-attr.ts @@ -4,7 +4,15 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { create, freeze, isString, isUndefined, StringCharCodeAt, StringSlice } from '@lwc/shared'; +import { + create, + freeze, + isNull, + isString, + isUndefined, + StringCharCodeAt, + StringSlice, +} from '@lwc/shared'; import { EmptyObject, SPACE_CHAR } from '../utils'; import { VBaseElement } from '../../3rdparty/snabbdom/types'; @@ -46,15 +54,14 @@ function getMapFromClassName(className: string | undefined): Record updateClassAttribute(emptyVNode, vnode), - update: updateClassAttribute, -}; diff --git a/packages/@lwc/engine-core/src/framework/modules/computed-style-attr.ts b/packages/@lwc/engine-core/src/framework/modules/computed-style-attr.ts index e6c5989c28..6d68470a55 100644 --- a/packages/@lwc/engine-core/src/framework/modules/computed-style-attr.ts +++ b/packages/@lwc/engine-core/src/framework/modules/computed-style-attr.ts @@ -4,31 +4,27 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { isString } from '@lwc/shared'; +import { isNull, isString } from '@lwc/shared'; import { VBaseElement } from '../../3rdparty/snabbdom/types'; // The style property is a string when defined via an expression in the template. -function updateStyleAttribute(oldVnode: VBaseElement, vnode: VBaseElement) { +export function patchStyleAttribute(oldVnode: VBaseElement | null, vnode: VBaseElement) { const { elm, data: { style: newStyle }, owner: { renderer }, } = vnode; - const { setAttribute, removeAttribute } = renderer; - if (oldVnode.data.style === newStyle) { + + const oldStyle = isNull(oldVnode) ? undefined : oldVnode.data.style; + if (oldStyle === newStyle) { return; } + const { setAttribute, removeAttribute } = renderer; + if (!isString(newStyle) || newStyle === '') { removeAttribute(elm, 'style'); } else { setAttribute(elm, 'style', newStyle); } } - -const emptyVNode = { data: {} } as VBaseElement; - -export default { - create: (vnode: VBaseElement) => updateStyleAttribute(emptyVNode, vnode), - update: updateStyleAttribute, -}; diff --git a/packages/@lwc/engine-core/src/framework/modules/events.ts b/packages/@lwc/engine-core/src/framework/modules/events.ts index 3762364a36..ede8082034 100644 --- a/packages/@lwc/engine-core/src/framework/modules/events.ts +++ b/packages/@lwc/engine-core/src/framework/modules/events.ts @@ -33,16 +33,7 @@ function createListener(): EventListener { }; } -function updateAllEventListeners(oldVnode: InteractiveVNode, vnode: InteractiveVNode) { - if (isUndefined(oldVnode.listener)) { - createAllEventListeners(vnode); - } else { - vnode.listener = oldVnode.listener; - vnode.listener.vnode = vnode; - } -} - -function createAllEventListeners(vnode: VBaseElement) { +export function applyEventListeners(vnode: VBaseElement) { const { elm, data: { on }, @@ -61,8 +52,3 @@ function createAllEventListeners(vnode: VBaseElement) { renderer.addEventListener(elm, name, listener); } } - -export default { - update: updateAllEventListeners, - create: createAllEventListeners, -}; diff --git a/packages/@lwc/engine-core/src/framework/modules/props.ts b/packages/@lwc/engine-core/src/framework/modules/props.ts index cb797609bd..c35418a90a 100644 --- a/packages/@lwc/engine-core/src/framework/modules/props.ts +++ b/packages/@lwc/engine-core/src/framework/modules/props.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { assert, isUndefined, keys } from '@lwc/shared'; +import { assert, isNull, isUndefined, keys } from '@lwc/shared'; import { VBaseElement } from '../../3rdparty/snabbdom/types'; function isLiveBindingProp(sel: string, key: string): boolean { @@ -13,13 +13,13 @@ function isLiveBindingProp(sel: string, key: string): boolean { return sel === 'input' && (key === 'value' || key === 'checked'); } -function update(oldVnode: VBaseElement, vnode: VBaseElement) { +export function patchProps(oldVnode: VBaseElement | null, vnode: VBaseElement) { const props = vnode.data.props; - if (isUndefined(props)) { return; } - const oldProps = oldVnode.data.props; + + const oldProps = isNull(oldVnode) ? undefined : oldVnode.data.props; if (oldProps === props) { return; } @@ -53,10 +53,3 @@ function update(oldVnode: VBaseElement, vnode: VBaseElement) { } } } - -const emptyVNode = { data: {} } as VBaseElement; - -export default { - create: (vnode: VBaseElement) => update(emptyVNode, vnode), - update, -}; diff --git a/packages/@lwc/engine-core/src/framework/modules/static-class-attr.ts b/packages/@lwc/engine-core/src/framework/modules/static-class-attr.ts index 6d513875f7..b1b7cf972e 100644 --- a/packages/@lwc/engine-core/src/framework/modules/static-class-attr.ts +++ b/packages/@lwc/engine-core/src/framework/modules/static-class-attr.ts @@ -10,7 +10,7 @@ import { VBaseElement } from '../../3rdparty/snabbdom/types'; // The HTML class property becomes the vnode.data.classMap object when defined as a string in the template. // The compiler takes care of transforming the inline classnames into an object. It's faster to set the // different classnames properties individually instead of via a string. -function createClassAttribute(vnode: VBaseElement) { +export function applyStaticClassAttribute(vnode: VBaseElement) { const { elm, data: { classMap }, @@ -26,7 +26,3 @@ function createClassAttribute(vnode: VBaseElement) { classList.add(name); } } - -export default { - create: createClassAttribute, -}; diff --git a/packages/@lwc/engine-core/src/framework/modules/static-style-attr.ts b/packages/@lwc/engine-core/src/framework/modules/static-style-attr.ts index b0e4739e49..b97660454b 100644 --- a/packages/@lwc/engine-core/src/framework/modules/static-style-attr.ts +++ b/packages/@lwc/engine-core/src/framework/modules/static-style-attr.ts @@ -10,7 +10,7 @@ import { VBaseElement } from '../../3rdparty/snabbdom/types'; // The HTML style property becomes the vnode.data.styleDecls object when defined as a string in the template. // The compiler takes care of transforming the inline style into an object. It's faster to set the // different style properties individually instead of via a string. -function createStyleAttribute(vnode: VBaseElement) { +export function applyStaticStyleAttribute(vnode: VBaseElement) { const { elm, data: { styleDecls }, @@ -26,7 +26,3 @@ function createStyleAttribute(vnode: VBaseElement) { renderer.setCSSStyleProperty(elm, prop, value, important); } } - -export default { - create: createStyleAttribute, -}; From 0f8b3ae4211a5f2d833a745573efabbc86ac68fc Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Wed, 15 Dec 2021 07:49:17 +0100 Subject: [PATCH 06/17] chore: remove duplicate patching code --- .../@lwc/engine-core/src/framework/hooks.ts | 79 ++++++------------- .../src/framework/modules/events.ts | 37 ++------- 2 files changed, 29 insertions(+), 87 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/hooks.ts b/packages/@lwc/engine-core/src/framework/hooks.ts index 68d495570e..69a27c4de2 100644 --- a/packages/@lwc/engine-core/src/framework/hooks.ts +++ b/packages/@lwc/engine-core/src/framework/hooks.ts @@ -60,6 +60,14 @@ import { logError, logWarn } from '../shared/logger'; import { markComponentAsDirty } from './component'; import { getUpgradableConstructor } from './upgradable-element'; +const enum LWCDOMMode { + manual = 'manual', +} + +interface KeyToIndexMap { + [key: string]: number; +} + const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; export const TextHook: Hooks = { @@ -153,10 +161,10 @@ export const ElementHook: Hooks = { fallbackElmHook(elm, vnode); vnode.elm = elm; - createElmHook(vnode); + patchElementAttrsAndProps(null, vnode); }, update: (oldVnode, vnode) => { - updateElmHook(oldVnode, vnode); + patchElementAttrsAndProps(oldVnode, vnode); patchChildren(vnode.elm!, oldVnode.children, vnode.children); }, insert: (vnode, parentNode, referenceNode) => { @@ -230,10 +238,10 @@ export const CustomElementHook: Hooks = { } else if (vnode.ctor !== UpgradableConstructor) { throw new TypeError(`Incorrect Component Constructor`); } - createCustomElmHook(vnode); + patchElementAttrsAndProps(null, vnode); }, update: (oldVnode, vnode) => { - updateCustomElmHook(oldVnode, vnode); + patchElementAttrsAndProps(oldVnode, vnode); const vm = getAssociatedVMIfPresent(vnode.elm); if (vm) { // in fallback mode, the allocation will always set children to @@ -386,21 +394,19 @@ function removeNodeHook(vnode: VNode, parentNode: Node) { } } -function createElmHook(vnode: VElement) { - applyEventListeners(vnode); - // Attrs need to be applied to element before props - // IE11 will wipe out value on radio inputs if value - // is set before type=radio. - patchAttributes(null, vnode); - patchProps(null, vnode); - applyStaticClassAttribute(vnode); - applyStaticStyleAttribute(vnode); - patchClassAttribute(null, vnode); - patchStyleAttribute(null, vnode); -} +function patchElementAttrsAndProps(oldVnode: VBaseElement | null, vnode: VBaseElement) { + if (isNull(oldVnode)) { + applyEventListeners(vnode); + applyStaticClassAttribute(vnode); + applyStaticStyleAttribute(vnode); + } -const enum LWCDOMMode { - manual = 'manual', + // Attrs need to be applied to element before props IE11 will wipe out value on radio inputs if + // value is set before type=radio. + patchAttributes(oldVnode, vnode); + patchProps(oldVnode, vnode); + patchClassAttribute(oldVnode, vnode); + patchStyleAttribute(oldVnode, vnode); } function hydrateElmHook(vnode: VBaseElement) { @@ -448,16 +454,6 @@ function fallbackElmHook(elm: Element, vnode: VElement) { } } -function updateElmHook(oldVnode: VElement, vnode: VElement) { - // Attrs need to be applied to element before props - // IE11 will wipe out value on radio inputs if value - // is set before type=radio. - patchAttributes(oldVnode, vnode); - patchProps(oldVnode, vnode); - patchClassAttribute(oldVnode, vnode); - patchStyleAttribute(oldVnode, vnode); -} - function allocateChildrenHook(vnode: VCustomElement, vm: VM) { // A component with slots will re-render because: // 1- There is a change of the internal state. @@ -514,19 +510,6 @@ function createViewModelHook(elm: HTMLElement, vnode: VCustomElement) { } } -function createCustomElmHook(vnode: VCustomElement) { - applyEventListeners(vnode); - // Attrs need to be applied to element before props - // IE11 will wipe out value on radio inputs if value - // is set before type=radio. - patchAttributes(null, vnode); - patchProps(null, vnode); - applyStaticClassAttribute(vnode); - applyStaticStyleAttribute(vnode); - patchClassAttribute(null, vnode); - patchStyleAttribute(null, vnode); -} - function createChildrenHook(vnode: VParentElement) { const { elm, children } = vnode; for (let j = 0; j < children.length; ++j) { @@ -715,16 +698,6 @@ export function hydrateChildrenHook(elmChildren: NodeListOf, children } } -function updateCustomElmHook(oldVnode: VCustomElement, vnode: VCustomElement) { - // Attrs need to be applied to element before props - // IE11 will wipe out value on radio inputs if value - // is set before type=radio. - patchAttributes(oldVnode, vnode); - patchProps(oldVnode, vnode); - patchClassAttribute(oldVnode, vnode); - patchStyleAttribute(oldVnode, vnode); -} - function removeElmHook(vnode: VElement) { // this method only needs to search on child vnodes from template // to trigger the remove hook just in case some of those children @@ -738,10 +711,6 @@ function removeElmHook(vnode: VElement) { } } -interface KeyToIndexMap { - [key: string]: number; -} - function sameVnode(vnode1: VNode, vnode2: VNode): boolean { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; } diff --git a/packages/@lwc/engine-core/src/framework/modules/events.ts b/packages/@lwc/engine-core/src/framework/modules/events.ts index ede8082034..a8f24953b2 100644 --- a/packages/@lwc/engine-core/src/framework/modules/events.ts +++ b/packages/@lwc/engine-core/src/framework/modules/events.ts @@ -7,32 +7,6 @@ import { isUndefined } from '@lwc/shared'; import { VBaseElement } from '../../3rdparty/snabbdom/types'; -function handleEvent(event: Event, vnode: VBaseElement) { - const { type } = event; - const { - data: { on }, - } = vnode; - const handler = on && on[type]; - // call event handler if exists - if (handler) { - handler.call(undefined, event); - } -} - -interface VNodeEventListener extends EventListener { - vnode?: VBaseElement; -} - -interface InteractiveVNode extends VBaseElement { - listener: VNodeEventListener | undefined; -} - -function createListener(): EventListener { - return function handler(event: Event) { - handleEvent(event, (handler as VNodeEventListener).vnode as VBaseElement); - }; -} - export function applyEventListeners(vnode: VBaseElement) { const { elm, @@ -44,11 +18,10 @@ export function applyEventListeners(vnode: VBaseElement) { return; } - const listener: VNodeEventListener = ((vnode as InteractiveVNode).listener = createListener()); - listener.vnode = vnode; - - let name; - for (name in on) { - renderer.addEventListener(elm, name, listener); + for (const name in on) { + const handler = on[name]; + renderer.addEventListener(elm, name, (event: Event) => { + handler(event); + }); } } From 02053e5f34f71a172a2b1a1c563d5670f13f27dd Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Wed, 15 Dec 2021 16:32:57 +0100 Subject: [PATCH 07/17] refactor: remove hooks --- .../src/3rdparty/snabbdom/types.ts | 18 +- .../@lwc/engine-core/src/framework/api.ts | 16 +- .../@lwc/engine-core/src/framework/hooks.ts | 1178 ++++++++++------- 3 files changed, 716 insertions(+), 496 deletions(-) diff --git a/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts b/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts index edc67fd731..7633baa194 100644 --- a/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts +++ b/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts @@ -35,7 +35,7 @@ export interface VBaseNode { elm: Node | undefined; text: string | undefined; key: Key | undefined; - hook: Hooks; + // hook: Hooks; owner: VM; type: VNodeType; } @@ -92,11 +92,11 @@ export interface VElementData extends VNodeData { key: Key; } -export interface Hooks { - create: (vNode: N) => void; - insert: (vNode: N, parentNode: Node, referenceNode: Node | null) => void; - move: (vNode: N, parentNode: Node, referenceNode: Node | null) => void; - update: (oldVNode: N, vNode: N) => void; - remove: (vNode: N, parentNode: Node) => void; - hydrate: (vNode: N, node: Node) => void; -} +// export interface Hooks { +// create: (vNode: N) => void; +// insert: (vNode: N, parentNode: Node, referenceNode: Node | null) => void; +// move: (vNode: N, parentNode: Node, referenceNode: Node | null) => void; +// update: (oldVNode: N, vNode: N) => void; +// remove: (vNode: N, parentNode: Node) => void; +// hydrate: (vNode: N, node: Node) => void; +// } diff --git a/packages/@lwc/engine-core/src/framework/api.ts b/packages/@lwc/engine-core/src/framework/api.ts index 09030a0333..ff3123c025 100644 --- a/packages/@lwc/engine-core/src/framework/api.ts +++ b/packages/@lwc/engine-core/src/framework/api.ts @@ -40,10 +40,10 @@ import { import { LightningElementConstructor } from './base-lightning-element'; import { markAsDynamicChildren, - TextHook, - CommentHook, - ElementHook, - CustomElementHook, + // TextHook, + // CommentHook, + // ElementHook, + // CustomElementHook, } from './hooks'; import { isComponentConstructor } from './def'; @@ -102,7 +102,7 @@ function h(sel: string, data: VElementData, children: VNodes): VElement { text, elm, key, - hook: ElementHook, + // hook: ElementHook, owner: vmBeingRendered, }; } @@ -219,7 +219,7 @@ function c( elm, key, - hook: CustomElementHook, + // hook: CustomElementHook, ctor: Ctor, owner: vmBeingRendered, mode: 'open', // TODO [#1294]: this should be defined in Ctor @@ -354,7 +354,7 @@ function t(text: string): VText { elm, key, - hook: TextHook, + // hook: TextHook, owner: getVMBeingRendered()!, }; } @@ -372,7 +372,7 @@ function co(text: string): VComment { elm, key, - hook: CommentHook, + // hook: CommentHook, owner: getVMBeingRendered()!, }; } diff --git a/packages/@lwc/engine-core/src/framework/hooks.ts b/packages/@lwc/engine-core/src/framework/hooks.ts index 69a27c4de2..48850c8aab 100644 --- a/packages/@lwc/engine-core/src/framework/hooks.ts +++ b/packages/@lwc/engine-core/src/framework/hooks.ts @@ -5,8 +5,8 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { - ArrayFilter, - ArrayJoin, + // ArrayFilter, + // ArrayJoin, ArrayPush, assert, create, @@ -17,7 +17,10 @@ import { keys, KEY__SHADOW_RESOLVER, } from '@lwc/shared'; -import { EmptyArray, parseStyleText } from './utils'; +import { + EmptyArray, + // parseStyleText +} from './utils'; import { createVM, getAssociatedVMIfPresent, @@ -29,8 +32,8 @@ import { runConnectedCallback, appendVM, removeVM, - getAssociatedVM, - hydrateVM, + // getAssociatedVM, + // hydrateVM, getRenderRoot, } from './vm'; import { @@ -39,11 +42,12 @@ import { VElement, VNodes, Key, - Hooks, + // Hooks, VText, VComment, - VParentElement, + // VParentElement, VBaseElement, + VNodeType, } from '../3rdparty/snabbdom/types'; import { applyEventListeners } from './modules/events'; @@ -56,7 +60,7 @@ import { applyStaticStyleAttribute } from './modules/static-style-attr'; import { patchElementWithRestrictions, unlockDomMutation, lockDomMutation } from './restrictions'; import { getComponentInternalDef } from './def'; -import { logError, logWarn } from '../shared/logger'; +// import { logError, logWarn } from '../shared/logger'; import { markComponentAsDirty } from './component'; import { getUpgradableConstructor } from './upgradable-element'; @@ -70,259 +74,474 @@ interface KeyToIndexMap { const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; -export const TextHook: Hooks = { - create: (vnode) => { - const { owner } = vnode; - const { renderer } = owner; - - const elm = renderer.createText(vnode.text!); - linkNodeToShadow(elm, owner); - vnode.elm = elm; - }, - update: updateNodeHook, - insert: insertNodeHook, - move: insertNodeHook, // same as insert for text nodes - remove: removeNodeHook, - hydrate: (vNode: VNode, node: Node) => { - if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line lwc-internal/no-global-node - if (node.nodeType !== Node.TEXT_NODE) { - logError('Hydration mismatch: incorrect node type received', vNode.owner); - assert.fail('Hydration mismatch: incorrect node type received.'); - } +function patch(n1: VNode | null, n2: VNode, parent: ParentNode, anchor: Node | null) { + if (n1 === n2) { + return; + } - if (node.nodeValue !== vNode.text) { - logWarn( - 'Hydration mismatch: text values do not match, will recover from the difference', - vNode.owner - ); - } - } + // FIXME: When does this occurs, is it possible with LWC? + if (!isNull(n1) && !sameVnode(n1, n2)) { + anchor = n2.owner.renderer.nextSibling(n1.elm); + unmount(n1, parent); + n1 = null; + } - // always set the text value to the one from the vnode. - node.nodeValue = vNode.text ?? null; - vNode.elm = node; - }, -}; - -export const CommentHook: Hooks = { - create: (vnode) => { - const { owner, text } = vnode; - const { renderer } = owner; - - const elm = renderer.createComment(text); - linkNodeToShadow(elm, owner); - vnode.elm = elm; - }, - update: updateNodeHook, - insert: insertNodeHook, - move: insertNodeHook, // same as insert for text nodes - remove: removeNodeHook, - hydrate: (vNode: VNode, node: Node) => { - if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line lwc-internal/no-global-node - if (node.nodeType !== Node.COMMENT_NODE) { - logError('Hydration mismatch: incorrect node type received', vNode.owner); - assert.fail('Hydration mismatch: incorrect node type received.'); - } + switch (n2.type) { + case VNodeType.Text: + processText(n1 as VText, n2, parent, anchor); + break; - if (node.nodeValue !== vNode.text) { - logWarn( - 'Hydration mismatch: comment values do not match, will recover from the difference', - vNode.owner - ); - } - } + case VNodeType.Comment: + processComment(n1 as VComment, n2, parent, anchor); + break; - // always set the text value to the one from the vnode. - node.nodeValue = vNode.text ?? null; - vNode.elm = node; - }, -}; - -// insert is called after update, which is used somewhere else (via a module) -// to mark the vm as inserted, that means we cannot use update as the main channel -// to rehydrate when dirty, because sometimes the element is not inserted just yet, -// which breaks some invariants. For that reason, we have the following for any -// Custom Element that is inserted via a template. -export const ElementHook: Hooks = { - create: (vnode) => { - const { - sel, - owner, - data: { svg }, - } = vnode; - const { renderer } = owner; - - const namespace = isTrue(svg) ? SVG_NAMESPACE : undefined; - const elm = renderer.createElement(sel, namespace); - - linkNodeToShadow(elm, owner); - fallbackElmHook(elm, vnode); - vnode.elm = elm; - - patchElementAttrsAndProps(null, vnode); - }, - update: (oldVnode, vnode) => { - patchElementAttrsAndProps(oldVnode, vnode); - patchChildren(vnode.elm!, oldVnode.children, vnode.children); - }, - insert: (vnode, parentNode, referenceNode) => { - insertNodeHook(vnode, parentNode, referenceNode); - createChildrenHook(vnode); - }, - move: (vnode, parentNode, referenceNode) => { - insertNodeHook(vnode, parentNode, referenceNode); - }, - remove: (vnode, parentNode) => { - removeNodeHook(vnode, parentNode); - removeElmHook(vnode); - }, - hydrate: (vnode, node) => { - const elm = node as Element; - vnode.elm = elm; - - const { context } = vnode.data; - const isDomManual = Boolean( - !isUndefined(context) && - !isUndefined(context.lwc) && - context.lwc.dom === LWCDOMMode.manual - ); + case VNodeType.Element: + processElement(n1 as VElement, n2, parent, anchor); + break; - if (isDomManual) { - // it may be that this element has lwc:inner-html, we need to diff and in case are the same, - // remove the innerHTML from props so it reuses the existing dom elements. - const { props } = vnode.data; - if (!isUndefined(props) && !isUndefined(props.innerHTML)) { - if (elm.innerHTML === props.innerHTML) { - delete props.innerHTML; - } else { - logWarn( - `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: innerHTML values do not match for element, will recover from the difference`, - vnode.owner - ); - } - } - } + case VNodeType.CustomElement: + processCustomElement(n1 as VCustomElement, n2, parent, anchor); + break; + } +} - hydrateElmHook(vnode); +function processText(n1: VText, n2: VText, parent: ParentNode, anchor: Node | null) { + const { owner } = n2; + const { renderer } = owner; - if (!isDomManual) { - hydrateChildrenHook(vnode.elm.childNodes, vnode.children, vnode.owner); - } - }, -}; - -export const CustomElementHook: Hooks = { - create: (vnode) => { - const { sel, owner } = vnode; - const { renderer } = owner; - const UpgradableConstructor = getUpgradableConstructor(sel, renderer); - /** - * Note: if the upgradable constructor does not expect, or throw when we new it - * with a callback as the first argument, we could implement a more advanced - * mechanism that only passes that argument if the constructor is known to be - * an upgradable custom element. - */ - const elm = new UpgradableConstructor((elm: HTMLElement) => { - // the custom element from the registry is expecting an upgrade callback - createViewModelHook(elm, vnode); - }); - - linkNodeToShadow(elm, owner); - vnode.elm = elm; - - const vm = getAssociatedVMIfPresent(elm); - if (vm) { - allocateChildrenHook(vnode, vm); - } else if (vnode.ctor !== UpgradableConstructor) { - throw new TypeError(`Incorrect Component Constructor`); - } - patchElementAttrsAndProps(null, vnode); - }, - update: (oldVnode, vnode) => { - patchElementAttrsAndProps(oldVnode, vnode); - const vm = getAssociatedVMIfPresent(vnode.elm); - if (vm) { - // in fallback mode, the allocation will always set children to - // empty and delegate the real allocation to the slot elements - allocateChildrenHook(vnode, vm); - } - // in fallback mode, the children will be always empty, so, nothing - // will happen, but in native, it does allocate the light dom - patchChildren(vnode.elm!, oldVnode.children, vnode.children); - if (vm) { - if (process.env.NODE_ENV !== 'production') { - assert.isTrue( - isArray(vnode.children), - `Invalid vnode for a custom element, it must have children defined.` - ); - } - // this will probably update the shadowRoot, but only if the vm is in a dirty state - // this is important to preserve the top to bottom synchronous rendering phase. - rerenderVM(vm); - } - }, - insert: (vnode, parentNode, referenceNode) => { - insertNodeHook(vnode, parentNode, referenceNode); - const vm = getAssociatedVMIfPresent(vnode.elm); - if (vm) { - if (process.env.NODE_ENV !== 'production') { - assert.isTrue(vm.state === VMState.created, `${vm} cannot be recycled.`); - } - runConnectedCallback(vm); - } - createChildrenHook(vnode); - if (vm) { - appendVM(vm); - } - }, - move: (vnode, parentNode, referenceNode) => { - insertNodeHook(vnode, parentNode, referenceNode); - }, - remove: (vnode, parentNode) => { - removeNodeHook(vnode, parentNode); - const vm = getAssociatedVMIfPresent(vnode.elm); - if (vm) { - // for custom elements we don't have to go recursively because the removeVM routine - // will take care of disconnecting any child VM attached to its shadow as well. - removeVM(vm); + if (isNull(n1)) { + const textNode = (n2.elm = renderer.createText(n2.text)); + linkNodeToShadow(textNode, owner); + + insertNodeHook(n2, parent, anchor); + } else { + n2.elm = n1.elm; + + // FIXME: We shouldn't need compare the node content in `updateNodeHook`. + if (n2.text !== n1.text) { + updateNodeHook(n1, n2); } - }, - hydrate: (vnode, elm) => { - // the element is created, but the vm is not - const { sel, mode, ctor, owner } = vnode; - - const def = getComponentInternalDef(ctor); - createVM(elm, def, { - mode, - owner, - tagName: sel, - renderer: owner.renderer, - }); - - vnode.elm = elm as Element; - - const vm = getAssociatedVM(elm); + } +} + +function processComment(n1: VComment, n2: VComment, parent: ParentNode, anchor: Node | null) { + const { owner } = n2; + const { renderer } = owner; + + if (isNull(n1)) { + const textNode = (n2.elm = renderer.createComment(n2.text)); + linkNodeToShadow(textNode, owner); + + insertNodeHook(n2, parent, anchor); + } else { + // No need to patch the comment text, as it is static. + n2.elm = n1.elm; + } +} + +function processElement(n1: VElement, n2: VElement, parent: ParentNode, anchor: Node | null) { + if (isNull(n1)) { + mountElement(n2, parent, anchor); + } else { + patchElement(n1, n2); + } +} + +function mountElement(vnode: VElement, parent: ParentNode, anchor: Node | null) { + const { + sel, + owner, + data: { svg }, + } = vnode; + const { renderer } = owner; + + const namespace = isTrue(svg) ? SVG_NAMESPACE : undefined; + const elm = (vnode.elm = renderer.createElement(sel, namespace)); + linkNodeToShadow(elm, owner); + + fallbackElmHook(elm, vnode); + patchElementAttrsAndProps(null, vnode); + + insertNodeHook(vnode, parent, anchor); + + mountChildren(vnode.children, elm); +} + +function patchElement(n1: VElement, n2: VElement) { + const elm = (n2.elm = n1.elm!); + + patchElementAttrsAndProps(n1, n2); + patchChildren(elm, n1.children, n2.children); +} + +function processCustomElement( + n1: VCustomElement, + n2: VCustomElement, + parent: ParentNode, + anchor: Node | null +) { + if (isNull(n1)) { + mountCustomElement(n2, parent, anchor); + } else { + patchCustomElement(n1, n2); + } +} + +function mountCustomElement(vnode: VCustomElement, parent: ParentNode, anchor: Node | null) { + const { sel, owner } = vnode; + const { renderer } = owner; + + const UpgradableConstructor = getUpgradableConstructor(sel, renderer); + /** + * Note: if the upgradable constructor does not expect, or throw when we new it + * with a callback as the first argument, we could implement a more advanced + * mechanism that only passes that argument if the constructor is known to be + * an upgradable custom element. + */ + const elm = (vnode.elm = new UpgradableConstructor((elm: HTMLElement) => { + // the custom element from the registry is expecting an upgrade callback + createViewModelHook(elm, vnode); + })); + linkNodeToShadow(elm, owner); + + const vm = getAssociatedVMIfPresent(elm); + + if (!isUndefined(vm)) { allocateChildrenHook(vnode, vm); + } else if (vnode.ctor !== UpgradableConstructor) { + throw new TypeError(`Incorrect Component Constructor`); + } + + patchElementAttrsAndProps(null, vnode); - hydrateElmHook(vnode); + insertNodeHook(vnode, parent, anchor); - // Insert hook section: + if (!isUndefined(vm)) { if (process.env.NODE_ENV !== 'production') { assert.isTrue(vm.state === VMState.created, `${vm} cannot be recycled.`); } runConnectedCallback(vm); + } + + mountChildren(vnode.children, elm); + + if (vm) { + appendVM(vm); + } +} + +function patchCustomElement(n1: VCustomElement, n2: VCustomElement) { + const elm = (n2.elm = n1.elm!); + const vm = getAssociatedVMIfPresent(elm); + + patchElementAttrsAndProps(n1, n2); + + if (!isUndefined(vm)) { + // in fallback mode, the allocation will always set children to + // empty and delegate the real allocation to the slot elements + allocateChildrenHook(n2, vm); + } + + // in fallback mode, the children will be always empty, so, nothing + // will happen, but in native, it does allocate the light dom + patchChildren(elm, n1.children, n1.children); - if (vm.renderMode !== RenderMode.Light) { - // VM is not rendering in Light DOM, we can proceed and hydrate the slotted content. - // Note: for Light DOM, this is handled while hydrating the VM - hydrateChildrenHook(vnode.elm.childNodes, vnode.children, vm); + if (!isUndefined(vm)) { + if (process.env.NODE_ENV !== 'production') { + assert.isTrue( + isArray(n2.children), + `Invalid vnode for a custom element, it must have children defined.` + ); } + // this will probably update the shadowRoot, but only if the vm is in a dirty state + // this is important to preserve the top to bottom synchronous rendering phase. + rerenderVM(vm); + } +} + +function unmount(vnode: VNode, parent: ParentNode) { + removeNodeHook(vnode, parent); + + switch (vnode.type) { + case VNodeType.Element: + unmountChildren(vnode.children, vnode.elm!); + break; - hydrateVM(vm); - }, -}; + case VNodeType.CustomElement: { + const vm = getAssociatedVMIfPresent(vnode.elm); + + // No need to unmount the children here, `removeVM` will take care of removing the + // children. + if (!isUndefined(vm)) { + removeVM(vm); + } + } + } +} + +function unmountChildren(children: VNodes, parent: ParentNode) { + for (let i = 0; i < children.length; ++i) { + const child = children[i]; + + if (child != null) { + unmount(child, parent); + } + } +} + +// export const TextHook: Hooks = { +// create: (vnode) => { +// const { owner } = vnode; +// const { renderer } = owner; + +// const elm = renderer.createText(vnode.text!); +// linkNodeToShadow(elm, owner); +// vnode.elm = elm; +// }, +// update: updateNodeHook, +// insert: insertNodeHook, +// move: insertNodeHook, // same as insert for text nodes +// remove: removeNodeHook, +// hydrate: (vNode: VNode, node: Node) => { +// if (process.env.NODE_ENV !== 'production') { +// // eslint-disable-next-line lwc-internal/no-global-node +// if (node.nodeType !== Node.TEXT_NODE) { +// logError('Hydration mismatch: incorrect node type received', vNode.owner); +// assert.fail('Hydration mismatch: incorrect node type received.'); +// } + +// if (node.nodeValue !== vNode.text) { +// logWarn( +// 'Hydration mismatch: text values do not match, will recover from the difference', +// vNode.owner +// ); +// } +// } + +// // always set the text value to the one from the vnode. +// node.nodeValue = vNode.text ?? null; +// vNode.elm = node; +// }, +// }; + +// export const CommentHook: Hooks = { +// create: (vnode) => { +// const { owner, text } = vnode; +// const { renderer } = owner; + +// const elm = renderer.createComment(text); +// linkNodeToShadow(elm, owner); +// vnode.elm = elm; +// }, +// update: updateNodeHook, +// insert: insertNodeHook, +// move: insertNodeHook, // same as insert for text nodes +// remove: removeNodeHook, +// hydrate: (vNode: VNode, node: Node) => { +// if (process.env.NODE_ENV !== 'production') { +// // eslint-disable-next-line lwc-internal/no-global-node +// if (node.nodeType !== Node.COMMENT_NODE) { +// logError('Hydration mismatch: incorrect node type received', vNode.owner); +// assert.fail('Hydration mismatch: incorrect node type received.'); +// } + +// if (node.nodeValue !== vNode.text) { +// logWarn( +// 'Hydration mismatch: comment values do not match, will recover from the difference', +// vNode.owner +// ); +// } +// } + +// // always set the text value to the one from the vnode. +// node.nodeValue = vNode.text ?? null; +// vNode.elm = node; +// }, +// }; + +// // insert is called after update, which is used somewhere else (via a module) +// // to mark the vm as inserted, that means we cannot use update as the main channel +// // to rehydrate when dirty, because sometimes the element is not inserted just yet, +// // which breaks some invariants. For that reason, we have the following for any +// // Custom Element that is inserted via a template. +// export const ElementHook: Hooks = { +// create: (vnode) => { +// const { +// sel, +// owner, +// data: { svg }, +// } = vnode; +// const { renderer } = owner; + +// const namespace = isTrue(svg) ? SVG_NAMESPACE : undefined; +// const elm = renderer.createElement(sel, namespace); + +// linkNodeToShadow(elm, owner); +// fallbackElmHook(elm, vnode); +// vnode.elm = elm; + +// patchElementAttrsAndProps(null, vnode); +// }, +// update: (oldVnode, vnode) => { +// patchElementAttrsAndProps(oldVnode, vnode); +// patchChildren(vnode.elm!, oldVnode.children, vnode.children); +// }, +// insert: (vnode, parentNode, referenceNode) => { +// insertNodeHook(vnode, parentNode, referenceNode); +// mountChildren(vnode); +// }, +// move: (vnode, parentNode, referenceNode) => { +// insertNodeHook(vnode, parentNode, referenceNode); +// }, +// remove: (vnode, parentNode) => { +// removeNodeHook(vnode, parentNode); +// removeElmHook(vnode); +// }, +// hydrate: (vnode, node) => { +// const elm = node as Element; +// vnode.elm = elm; + +// const { context } = vnode.data; +// const isDomManual = Boolean( +// !isUndefined(context) && +// !isUndefined(context.lwc) && +// context.lwc.dom === LWCDOMMode.manual +// ); + +// if (isDomManual) { +// // it may be that this element has lwc:inner-html, we need to diff and in case are the same, +// // remove the innerHTML from props so it reuses the existing dom elements. +// const { props } = vnode.data; +// if (!isUndefined(props) && !isUndefined(props.innerHTML)) { +// if (elm.innerHTML === props.innerHTML) { +// delete props.innerHTML; +// } else { +// logWarn( +// `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: innerHTML values do not match for element, will recover from the difference`, +// vnode.owner +// ); +// } +// } +// } + +// hydrateElmHook(vnode); + +// if (!isDomManual) { +// hydrateChildrenHook(vnode.elm.childNodes, vnode.children, vnode.owner); +// } +// }, +// }; + +// export const CustomElementHook: Hooks = { +// create: (vnode) => { +// const { sel, owner } = vnode; +// const { renderer } = owner; +// const UpgradableConstructor = getUpgradableConstructor(sel, renderer); +// /** +// * Note: if the upgradable constructor does not expect, or throw when we new it +// * with a callback as the first argument, we could implement a more advanced +// * mechanism that only passes that argument if the constructor is known to be +// * an upgradable custom element. +// */ +// const elm = new UpgradableConstructor((elm: HTMLElement) => { +// // the custom element from the registry is expecting an upgrade callback +// createViewModelHook(elm, vnode); +// }); + +// linkNodeToShadow(elm, owner); +// vnode.elm = elm; + +// const vm = getAssociatedVMIfPresent(elm); +// if (vm) { +// allocateChildrenHook(vnode, vm); +// } else if (vnode.ctor !== UpgradableConstructor) { +// throw new TypeError(`Incorrect Component Constructor`); +// } +// patchElementAttrsAndProps(null, vnode); +// }, +// update: (oldVnode, vnode) => { +// patchElementAttrsAndProps(oldVnode, vnode); +// const vm = getAssociatedVMIfPresent(vnode.elm); +// if (vm) { +// // in fallback mode, the allocation will always set children to +// // empty and delegate the real allocation to the slot elements +// allocateChildrenHook(vnode, vm); +// } +// // in fallback mode, the children will be always empty, so, nothing +// // will happen, but in native, it does allocate the light dom +// patchChildren(vnode.elm!, oldVnode.children, vnode.children); +// if (vm) { +// if (process.env.NODE_ENV !== 'production') { +// assert.isTrue( +// isArray(vnode.children), +// `Invalid vnode for a custom element, it must have children defined.` +// ); +// } +// // this will probably update the shadowRoot, but only if the vm is in a dirty state +// // this is important to preserve the top to bottom synchronous rendering phase. +// rerenderVM(vm); +// } +// }, +// insert: (vnode, parentNode, referenceNode) => { +// insertNodeHook(vnode, parentNode, referenceNode); +// const vm = getAssociatedVMIfPresent(vnode.elm); +// if (vm) { +// if (process.env.NODE_ENV !== 'production') { +// assert.isTrue(vm.state === VMState.created, `${vm} cannot be recycled.`); +// } +// runConnectedCallback(vm); +// } +// mountChildren(vnode); +// if (vm) { +// appendVM(vm); +// } +// }, +// move: (vnode, parentNode, referenceNode) => { +// insertNodeHook(vnode, parentNode, referenceNode); +// }, +// remove: (vnode, parentNode) => { +// removeNodeHook(vnode, parentNode); +// const vm = getAssociatedVMIfPresent(vnode.elm); +// if (vm) { +// // for custom elements we don't have to go recursively because the removeVM routine +// // will take care of disconnecting any child VM attached to its shadow as well. +// removeVM(vm); +// } +// }, +// hydrate: (vnode, elm) => { +// // the element is created, but the vm is not +// const { sel, mode, ctor, owner } = vnode; + +// const def = getComponentInternalDef(ctor); +// createVM(elm, def, { +// mode, +// owner, +// tagName: sel, +// renderer: owner.renderer, +// }); + +// vnode.elm = elm as Element; + +// const vm = getAssociatedVM(elm); +// allocateChildrenHook(vnode, vm); + +// hydrateElmHook(vnode); + +// // Insert hook section: +// if (process.env.NODE_ENV !== 'production') { +// assert.isTrue(vm.state === VMState.created, `${vm} cannot be recycled.`); +// } +// runConnectedCallback(vm); + +// if (vm.renderMode !== RenderMode.Light) { +// // VM is not rendering in Light DOM, we can proceed and hydrate the slotted content. +// // Note: for Light DOM, this is handled while hydrating the VM +// hydrateChildrenHook(vnode.elm.childNodes, vnode.children, vm); +// } + +// hydrateVM(vm); +// }, +// }; function linkNodeToShadow(elm: Node, owner: VM) { const { renderer, renderMode, shadowMode } = owner; @@ -409,17 +628,17 @@ function patchElementAttrsAndProps(oldVnode: VBaseElement | null, vnode: VBaseEl patchStyleAttribute(oldVnode, vnode); } -function hydrateElmHook(vnode: VBaseElement) { - applyEventListeners(vnode); - // Attrs are already on the element. - // modAttrs.create(vnode); - patchProps(null, vnode); - // Already set. - // applyStaticClassAttribute(vnode); - // applyStaticStyleAttribute(vnode); - // modComputedClassName.create(vnode); - // modComputedStyle.create(vnode); -} +// function hydrateElmHook(vnode: VBaseElement) { +// applyEventListeners(vnode); +// // Attrs are already on the element. +// // modAttrs.create(vnode); +// patchProps(null, vnode); +// // Already set. +// // applyStaticClassAttribute(vnode); +// // applyStaticStyleAttribute(vnode); +// // modComputedClassName.create(vnode); +// // modComputedStyle.create(vnode); +// } function fallbackElmHook(elm: Element, vnode: VElement) { const { owner } = vnode; @@ -510,206 +729,203 @@ function createViewModelHook(elm: HTMLElement, vnode: VCustomElement) { } } -function createChildrenHook(vnode: VParentElement) { - const { elm, children } = vnode; - for (let j = 0; j < children.length; ++j) { - const ch = children[j]; - if (ch != null) { - ch.hook.create(ch); - ch.hook.insert(ch, elm!, null); - } - } -} - -function isElementNode(node: ChildNode): node is Element { - // eslint-disable-next-line lwc-internal/no-global-node - return node.nodeType === Node.ELEMENT_NODE; -} - -function vnodesAndElementHaveCompatibleAttrs(vnode: VNode, elm: Element): boolean { - const { - data: { attrs = {} }, - owner: { renderer }, - } = vnode; - - let nodesAreCompatible = true; - - // Validate attributes, though we could always recovery from those by running the update mods. - // Note: intentionally ONLY matching vnodes.attrs to elm.attrs, in case SSR is adding extra attributes. - for (const [attrName, attrValue] of Object.entries(attrs)) { - const elmAttrValue = renderer.getAttribute(elm, attrName); - if (String(attrValue) !== elmAttrValue) { - logError( - `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: attribute "${attrName}" has different values, expected "${attrValue}" but found "${elmAttrValue}"`, - vnode.owner - ); - nodesAreCompatible = false; - } - } - - return nodesAreCompatible; -} - -function vnodesAndElementHaveCompatibleClass(vnode: VNode, elm: Element): boolean { - const { - data: { className, classMap }, - owner: { renderer }, - } = vnode; - - let nodesAreCompatible = true; - let vnodeClassName; - - if (!isUndefined(className) && String(className) !== elm.className) { - // className is used when class is bound to an expr. - nodesAreCompatible = false; - vnodeClassName = className; - } else if (!isUndefined(classMap)) { - // classMap is used when class is set to static value. - const classList = renderer.getClassList(elm); - let computedClassName = ''; - - // all classes from the vnode should be in the element.classList - for (const name in classMap) { - computedClassName += ' ' + name; - if (!classList.contains(name)) { - nodesAreCompatible = false; - } - } - - vnodeClassName = computedClassName.trim(); - - if (classList.length > keys(classMap).length) { - nodesAreCompatible = false; - } - } - - if (!nodesAreCompatible) { - logError( - `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: attribute "class" has different values, expected "${vnodeClassName}" but found "${ - elm.className - }"`, - vnode.owner - ); - } - - return nodesAreCompatible; -} - -function vnodesAndElementHaveCompatibleStyle(vnode: VNode, elm: Element): boolean { - const { - data: { style, styleDecls }, - owner: { renderer }, - } = vnode; - const elmStyle = renderer.getAttribute(elm, 'style') || ''; - let vnodeStyle; - let nodesAreCompatible = true; - - if (!isUndefined(style) && style !== elmStyle) { - nodesAreCompatible = false; - vnodeStyle = style; - } else if (!isUndefined(styleDecls)) { - const parsedVnodeStyle = parseStyleText(elmStyle); - const expectedStyle = []; - // styleMap is used when style is set to static value. - for (let i = 0, n = styleDecls.length; i < n; i++) { - const [prop, value, important] = styleDecls[i]; - expectedStyle.push(`${prop}: ${value + (important ? ' important!' : '')}`); - - const parsedPropValue = parsedVnodeStyle[prop]; - - if (isUndefined(parsedPropValue)) { - nodesAreCompatible = false; - } else if (!parsedPropValue.startsWith(value)) { - nodesAreCompatible = false; - } else if (important && !parsedPropValue.endsWith('!important')) { - nodesAreCompatible = false; - } +function mountChildren(children: VNodes, parent: Element) { + for (let i = 0; i < children.length; ++i) { + const child = children[i]; + if (child != null) { + patch(null, child, parent, null); + // ch.hook.create(ch); + // ch.hook.insert(ch, elm!, null); } - - if (keys(parsedVnodeStyle).length > styleDecls.length) { - nodesAreCompatible = false; - } - - vnodeStyle = ArrayJoin.call(expectedStyle, ';'); } - - if (!nodesAreCompatible) { - // style is used when class is bound to an expr. - logError( - `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: attribute "style" has different values, expected "${vnodeStyle}" but found "${elmStyle}".`, - vnode.owner - ); - } - - return nodesAreCompatible; } -function throwHydrationError() { - assert.fail('Server rendered elements do not match client side generated elements'); -} - -export function hydrateChildrenHook(elmChildren: NodeListOf, children: VNodes, vm?: VM) { - if (process.env.NODE_ENV !== 'production') { - const filteredVNodes = ArrayFilter.call(children, (vnode) => !!vnode); - - if (elmChildren.length !== filteredVNodes.length) { - logError( - `Hydration mismatch: incorrect number of rendered nodes, expected ${filteredVNodes.length} but found ${elmChildren.length}.`, - vm - ); - throwHydrationError(); - } - } - - let elmCurrentChildIdx = 0; - for (let j = 0, n = children.length; j < n; j++) { - const ch = children[j]; - if (ch != null) { - const childNode = elmChildren[elmCurrentChildIdx]; - - if (process.env.NODE_ENV !== 'production') { - // VComments and VTexts validation is handled in their hooks - if (isElementNode(childNode)) { - if (ch.sel?.toLowerCase() !== childNode.tagName.toLowerCase()) { - logError( - `Hydration mismatch: expecting element with tag "${ch.sel?.toLowerCase()}" but found "${childNode.tagName.toLowerCase()}".`, - vm - ); - - throwHydrationError(); - } - - // Note: props are not yet set - const hasIncompatibleAttrs = vnodesAndElementHaveCompatibleAttrs(ch, childNode); - const hasIncompatibleClass = vnodesAndElementHaveCompatibleClass(ch, childNode); - const hasIncompatibleStyle = vnodesAndElementHaveCompatibleStyle(ch, childNode); - const isVNodeAndElementCompatible = - hasIncompatibleAttrs && hasIncompatibleClass && hasIncompatibleStyle; - - if (!isVNodeAndElementCompatible) { - throwHydrationError(); - } - } - } - - ch.hook.hydrate(ch, childNode); - elmCurrentChildIdx++; - } - } +// function isElementNode(node: ChildNode): node is Element { +// // eslint-disable-next-line lwc-internal/no-global-node +// return node.nodeType === Node.ELEMENT_NODE; +// } + +// function vnodesAndElementHaveCompatibleAttrs(vnode: VNode, elm: Element): boolean { +// const { +// data: { attrs = {} }, +// owner: { renderer }, +// } = vnode; + +// let nodesAreCompatible = true; + +// // Validate attributes, though we could always recovery from those by running the update mods. +// // Note: intentionally ONLY matching vnodes.attrs to elm.attrs, in case SSR is adding extra attributes. +// for (const [attrName, attrValue] of Object.entries(attrs)) { +// const elmAttrValue = renderer.getAttribute(elm, attrName); +// if (String(attrValue) !== elmAttrValue) { +// logError( +// `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: attribute "${attrName}" has different values, expected "${attrValue}" but found "${elmAttrValue}"`, +// vnode.owner +// ); +// nodesAreCompatible = false; +// } +// } + +// return nodesAreCompatible; +// } + +// function vnodesAndElementHaveCompatibleClass(vnode: VNode, elm: Element): boolean { +// const { +// data: { className, classMap }, +// owner: { renderer }, +// } = vnode; + +// let nodesAreCompatible = true; +// let vnodeClassName; + +// if (!isUndefined(className) && String(className) !== elm.className) { +// // className is used when class is bound to an expr. +// nodesAreCompatible = false; +// vnodeClassName = className; +// } else if (!isUndefined(classMap)) { +// // classMap is used when class is set to static value. +// const classList = renderer.getClassList(elm); +// let computedClassName = ''; + +// // all classes from the vnode should be in the element.classList +// for (const name in classMap) { +// computedClassName += ' ' + name; +// if (!classList.contains(name)) { +// nodesAreCompatible = false; +// } +// } + +// vnodeClassName = computedClassName.trim(); + +// if (classList.length > keys(classMap).length) { +// nodesAreCompatible = false; +// } +// } + +// if (!nodesAreCompatible) { +// logError( +// `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: attribute "class" has different values, expected "${vnodeClassName}" but found "${ +// elm.className +// }"`, +// vnode.owner +// ); +// } + +// return nodesAreCompatible; +// } + +// function vnodesAndElementHaveCompatibleStyle(vnode: VNode, elm: Element): boolean { +// const { +// data: { style, styleDecls }, +// owner: { renderer }, +// } = vnode; +// const elmStyle = renderer.getAttribute(elm, 'style') || ''; +// let vnodeStyle; +// let nodesAreCompatible = true; + +// if (!isUndefined(style) && style !== elmStyle) { +// nodesAreCompatible = false; +// vnodeStyle = style; +// } else if (!isUndefined(styleDecls)) { +// const parsedVnodeStyle = parseStyleText(elmStyle); +// const expectedStyle = []; +// // styleMap is used when style is set to static value. +// for (let i = 0, n = styleDecls.length; i < n; i++) { +// const [prop, value, important] = styleDecls[i]; +// expectedStyle.push(`${prop}: ${value + (important ? ' important!' : '')}`); + +// const parsedPropValue = parsedVnodeStyle[prop]; + +// if (isUndefined(parsedPropValue)) { +// nodesAreCompatible = false; +// } else if (!parsedPropValue.startsWith(value)) { +// nodesAreCompatible = false; +// } else if (important && !parsedPropValue.endsWith('!important')) { +// nodesAreCompatible = false; +// } +// } + +// if (keys(parsedVnodeStyle).length > styleDecls.length) { +// nodesAreCompatible = false; +// } + +// vnodeStyle = ArrayJoin.call(expectedStyle, ';'); +// } + +// if (!nodesAreCompatible) { +// // style is used when class is bound to an expr. +// logError( +// `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: attribute "style" has different values, expected "${vnodeStyle}" but found "${elmStyle}".`, +// vnode.owner +// ); +// } + +// return nodesAreCompatible; +// } + +// function throwHydrationError() { +// assert.fail('Server rendered elements do not match client side generated elements'); +// } + +export function hydrateChildrenHook( + _elmChildren: NodeListOf, + _children: VNodes, + _vm?: VM +) { + // if (process.env.NODE_ENV !== 'production') { + // const filteredVNodes = ArrayFilter.call(children, (vnode) => !!vnode); + // if (elmChildren.length !== filteredVNodes.length) { + // logError( + // `Hydration mismatch: incorrect number of rendered nodes, expected ${filteredVNodes.length} but found ${elmChildren.length}.`, + // vm + // ); + // throwHydrationError(); + // } + // } + // let elmCurrentChildIdx = 0; + // for (let j = 0, n = children.length; j < n; j++) { + // const ch = children[j]; + // if (ch != null) { + // const childNode = elmChildren[elmCurrentChildIdx]; + // if (process.env.NODE_ENV !== 'production') { + // // VComments and VTexts validation is handled in their hooks + // if (isElementNode(childNode)) { + // if (ch.sel?.toLowerCase() !== childNode.tagName.toLowerCase()) { + // logError( + // `Hydration mismatch: expecting element with tag "${ch.sel?.toLowerCase()}" but found "${childNode.tagName.toLowerCase()}".`, + // vm + // ); + // throwHydrationError(); + // } + // // Note: props are not yet set + // const hasIncompatibleAttrs = vnodesAndElementHaveCompatibleAttrs(ch, childNode); + // const hasIncompatibleClass = vnodesAndElementHaveCompatibleClass(ch, childNode); + // const hasIncompatibleStyle = vnodesAndElementHaveCompatibleStyle(ch, childNode); + // const isVNodeAndElementCompatible = + // hasIncompatibleAttrs && hasIncompatibleClass && hasIncompatibleStyle; + // if (!isVNodeAndElementCompatible) { + // throwHydrationError(); + // } + // } + // } + // ch.hook.hydrate(ch, childNode); + // elmCurrentChildIdx++; + // } + // } } -function removeElmHook(vnode: VElement) { - // this method only needs to search on child vnodes from template - // to trigger the remove hook just in case some of those children - // are custom elements. - const { children, elm } = vnode; - for (let j = 0, len = children.length; j < len; ++j) { - const ch = children[j]; - if (!isNull(ch)) { - ch.hook.remove(ch, elm!); - } - } -} +// function removeElmHook(vnode: VElement) { +// // this method only needs to search on child vnodes from template +// // to trigger the remove hook just in case some of those children +// // are custom elements. +// const { children, elm } = vnode; +// for (let j = 0, len = children.length; j < len; ++j) { +// const ch = children[j]; +// if (!isNull(ch)) { +// ch.hook.remove(ch, elm!); +// } +// } +// } function sameVnode(vnode1: VNode, vnode2: VNode): boolean { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; @@ -736,7 +952,7 @@ function createKeyToOldIdx(children: VNodes, beginIdx: number, endIdx: number): } function addVnodes( - parentElm: Node, + parentElm: ParentNode, before: Node | null, vnodes: VNodes, startIdx: number, @@ -745,23 +961,30 @@ function addVnodes( for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx]; if (isVNode(ch)) { - ch.hook.create(ch); - ch.hook.insert(ch, parentElm, before); + patch(null, ch, parentElm, before); + // ch.hook.create(ch); + // ch.hook.insert(ch, parentElm, before); } } } -function removeVnodes(parentElm: Node, vnodes: VNodes, startIdx: number, endIdx: number): void { +function removeVnodes( + parentElm: ParentNode, + vnodes: VNodes, + startIdx: number, + endIdx: number +): void { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx]; // text nodes do not have logic associated to them if (isVNode(ch)) { - ch.hook.remove(ch, parentElm); + unmount(ch, parentElm); + // ch.hook.remove(ch, parentElm); } } } -function updateDynamicChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes) { +function updateDynamicChildren(parentElm: ParentNode, oldCh: VNodes, newCh: VNodes) { let oldStartIdx = 0; let newStartIdx = 0; let oldEndIdx = oldCh.length - 1; @@ -785,17 +1008,17 @@ function updateDynamicChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes) { } else if (!isVNode(newEndVnode)) { newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newStartVnode)) { - patchVnode(oldStartVnode, newStartVnode); + patch(oldStartVnode, newStartVnode, parentElm, null); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (sameVnode(oldEndVnode, newEndVnode)) { - patchVnode(oldEndVnode, newEndVnode); + patch(oldEndVnode, newEndVnode, parentElm, null); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right - patchVnode(oldStartVnode, newEndVnode); - newEndVnode.hook.move( + patch(oldStartVnode, newEndVnode, parentElm, null); + insertNodeHook( oldStartVnode, parentElm, oldEndVnode.owner.renderer.nextSibling(oldEndVnode.elm!) @@ -804,8 +1027,8 @@ function updateDynamicChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes) { newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left - patchVnode(oldEndVnode, newStartVnode); - newStartVnode.hook.move(oldEndVnode, parentElm, oldStartVnode.elm!); + patch(oldEndVnode, newStartVnode, parentElm, null); + insertNodeHook(oldEndVnode, parentElm, oldStartVnode.elm!); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { @@ -815,20 +1038,18 @@ function updateDynamicChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes) { idxInOld = oldKeyToIdx[newStartVnode.key!]; if (isUndefined(idxInOld)) { // New element - newStartVnode.hook.create(newStartVnode); - newStartVnode.hook.insert(newStartVnode, parentElm, oldStartVnode.elm!); + patch(null, newStartVnode, parentElm, oldStartVnode.elm!); newStartVnode = newCh[++newStartIdx]; } else { elmToMove = oldCh[idxInOld]; if (isVNode(elmToMove)) { if (elmToMove.sel !== newStartVnode.sel) { // New element - newStartVnode.hook.create(newStartVnode); - newStartVnode.hook.insert(newStartVnode, parentElm, oldStartVnode.elm!); + patch(null, newStartVnode, parentElm, oldStartVnode.elm!); } else { - patchVnode(elmToMove, newStartVnode); + patch(elmToMove, newStartVnode, parentElm, null); oldCh[idxInOld] = undefined as any; - newStartVnode.hook.move(elmToMove, parentElm, oldStartVnode.elm!); + insertNodeHook(elmToMove, parentElm, oldStartVnode.elm!); } } newStartVnode = newCh[++newStartIdx]; @@ -852,7 +1073,7 @@ function updateDynamicChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes) { } } -function updateStaticChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes) { +function updateStaticChildren(parentElm: ParentNode, oldCh: VNodes, newCh: VNodes) { const oldChLength = oldCh.length; const newChLength = newCh.length; @@ -877,24 +1098,23 @@ function updateStaticChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes) { if (isVNode(oldVNode)) { if (isVNode(vnode)) { // both vnodes must be equivalent, and se just need to patch them - patchVnode(oldVNode, vnode); + patch(oldVNode, vnode, parentElm, null); referenceElm = vnode.elm!; } else { // removing the old vnode since the new one is null - oldVNode.hook.remove(oldVNode, parentElm); + unmount(oldVNode, parentElm); } } else if (isVNode(vnode)) { // this condition is unnecessary - vnode.hook.create(vnode); - // insert the new node one since the old one is null - vnode.hook.insert(vnode, parentElm, referenceElm); + // insert the new node one since the old one is nul + patch(null, vnode, parentElm, referenceElm); referenceElm = vnode.elm!; } } } } -export function patchChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes): void { +export function patchChildren(parentElm: ParentNode, oldCh: VNodes, newCh: VNodes): void { if (hasDynamicChildren(newCh)) { updateDynamicChildren(parentElm, oldCh, newCh); } else { @@ -902,12 +1122,12 @@ export function patchChildren(parentElm: Node, oldCh: VNodes, newCh: VNodes): vo } } -function patchVnode(oldVnode: VNode, vnode: VNode) { - if (oldVnode !== vnode) { - vnode.elm = oldVnode.elm; - vnode.hook.update(oldVnode, vnode); - } -} +// function patchVnode(oldVnode: VNode, vnode: VNode) { +// if (oldVnode !== vnode) { +// vnode.elm = oldVnode.elm; +// vnode.hook.update(oldVnode, vnode); +// } +// } // slow path routine // NOTE: we should probably more this routine to the synthetic shadow folder From 567ce7d4f9a8e08e4fab1bf8a27aafe0e8d7368f Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Wed, 15 Dec 2021 17:28:15 +0100 Subject: [PATCH 08/17] refactor: move hydration in a different file --- .../src/3rdparty/snabbdom/types.ts | 10 - .../@lwc/engine-core/src/framework/api.ts | 27 +- .../engine-core/src/framework/hydration.ts | 342 ++++++++++ .../src/framework/{hooks.ts => rendering.ts} | 614 ++---------------- packages/@lwc/engine-core/src/framework/vm.ts | 9 +- 5 files changed, 424 insertions(+), 578 deletions(-) create mode 100644 packages/@lwc/engine-core/src/framework/hydration.ts rename packages/@lwc/engine-core/src/framework/{hooks.ts => rendering.ts} (54%) diff --git a/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts b/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts index 7633baa194..27666c9934 100644 --- a/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts +++ b/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts @@ -35,7 +35,6 @@ export interface VBaseNode { elm: Node | undefined; text: string | undefined; key: Key | undefined; - // hook: Hooks; owner: VM; type: VNodeType; } @@ -91,12 +90,3 @@ export interface VNodeData { export interface VElementData extends VNodeData { key: Key; } - -// export interface Hooks { -// create: (vNode: N) => void; -// insert: (vNode: N, parentNode: Node, referenceNode: Node | null) => void; -// move: (vNode: N, parentNode: Node, referenceNode: Node | null) => void; -// update: (oldVNode: N, vNode: N) => void; -// remove: (vNode: N, parentNode: Node) => void; -// hydrate: (vNode: N, node: Node) => void; -// } diff --git a/packages/@lwc/engine-core/src/framework/api.ts b/packages/@lwc/engine-core/src/framework/api.ts index ff3123c025..64e518ccb3 100644 --- a/packages/@lwc/engine-core/src/framework/api.ts +++ b/packages/@lwc/engine-core/src/framework/api.ts @@ -38,17 +38,23 @@ import { VNodeType, } from '../3rdparty/snabbdom/types'; import { LightningElementConstructor } from './base-lightning-element'; -import { - markAsDynamicChildren, - // TextHook, - // CommentHook, - // ElementHook, - // CustomElementHook, -} from './hooks'; import { isComponentConstructor } from './def'; const SymbolIterator: typeof Symbol.iterator = Symbol.iterator; +// Using a WeakMap instead of a WeakSet because this one works in IE11 :( +const FromIteration: WeakMap = new WeakMap(); + +// dynamic children means it was generated by an iteration +// in a template, and will require a more complex diffing algo. +function markAsDynamicChildren(children: VNodes) { + FromIteration.set(children, 1); +} + +export function hasDynamicChildren(children: VNodes): boolean { + return FromIteration.has(children); +} + // [h]tml node function h(sel: string, data: VElementData, children: VNodes): VElement { const vmBeingRendered = getVMBeingRendered()!; @@ -102,7 +108,6 @@ function h(sel: string, data: VElementData, children: VNodes): VElement { text, elm, key, - // hook: ElementHook, owner: vmBeingRendered, }; } @@ -218,8 +223,6 @@ function c( text, elm, key, - - // hook: CustomElementHook, ctor: Ctor, owner: vmBeingRendered, mode: 'open', // TODO [#1294]: this should be defined in Ctor @@ -353,8 +356,6 @@ function t(text: string): VText { text, elm, key, - - // hook: TextHook, owner: getVMBeingRendered()!, }; } @@ -371,8 +372,6 @@ function co(text: string): VComment { text, elm, key, - - // hook: CommentHook, owner: getVMBeingRendered()!, }; } diff --git a/packages/@lwc/engine-core/src/framework/hydration.ts b/packages/@lwc/engine-core/src/framework/hydration.ts new file mode 100644 index 0000000000..55c1cbc633 --- /dev/null +++ b/packages/@lwc/engine-core/src/framework/hydration.ts @@ -0,0 +1,342 @@ +/* + * Copyright (c) 2018, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +import { ArrayFilter, ArrayJoin, assert, isUndefined, keys } from '@lwc/shared'; + +import { logError, logWarn } from '../shared/logger'; +import { + VBaseElement, + VComment, + VCustomElement, + VElement, + VNode, + VNodes, + VNodeType, + VText, +} from '../3rdparty/snabbdom/types'; + +import { parseStyleText } from './utils'; +import { getComponentInternalDef } from './def'; +import { allocateCustomElementChildren } from './rendering'; +import { patchProps } from './modules/props'; +import { applyEventListeners } from './modules/events'; +import { + createVM, + getAssociatedVM, + hydrateVM, + LWCDOMMode, + RenderMode, + runConnectedCallback, + VM, + VMState, +} from './vm'; + +function hydrate(vnode: VNode, node: Node) { + switch (vnode.type) { + case VNodeType.Text: + processText(vnode, node); + break; + + case VNodeType.Comment: + processComment(vnode, node); + break; + + case VNodeType.Element: + processElement(vnode, node); + break; + + case VNodeType.CustomElement: + processCustomElement(vnode, node); + break; + } +} + +function processText(vnode: VText, node: Node) { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line lwc-internal/no-global-node + if (node.nodeType !== Node.TEXT_NODE) { + logError('Hydration mismatch: incorrect node type received', vnode.owner); + assert.fail('Hydration mismatch: incorrect node type received.'); + } + + if (node.nodeValue !== vnode.text) { + logWarn( + 'Hydration mismatch: text values do not match, will recover from the difference', + vnode.owner + ); + } + } + + // always set the text value to the one from the vnode. + node.nodeValue = vnode.text ?? null; + vnode.elm = node; +} + +function processComment(vnode: VComment, node: Node) { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line lwc-internal/no-global-node + if (node.nodeType !== Node.COMMENT_NODE) { + logError('Hydration mismatch: incorrect node type received', vnode.owner); + assert.fail('Hydration mismatch: incorrect node type received.'); + } + + if (node.nodeValue !== vnode.text) { + logWarn( + 'Hydration mismatch: comment values do not match, will recover from the difference', + vnode.owner + ); + } + } + + // always set the text value to the one from the vnode. + node.nodeValue = vnode.text ?? null; + vnode.elm = node; +} + +function processElement(vnode: VElement, node: Node) { + const elm = node as Element; + vnode.elm = elm; + + const { context } = vnode.data; + const isDomManual = Boolean( + !isUndefined(context) && !isUndefined(context.lwc) && context.lwc.dom === LWCDOMMode.manual + ); + + if (isDomManual) { + // it may be that this element has lwc:inner-html, we need to diff and in case are the same, + // remove the innerHTML from props so it reuses the existing dom elements. + const { props } = vnode.data; + if (!isUndefined(props) && !isUndefined(props.innerHTML)) { + if (elm.innerHTML === props.innerHTML) { + delete props.innerHTML; + } else { + logWarn( + `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: innerHTML values do not match for element, will recover from the difference`, + vnode.owner + ); + } + } + } + + hydrateElementAttrsAndProps(vnode); + + if (!isDomManual) { + hydrateChildren(vnode.elm.childNodes, vnode.children, vnode.owner); + } +} + +function processCustomElement(vnode: VCustomElement, node: Node) { + const elm = node as Element; + + // the element is created, but the vm is not + const { sel, mode, ctor, owner } = vnode; + + const def = getComponentInternalDef(ctor); + createVM(elm, def, { + mode, + owner, + tagName: sel, + renderer: owner.renderer, + }); + + vnode.elm = elm as Element; + + const vm = getAssociatedVM(elm); + allocateCustomElementChildren(vnode, vm); + + hydrateElementAttrsAndProps(vnode); + + // Insert hook section: + if (process.env.NODE_ENV !== 'production') { + assert.isTrue(vm.state === VMState.created, `${vm} cannot be recycled.`); + } + runConnectedCallback(vm); + + if (vm.renderMode !== RenderMode.Light) { + // VM is not rendering in Light DOM, we can proceed and hydrate the slotted content. + // Note: for Light DOM, this is handled while hydrating the VM + hydrateChildren(vnode.elm.childNodes, vnode.children, vm); + } + + hydrateVM(vm); +} + +function hydrateElementAttrsAndProps(vnode: VBaseElement) { + applyEventListeners(vnode); + patchProps(null, vnode); +} + +export function hydrateChildren(elmChildren: NodeListOf, children: VNodes, vm?: VM) { + if (process.env.NODE_ENV !== 'production') { + const filteredVNodes = ArrayFilter.call(children, (vnode) => !!vnode); + if (elmChildren.length !== filteredVNodes.length) { + logError( + `Hydration mismatch: incorrect number of rendered nodes, expected ${filteredVNodes.length} but found ${elmChildren.length}.`, + vm + ); + throwHydrationError(); + } + } + let elmCurrentChildIdx = 0; + for (let j = 0, n = children.length; j < n; j++) { + const ch = children[j]; + if (ch != null) { + const childNode = elmChildren[elmCurrentChildIdx]; + if (process.env.NODE_ENV !== 'production') { + // VComments and VTexts validation is handled in their hooks + if (isElementNode(childNode)) { + if (ch.sel?.toLowerCase() !== childNode.tagName.toLowerCase()) { + logError( + `Hydration mismatch: expecting element with tag "${ch.sel?.toLowerCase()}" but found "${childNode.tagName.toLowerCase()}".`, + vm + ); + throwHydrationError(); + } + // Note: props are not yet set + const hasIncompatibleAttrs = vnodesAndElementHaveCompatibleAttrs(ch, childNode); + const hasIncompatibleClass = vnodesAndElementHaveCompatibleClass(ch, childNode); + const hasIncompatibleStyle = vnodesAndElementHaveCompatibleStyle(ch, childNode); + const isVNodeAndElementCompatible = + hasIncompatibleAttrs && hasIncompatibleClass && hasIncompatibleStyle; + if (!isVNodeAndElementCompatible) { + throwHydrationError(); + } + } + } + + hydrate(ch, childNode); + elmCurrentChildIdx++; + } + } +} + +function isElementNode(node: ChildNode): node is Element { + // eslint-disable-next-line lwc-internal/no-global-node + return node.nodeType === Node.ELEMENT_NODE; +} + +function vnodesAndElementHaveCompatibleAttrs(vnode: VNode, elm: Element): boolean { + const { + data: { attrs = {} }, + owner: { renderer }, + } = vnode; + + let nodesAreCompatible = true; + + // Validate attributes, though we could always recovery from those by running the update mods. + // Note: intentionally ONLY matching vnodes.attrs to elm.attrs, in case SSR is adding extra attributes. + for (const [attrName, attrValue] of Object.entries(attrs)) { + const elmAttrValue = renderer.getAttribute(elm, attrName); + if (String(attrValue) !== elmAttrValue) { + logError( + `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: attribute "${attrName}" has different values, expected "${attrValue}" but found "${elmAttrValue}"`, + vnode.owner + ); + nodesAreCompatible = false; + } + } + + return nodesAreCompatible; +} + +function vnodesAndElementHaveCompatibleClass(vnode: VNode, elm: Element): boolean { + const { + data: { className, classMap }, + owner: { renderer }, + } = vnode; + + let nodesAreCompatible = true; + let vnodeClassName; + + if (!isUndefined(className) && String(className) !== elm.className) { + // className is used when class is bound to an expr. + nodesAreCompatible = false; + vnodeClassName = className; + } else if (!isUndefined(classMap)) { + // classMap is used when class is set to static value. + const classList = renderer.getClassList(elm); + let computedClassName = ''; + + // all classes from the vnode should be in the element.classList + for (const name in classMap) { + computedClassName += ' ' + name; + if (!classList.contains(name)) { + nodesAreCompatible = false; + } + } + + vnodeClassName = computedClassName.trim(); + + if (classList.length > keys(classMap).length) { + nodesAreCompatible = false; + } + } + + if (!nodesAreCompatible) { + logError( + `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: attribute "class" has different values, expected "${vnodeClassName}" but found "${ + elm.className + }"`, + vnode.owner + ); + } + + return nodesAreCompatible; +} + +function vnodesAndElementHaveCompatibleStyle(vnode: VNode, elm: Element): boolean { + const { + data: { style, styleDecls }, + owner: { renderer }, + } = vnode; + const elmStyle = renderer.getAttribute(elm, 'style') || ''; + let vnodeStyle; + let nodesAreCompatible = true; + + if (!isUndefined(style) && style !== elmStyle) { + nodesAreCompatible = false; + vnodeStyle = style; + } else if (!isUndefined(styleDecls)) { + const parsedVnodeStyle = parseStyleText(elmStyle); + const expectedStyle = []; + // styleMap is used when style is set to static value. + for (let i = 0, n = styleDecls.length; i < n; i++) { + const [prop, value, important] = styleDecls[i]; + expectedStyle.push(`${prop}: ${value + (important ? ' important!' : '')}`); + + const parsedPropValue = parsedVnodeStyle[prop]; + + if (isUndefined(parsedPropValue)) { + nodesAreCompatible = false; + } else if (!parsedPropValue.startsWith(value)) { + nodesAreCompatible = false; + } else if (important && !parsedPropValue.endsWith('!important')) { + nodesAreCompatible = false; + } + } + + if (keys(parsedVnodeStyle).length > styleDecls.length) { + nodesAreCompatible = false; + } + + vnodeStyle = ArrayJoin.call(expectedStyle, ';'); + } + + if (!nodesAreCompatible) { + // style is used when class is bound to an expr. + logError( + `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: attribute "style" has different values, expected "${vnodeStyle}" but found "${elmStyle}".`, + vnode.owner + ); + } + + return nodesAreCompatible; +} + +function throwHydrationError() { + assert.fail('Server rendered elements do not match client side generated elements'); +} diff --git a/packages/@lwc/engine-core/src/framework/hooks.ts b/packages/@lwc/engine-core/src/framework/rendering.ts similarity index 54% rename from packages/@lwc/engine-core/src/framework/hooks.ts rename to packages/@lwc/engine-core/src/framework/rendering.ts index 48850c8aab..0d8a27970d 100644 --- a/packages/@lwc/engine-core/src/framework/hooks.ts +++ b/packages/@lwc/engine-core/src/framework/rendering.ts @@ -17,10 +17,7 @@ import { keys, KEY__SHADOW_RESOLVER, } from '@lwc/shared'; -import { - EmptyArray, - // parseStyleText -} from './utils'; +import { EmptyArray } from './utils'; import { createVM, getAssociatedVMIfPresent, @@ -32,9 +29,8 @@ import { runConnectedCallback, appendVM, removeVM, - // getAssociatedVM, - // hydrateVM, getRenderRoot, + LWCDOMMode, } from './vm'; import { VNode, @@ -42,10 +38,8 @@ import { VElement, VNodes, Key, - // Hooks, VText, VComment, - // VParentElement, VBaseElement, VNodeType, } from '../3rdparty/snabbdom/types'; @@ -58,15 +52,11 @@ import { patchStyleAttribute } from './modules/computed-style-attr'; import { applyStaticClassAttribute } from './modules/static-class-attr'; import { applyStaticStyleAttribute } from './modules/static-style-attr'; -import { patchElementWithRestrictions, unlockDomMutation, lockDomMutation } from './restrictions'; +import { hasDynamicChildren } from './api'; import { getComponentInternalDef } from './def'; -// import { logError, logWarn } from '../shared/logger'; import { markComponentAsDirty } from './component'; import { getUpgradableConstructor } from './upgradable-element'; - -const enum LWCDOMMode { - manual = 'manual', -} +import { patchElementWithRestrictions, unlockDomMutation, lockDomMutation } from './restrictions'; interface KeyToIndexMap { [key: string]: number; @@ -74,6 +64,14 @@ interface KeyToIndexMap { const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; +export function patchChildren(parentElm: ParentNode, oldCh: VNodes, newCh: VNodes): void { + if (hasDynamicChildren(newCh)) { + updateDynamicChildren(parentElm, oldCh, newCh); + } else { + updateStaticChildren(parentElm, oldCh, newCh); + } +} + function patch(n1: VNode | null, n2: VNode, parent: ParentNode, anchor: Node | null) { if (n1 === n2) { return; @@ -207,7 +205,7 @@ function mountCustomElement(vnode: VCustomElement, parent: ParentNode, anchor: N const vm = getAssociatedVMIfPresent(elm); if (!isUndefined(vm)) { - allocateChildrenHook(vnode, vm); + allocateCustomElementChildren(vnode, vm); } else if (vnode.ctor !== UpgradableConstructor) { throw new TypeError(`Incorrect Component Constructor`); } @@ -239,7 +237,7 @@ function patchCustomElement(n1: VCustomElement, n2: VCustomElement) { if (!isUndefined(vm)) { // in fallback mode, the allocation will always set children to // empty and delegate the real allocation to the slot elements - allocateChildrenHook(n2, vm); + allocateCustomElementChildren(n2, vm); } // in fallback mode, the children will be always empty, so, nothing @@ -289,260 +287,6 @@ function unmountChildren(children: VNodes, parent: ParentNode) { } } -// export const TextHook: Hooks = { -// create: (vnode) => { -// const { owner } = vnode; -// const { renderer } = owner; - -// const elm = renderer.createText(vnode.text!); -// linkNodeToShadow(elm, owner); -// vnode.elm = elm; -// }, -// update: updateNodeHook, -// insert: insertNodeHook, -// move: insertNodeHook, // same as insert for text nodes -// remove: removeNodeHook, -// hydrate: (vNode: VNode, node: Node) => { -// if (process.env.NODE_ENV !== 'production') { -// // eslint-disable-next-line lwc-internal/no-global-node -// if (node.nodeType !== Node.TEXT_NODE) { -// logError('Hydration mismatch: incorrect node type received', vNode.owner); -// assert.fail('Hydration mismatch: incorrect node type received.'); -// } - -// if (node.nodeValue !== vNode.text) { -// logWarn( -// 'Hydration mismatch: text values do not match, will recover from the difference', -// vNode.owner -// ); -// } -// } - -// // always set the text value to the one from the vnode. -// node.nodeValue = vNode.text ?? null; -// vNode.elm = node; -// }, -// }; - -// export const CommentHook: Hooks = { -// create: (vnode) => { -// const { owner, text } = vnode; -// const { renderer } = owner; - -// const elm = renderer.createComment(text); -// linkNodeToShadow(elm, owner); -// vnode.elm = elm; -// }, -// update: updateNodeHook, -// insert: insertNodeHook, -// move: insertNodeHook, // same as insert for text nodes -// remove: removeNodeHook, -// hydrate: (vNode: VNode, node: Node) => { -// if (process.env.NODE_ENV !== 'production') { -// // eslint-disable-next-line lwc-internal/no-global-node -// if (node.nodeType !== Node.COMMENT_NODE) { -// logError('Hydration mismatch: incorrect node type received', vNode.owner); -// assert.fail('Hydration mismatch: incorrect node type received.'); -// } - -// if (node.nodeValue !== vNode.text) { -// logWarn( -// 'Hydration mismatch: comment values do not match, will recover from the difference', -// vNode.owner -// ); -// } -// } - -// // always set the text value to the one from the vnode. -// node.nodeValue = vNode.text ?? null; -// vNode.elm = node; -// }, -// }; - -// // insert is called after update, which is used somewhere else (via a module) -// // to mark the vm as inserted, that means we cannot use update as the main channel -// // to rehydrate when dirty, because sometimes the element is not inserted just yet, -// // which breaks some invariants. For that reason, we have the following for any -// // Custom Element that is inserted via a template. -// export const ElementHook: Hooks = { -// create: (vnode) => { -// const { -// sel, -// owner, -// data: { svg }, -// } = vnode; -// const { renderer } = owner; - -// const namespace = isTrue(svg) ? SVG_NAMESPACE : undefined; -// const elm = renderer.createElement(sel, namespace); - -// linkNodeToShadow(elm, owner); -// fallbackElmHook(elm, vnode); -// vnode.elm = elm; - -// patchElementAttrsAndProps(null, vnode); -// }, -// update: (oldVnode, vnode) => { -// patchElementAttrsAndProps(oldVnode, vnode); -// patchChildren(vnode.elm!, oldVnode.children, vnode.children); -// }, -// insert: (vnode, parentNode, referenceNode) => { -// insertNodeHook(vnode, parentNode, referenceNode); -// mountChildren(vnode); -// }, -// move: (vnode, parentNode, referenceNode) => { -// insertNodeHook(vnode, parentNode, referenceNode); -// }, -// remove: (vnode, parentNode) => { -// removeNodeHook(vnode, parentNode); -// removeElmHook(vnode); -// }, -// hydrate: (vnode, node) => { -// const elm = node as Element; -// vnode.elm = elm; - -// const { context } = vnode.data; -// const isDomManual = Boolean( -// !isUndefined(context) && -// !isUndefined(context.lwc) && -// context.lwc.dom === LWCDOMMode.manual -// ); - -// if (isDomManual) { -// // it may be that this element has lwc:inner-html, we need to diff and in case are the same, -// // remove the innerHTML from props so it reuses the existing dom elements. -// const { props } = vnode.data; -// if (!isUndefined(props) && !isUndefined(props.innerHTML)) { -// if (elm.innerHTML === props.innerHTML) { -// delete props.innerHTML; -// } else { -// logWarn( -// `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: innerHTML values do not match for element, will recover from the difference`, -// vnode.owner -// ); -// } -// } -// } - -// hydrateElmHook(vnode); - -// if (!isDomManual) { -// hydrateChildrenHook(vnode.elm.childNodes, vnode.children, vnode.owner); -// } -// }, -// }; - -// export const CustomElementHook: Hooks = { -// create: (vnode) => { -// const { sel, owner } = vnode; -// const { renderer } = owner; -// const UpgradableConstructor = getUpgradableConstructor(sel, renderer); -// /** -// * Note: if the upgradable constructor does not expect, or throw when we new it -// * with a callback as the first argument, we could implement a more advanced -// * mechanism that only passes that argument if the constructor is known to be -// * an upgradable custom element. -// */ -// const elm = new UpgradableConstructor((elm: HTMLElement) => { -// // the custom element from the registry is expecting an upgrade callback -// createViewModelHook(elm, vnode); -// }); - -// linkNodeToShadow(elm, owner); -// vnode.elm = elm; - -// const vm = getAssociatedVMIfPresent(elm); -// if (vm) { -// allocateChildrenHook(vnode, vm); -// } else if (vnode.ctor !== UpgradableConstructor) { -// throw new TypeError(`Incorrect Component Constructor`); -// } -// patchElementAttrsAndProps(null, vnode); -// }, -// update: (oldVnode, vnode) => { -// patchElementAttrsAndProps(oldVnode, vnode); -// const vm = getAssociatedVMIfPresent(vnode.elm); -// if (vm) { -// // in fallback mode, the allocation will always set children to -// // empty and delegate the real allocation to the slot elements -// allocateChildrenHook(vnode, vm); -// } -// // in fallback mode, the children will be always empty, so, nothing -// // will happen, but in native, it does allocate the light dom -// patchChildren(vnode.elm!, oldVnode.children, vnode.children); -// if (vm) { -// if (process.env.NODE_ENV !== 'production') { -// assert.isTrue( -// isArray(vnode.children), -// `Invalid vnode for a custom element, it must have children defined.` -// ); -// } -// // this will probably update the shadowRoot, but only if the vm is in a dirty state -// // this is important to preserve the top to bottom synchronous rendering phase. -// rerenderVM(vm); -// } -// }, -// insert: (vnode, parentNode, referenceNode) => { -// insertNodeHook(vnode, parentNode, referenceNode); -// const vm = getAssociatedVMIfPresent(vnode.elm); -// if (vm) { -// if (process.env.NODE_ENV !== 'production') { -// assert.isTrue(vm.state === VMState.created, `${vm} cannot be recycled.`); -// } -// runConnectedCallback(vm); -// } -// mountChildren(vnode); -// if (vm) { -// appendVM(vm); -// } -// }, -// move: (vnode, parentNode, referenceNode) => { -// insertNodeHook(vnode, parentNode, referenceNode); -// }, -// remove: (vnode, parentNode) => { -// removeNodeHook(vnode, parentNode); -// const vm = getAssociatedVMIfPresent(vnode.elm); -// if (vm) { -// // for custom elements we don't have to go recursively because the removeVM routine -// // will take care of disconnecting any child VM attached to its shadow as well. -// removeVM(vm); -// } -// }, -// hydrate: (vnode, elm) => { -// // the element is created, but the vm is not -// const { sel, mode, ctor, owner } = vnode; - -// const def = getComponentInternalDef(ctor); -// createVM(elm, def, { -// mode, -// owner, -// tagName: sel, -// renderer: owner.renderer, -// }); - -// vnode.elm = elm as Element; - -// const vm = getAssociatedVM(elm); -// allocateChildrenHook(vnode, vm); - -// hydrateElmHook(vnode); - -// // Insert hook section: -// if (process.env.NODE_ENV !== 'production') { -// assert.isTrue(vm.state === VMState.created, `${vm} cannot be recycled.`); -// } -// runConnectedCallback(vm); - -// if (vm.renderMode !== RenderMode.Light) { -// // VM is not rendering in Light DOM, we can proceed and hydrate the slotted content. -// // Note: for Light DOM, this is handled while hydrating the VM -// hydrateChildrenHook(vnode.elm.childNodes, vnode.children, vm); -// } - -// hydrateVM(vm); -// }, -// }; - function linkNodeToShadow(elm: Node, owner: VM) { const { renderer, renderMode, shadowMode } = owner; @@ -628,18 +372,6 @@ function patchElementAttrsAndProps(oldVnode: VBaseElement | null, vnode: VBaseEl patchStyleAttribute(oldVnode, vnode); } -// function hydrateElmHook(vnode: VBaseElement) { -// applyEventListeners(vnode); -// // Attrs are already on the element. -// // modAttrs.create(vnode); -// patchProps(null, vnode); -// // Already set. -// // applyStaticClassAttribute(vnode); -// // applyStaticStyleAttribute(vnode); -// // modComputedClassName.create(vnode); -// // modComputedStyle.create(vnode); -// } - function fallbackElmHook(elm: Element, vnode: VElement) { const { owner } = vnode; setScopeTokenClassIfNecessary(elm, owner); @@ -673,7 +405,8 @@ function fallbackElmHook(elm: Element, vnode: VElement) { } } -function allocateChildrenHook(vnode: VCustomElement, vm: VM) { +// FIXME: This function shouldn't be exported. +export function allocateCustomElementChildren(vnode: VCustomElement, vm: VM) { // A component with slots will re-render because: // 1- There is a change of the internal state. // 2- There is a change on the external api (ex: slots) @@ -699,6 +432,52 @@ function allocateChildrenHook(vnode: VCustomElement, vm: VM) { } } +function allocateInSlot(vm: VM, children: VNodes) { + const { cmpSlots: oldSlots } = vm; + const cmpSlots = (vm.cmpSlots = create(null)); + for (let i = 0, len = children.length; i < len; i += 1) { + const vnode = children[i]; + if (isNull(vnode)) { + continue; + } + const { data } = vnode; + const slotName = (data.attrs?.slot ?? '') as string; + const vnodes = (cmpSlots[slotName] = cmpSlots[slotName] || []); + // re-keying the vnodes is necessary to avoid conflicts with default content for the slot + // which might have similar keys. Each vnode will always have a key that + // starts with a numeric character from compiler. In this case, we add a unique + // notation for slotted vnodes keys, e.g.: `@foo:1:1` + if (!isUndefined(vnode.key)) { + vnode.key = `@${slotName}:${vnode.key}`; + } + ArrayPush.call(vnodes, vnode); + } + if (!vm.isDirty) { + // We need to determine if the old allocation is really different from the new one + // and mark the vm as dirty + const oldKeys = keys(oldSlots); + if (oldKeys.length !== keys(cmpSlots).length) { + markComponentAsDirty(vm); + return; + } + for (let i = 0, len = oldKeys.length; i < len; i += 1) { + const key = oldKeys[i]; + if (isUndefined(cmpSlots[key]) || oldSlots[key].length !== cmpSlots[key].length) { + markComponentAsDirty(vm); + return; + } + const oldVNodes = oldSlots[key]; + const vnodes = cmpSlots[key]; + for (let j = 0, a = cmpSlots[key].length; j < a; j += 1) { + if (oldVNodes[j] !== vnodes[j]) { + markComponentAsDirty(vm); + return; + } + } + } + } +} + function createViewModelHook(elm: HTMLElement, vnode: VCustomElement) { if (!isUndefined(getAssociatedVMIfPresent(elm))) { // There is a possibility that a custom element is registered under tagName, @@ -734,199 +513,10 @@ function mountChildren(children: VNodes, parent: Element) { const child = children[i]; if (child != null) { patch(null, child, parent, null); - // ch.hook.create(ch); - // ch.hook.insert(ch, elm!, null); } } } -// function isElementNode(node: ChildNode): node is Element { -// // eslint-disable-next-line lwc-internal/no-global-node -// return node.nodeType === Node.ELEMENT_NODE; -// } - -// function vnodesAndElementHaveCompatibleAttrs(vnode: VNode, elm: Element): boolean { -// const { -// data: { attrs = {} }, -// owner: { renderer }, -// } = vnode; - -// let nodesAreCompatible = true; - -// // Validate attributes, though we could always recovery from those by running the update mods. -// // Note: intentionally ONLY matching vnodes.attrs to elm.attrs, in case SSR is adding extra attributes. -// for (const [attrName, attrValue] of Object.entries(attrs)) { -// const elmAttrValue = renderer.getAttribute(elm, attrName); -// if (String(attrValue) !== elmAttrValue) { -// logError( -// `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: attribute "${attrName}" has different values, expected "${attrValue}" but found "${elmAttrValue}"`, -// vnode.owner -// ); -// nodesAreCompatible = false; -// } -// } - -// return nodesAreCompatible; -// } - -// function vnodesAndElementHaveCompatibleClass(vnode: VNode, elm: Element): boolean { -// const { -// data: { className, classMap }, -// owner: { renderer }, -// } = vnode; - -// let nodesAreCompatible = true; -// let vnodeClassName; - -// if (!isUndefined(className) && String(className) !== elm.className) { -// // className is used when class is bound to an expr. -// nodesAreCompatible = false; -// vnodeClassName = className; -// } else if (!isUndefined(classMap)) { -// // classMap is used when class is set to static value. -// const classList = renderer.getClassList(elm); -// let computedClassName = ''; - -// // all classes from the vnode should be in the element.classList -// for (const name in classMap) { -// computedClassName += ' ' + name; -// if (!classList.contains(name)) { -// nodesAreCompatible = false; -// } -// } - -// vnodeClassName = computedClassName.trim(); - -// if (classList.length > keys(classMap).length) { -// nodesAreCompatible = false; -// } -// } - -// if (!nodesAreCompatible) { -// logError( -// `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: attribute "class" has different values, expected "${vnodeClassName}" but found "${ -// elm.className -// }"`, -// vnode.owner -// ); -// } - -// return nodesAreCompatible; -// } - -// function vnodesAndElementHaveCompatibleStyle(vnode: VNode, elm: Element): boolean { -// const { -// data: { style, styleDecls }, -// owner: { renderer }, -// } = vnode; -// const elmStyle = renderer.getAttribute(elm, 'style') || ''; -// let vnodeStyle; -// let nodesAreCompatible = true; - -// if (!isUndefined(style) && style !== elmStyle) { -// nodesAreCompatible = false; -// vnodeStyle = style; -// } else if (!isUndefined(styleDecls)) { -// const parsedVnodeStyle = parseStyleText(elmStyle); -// const expectedStyle = []; -// // styleMap is used when style is set to static value. -// for (let i = 0, n = styleDecls.length; i < n; i++) { -// const [prop, value, important] = styleDecls[i]; -// expectedStyle.push(`${prop}: ${value + (important ? ' important!' : '')}`); - -// const parsedPropValue = parsedVnodeStyle[prop]; - -// if (isUndefined(parsedPropValue)) { -// nodesAreCompatible = false; -// } else if (!parsedPropValue.startsWith(value)) { -// nodesAreCompatible = false; -// } else if (important && !parsedPropValue.endsWith('!important')) { -// nodesAreCompatible = false; -// } -// } - -// if (keys(parsedVnodeStyle).length > styleDecls.length) { -// nodesAreCompatible = false; -// } - -// vnodeStyle = ArrayJoin.call(expectedStyle, ';'); -// } - -// if (!nodesAreCompatible) { -// // style is used when class is bound to an expr. -// logError( -// `Mismatch hydrating element <${elm.tagName.toLowerCase()}>: attribute "style" has different values, expected "${vnodeStyle}" but found "${elmStyle}".`, -// vnode.owner -// ); -// } - -// return nodesAreCompatible; -// } - -// function throwHydrationError() { -// assert.fail('Server rendered elements do not match client side generated elements'); -// } - -export function hydrateChildrenHook( - _elmChildren: NodeListOf, - _children: VNodes, - _vm?: VM -) { - // if (process.env.NODE_ENV !== 'production') { - // const filteredVNodes = ArrayFilter.call(children, (vnode) => !!vnode); - // if (elmChildren.length !== filteredVNodes.length) { - // logError( - // `Hydration mismatch: incorrect number of rendered nodes, expected ${filteredVNodes.length} but found ${elmChildren.length}.`, - // vm - // ); - // throwHydrationError(); - // } - // } - // let elmCurrentChildIdx = 0; - // for (let j = 0, n = children.length; j < n; j++) { - // const ch = children[j]; - // if (ch != null) { - // const childNode = elmChildren[elmCurrentChildIdx]; - // if (process.env.NODE_ENV !== 'production') { - // // VComments and VTexts validation is handled in their hooks - // if (isElementNode(childNode)) { - // if (ch.sel?.toLowerCase() !== childNode.tagName.toLowerCase()) { - // logError( - // `Hydration mismatch: expecting element with tag "${ch.sel?.toLowerCase()}" but found "${childNode.tagName.toLowerCase()}".`, - // vm - // ); - // throwHydrationError(); - // } - // // Note: props are not yet set - // const hasIncompatibleAttrs = vnodesAndElementHaveCompatibleAttrs(ch, childNode); - // const hasIncompatibleClass = vnodesAndElementHaveCompatibleClass(ch, childNode); - // const hasIncompatibleStyle = vnodesAndElementHaveCompatibleStyle(ch, childNode); - // const isVNodeAndElementCompatible = - // hasIncompatibleAttrs && hasIncompatibleClass && hasIncompatibleStyle; - // if (!isVNodeAndElementCompatible) { - // throwHydrationError(); - // } - // } - // } - // ch.hook.hydrate(ch, childNode); - // elmCurrentChildIdx++; - // } - // } -} - -// function removeElmHook(vnode: VElement) { -// // this method only needs to search on child vnodes from template -// // to trigger the remove hook just in case some of those children -// // are custom elements. -// const { children, elm } = vnode; -// for (let j = 0, len = children.length; j < len; ++j) { -// const ch = children[j]; -// if (!isNull(ch)) { -// ch.hook.remove(ch, elm!); -// } -// } -// } - function sameVnode(vnode1: VNode, vnode2: VNode): boolean { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; } @@ -962,8 +552,6 @@ function addVnodes( const ch = vnodes[startIdx]; if (isVNode(ch)) { patch(null, ch, parentElm, before); - // ch.hook.create(ch); - // ch.hook.insert(ch, parentElm, before); } } } @@ -979,7 +567,6 @@ function removeVnodes( // text nodes do not have logic associated to them if (isVNode(ch)) { unmount(ch, parentElm); - // ch.hook.remove(ch, parentElm); } } } @@ -1113,80 +700,3 @@ function updateStaticChildren(parentElm: ParentNode, oldCh: VNodes, newCh: VNode } } } - -export function patchChildren(parentElm: ParentNode, oldCh: VNodes, newCh: VNodes): void { - if (hasDynamicChildren(newCh)) { - updateDynamicChildren(parentElm, oldCh, newCh); - } else { - updateStaticChildren(parentElm, oldCh, newCh); - } -} - -// function patchVnode(oldVnode: VNode, vnode: VNode) { -// if (oldVnode !== vnode) { -// vnode.elm = oldVnode.elm; -// vnode.hook.update(oldVnode, vnode); -// } -// } - -// slow path routine -// NOTE: we should probably more this routine to the synthetic shadow folder -// and get the allocation to be cached by in the elm instead of in the VM -function allocateInSlot(vm: VM, children: VNodes) { - const { cmpSlots: oldSlots } = vm; - const cmpSlots = (vm.cmpSlots = create(null)); - for (let i = 0, len = children.length; i < len; i += 1) { - const vnode = children[i]; - if (isNull(vnode)) { - continue; - } - const { data } = vnode; - const slotName = (data.attrs?.slot ?? '') as string; - const vnodes = (cmpSlots[slotName] = cmpSlots[slotName] || []); - // re-keying the vnodes is necessary to avoid conflicts with default content for the slot - // which might have similar keys. Each vnode will always have a key that - // starts with a numeric character from compiler. In this case, we add a unique - // notation for slotted vnodes keys, e.g.: `@foo:1:1` - if (!isUndefined(vnode.key)) { - vnode.key = `@${slotName}:${vnode.key}`; - } - ArrayPush.call(vnodes, vnode); - } - if (!vm.isDirty) { - // We need to determine if the old allocation is really different from the new one - // and mark the vm as dirty - const oldKeys = keys(oldSlots); - if (oldKeys.length !== keys(cmpSlots).length) { - markComponentAsDirty(vm); - return; - } - for (let i = 0, len = oldKeys.length; i < len; i += 1) { - const key = oldKeys[i]; - if (isUndefined(cmpSlots[key]) || oldSlots[key].length !== cmpSlots[key].length) { - markComponentAsDirty(vm); - return; - } - const oldVNodes = oldSlots[key]; - const vnodes = cmpSlots[key]; - for (let j = 0, a = cmpSlots[key].length; j < a; j += 1) { - if (oldVNodes[j] !== vnodes[j]) { - markComponentAsDirty(vm); - return; - } - } - } - } -} - -// Using a WeakMap instead of a WeakSet because this one works in IE11 :( -const FromIteration: WeakMap = new WeakMap(); - -// dynamic children means it was generated by an iteration -// in a template, and will require a more complex diffing algo. -export function markAsDynamicChildren(children: VNodes) { - FromIteration.set(children, 1); -} - -function hasDynamicChildren(children: VNodes): boolean { - return FromIteration.has(children); -} diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index 93e8a64738..7ff15dcedc 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -33,7 +33,8 @@ import { logGlobalOperationEnd, logGlobalOperationStart, } from './profiler'; -import { hydrateChildrenHook, patchChildren } from './hooks'; +import { patchChildren } from './rendering'; +import { hydrateChildren } from './hydration'; import { ReactiveObserver } from './mutation-tracker'; import { connectWireAdapters, disconnectWireAdapters, installWireAdapters } from './wiring'; import { AccessorReactiveObserver } from './decorators/api'; @@ -74,6 +75,10 @@ export const enum ShadowSupportMode { Default = 'reset', } +export const enum LWCDOMMode { + manual = 'manual', +} + export interface Context { /** The string used for synthetic shadow DOM and light DOM style scoping. */ stylesheetToken: string | undefined; @@ -429,7 +434,7 @@ function hydrate(vm: VM) { const vmChildren = vm.renderMode === RenderMode.Light ? vm.elm.childNodes : vm.elm.shadowRoot.childNodes; - hydrateChildrenHook(vmChildren, children, vm); + hydrateChildren(vmChildren, children, vm); runRenderedCallback(vm); } From d923dfda400c230cd2eec9c4806903a239f03ae7 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Thu, 16 Dec 2021 06:41:20 +0100 Subject: [PATCH 09/17] chore: fix comment diffing --- packages/@lwc/engine-core/src/framework/rendering.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/@lwc/engine-core/src/framework/rendering.ts b/packages/@lwc/engine-core/src/framework/rendering.ts index 0d8a27970d..893f384b35 100644 --- a/packages/@lwc/engine-core/src/framework/rendering.ts +++ b/packages/@lwc/engine-core/src/framework/rendering.ts @@ -132,8 +132,13 @@ function processComment(n1: VComment, n2: VComment, parent: ParentNode, anchor: insertNodeHook(n2, parent, anchor); } else { - // No need to patch the comment text, as it is static. n2.elm = n1.elm; + + // FIXME: Comment nodes should be static, we shouldn't need to diff them together. However + // it is the case today. + if (n2.text !== n1.text) { + updateNodeHook(n1, n2); + } } } From fa79c488b6e0afe89018792619928d3915672dd3 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Thu, 16 Dec 2021 07:16:16 +0100 Subject: [PATCH 10/17] chore: simpify stylesheet injection --- .../engine-core/src/framework/rendering.ts | 198 +++++++----------- 1 file changed, 79 insertions(+), 119 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/rendering.ts b/packages/@lwc/engine-core/src/framework/rendering.ts index 893f384b35..63b004f9dd 100644 --- a/packages/@lwc/engine-core/src/framework/rendering.ts +++ b/packages/@lwc/engine-core/src/framework/rendering.ts @@ -111,13 +111,12 @@ function processText(n1: VText, n2: VText, parent: ParentNode, anchor: Node | nu const textNode = (n2.elm = renderer.createText(n2.text)); linkNodeToShadow(textNode, owner); - insertNodeHook(n2, parent, anchor); + insertVNode(n2, parent, anchor); } else { n2.elm = n1.elm; - // FIXME: We shouldn't need compare the node content in `updateNodeHook`. if (n2.text !== n1.text) { - updateNodeHook(n1, n2); + updateTextContent(n2); } } } @@ -130,14 +129,14 @@ function processComment(n1: VComment, n2: VComment, parent: ParentNode, anchor: const textNode = (n2.elm = renderer.createComment(n2.text)); linkNodeToShadow(textNode, owner); - insertNodeHook(n2, parent, anchor); + insertVNode(n2, parent, anchor); } else { n2.elm = n1.elm; // FIXME: Comment nodes should be static, we shouldn't need to diff them together. However // it is the case today. if (n2.text !== n1.text) { - updateNodeHook(n1, n2); + updateTextContent(n2); } } } @@ -151,22 +150,32 @@ function processElement(n1: VElement, n2: VElement, parent: ParentNode, anchor: } function mountElement(vnode: VElement, parent: ParentNode, anchor: Node | null) { - const { - sel, - owner, - data: { svg }, - } = vnode; + const { sel, owner, data } = vnode; const { renderer } = owner; - const namespace = isTrue(svg) ? SVG_NAMESPACE : undefined; + const namespace = isTrue(data.svg) ? SVG_NAMESPACE : undefined; const elm = (vnode.elm = renderer.createElement(sel, namespace)); linkNodeToShadow(elm, owner); - fallbackElmHook(elm, vnode); + // Handle dom:manual template directive. + const isDomManual = data.context?.lwc?.dom === LWCDOMMode.manual; + if (isDomManual) { + elm.$domManual$ = true; + } + + applyStyleScoping(elm, vnode); patchElementAttrsAndProps(null, vnode); - insertNodeHook(vnode, parent, anchor); + // Apply guardrails to the element in dev mode. + if (process.env.NODE_ENV !== 'production') { + patchElementWithRestrictions(elm, { + isPortal: isDomManual, + isLight: owner.renderMode === RenderMode.Light, + }); + } + // Insert node and recursively mount the children tree. + insertVNode(vnode, parent, anchor); mountChildren(vnode.children, elm); } @@ -215,9 +224,10 @@ function mountCustomElement(vnode: VCustomElement, parent: ParentNode, anchor: N throw new TypeError(`Incorrect Component Constructor`); } + applyStyleScoping(elm, vnode); patchElementAttrsAndProps(null, vnode); - insertNodeHook(vnode, parent, anchor); + insertVNode(vnode, parent, anchor); if (!isUndefined(vm)) { if (process.env.NODE_ENV !== 'production') { @@ -263,7 +273,7 @@ function patchCustomElement(n1: VCustomElement, n2: VCustomElement) { } function unmount(vnode: VNode, parent: ParentNode) { - removeNodeHook(vnode, parent); + removeVNode(vnode, parent); switch (vnode.type) { case VNodeType.Element: @@ -303,65 +313,6 @@ function linkNodeToShadow(elm: Node, owner: VM) { } } -function observeElementChildNodes(elm: Element) { - (elm as any).$domManual$ = true; -} - -function setElementShadowToken(elm: Element, token: string | undefined) { - (elm as any).$shadowToken$ = token; -} - -// Set the scope token class for *.scoped.css styles -function setScopeTokenClassIfNecessary(elm: Element, owner: VM) { - const { cmpTemplate, context } = owner; - const token = cmpTemplate?.stylesheetToken; - if (!isUndefined(token) && context.hasScopedStyles) { - owner.renderer.getClassList(elm).add(token); - } -} - -function updateNodeHook(oldVnode: VNode, vnode: VNode) { - const { - elm, - text, - owner: { renderer }, - } = vnode; - - if (oldVnode.text !== text) { - if (process.env.NODE_ENV !== 'production') { - unlockDomMutation(); - } - renderer.setText(elm, text!); - if (process.env.NODE_ENV !== 'production') { - lockDomMutation(); - } - } -} - -function insertNodeHook(vnode: VNode, parentNode: Node, referenceNode: Node | null) { - const { renderer } = vnode.owner; - - if (process.env.NODE_ENV !== 'production') { - unlockDomMutation(); - } - renderer.insert(vnode.elm!, parentNode, referenceNode); - if (process.env.NODE_ENV !== 'production') { - lockDomMutation(); - } -} - -function removeNodeHook(vnode: VNode, parentNode: Node) { - const { renderer } = vnode.owner; - - if (process.env.NODE_ENV !== 'production') { - unlockDomMutation(); - } - renderer.remove(vnode.elm!, parentNode); - if (process.env.NODE_ENV !== 'production') { - lockDomMutation(); - } -} - function patchElementAttrsAndProps(oldVnode: VBaseElement | null, vnode: VBaseElement) { if (isNull(oldVnode)) { applyEventListeners(vnode); @@ -377,36 +328,18 @@ function patchElementAttrsAndProps(oldVnode: VBaseElement | null, vnode: VBaseEl patchStyleAttribute(oldVnode, vnode); } -function fallbackElmHook(elm: Element, vnode: VElement) { - const { owner } = vnode; - setScopeTokenClassIfNecessary(elm, owner); - if (owner.shadowMode === ShadowMode.Synthetic) { - const { - data: { context }, - } = vnode; - const { stylesheetToken } = owner.context; - if ( - !isUndefined(context) && - !isUndefined(context.lwc) && - context.lwc.dom === LWCDOMMode.manual - ) { - // this element will now accept any manual content inserted into it - observeElementChildNodes(elm); - } - // when running in synthetic shadow mode, we need to set the shadowToken value - // into each element from the template, so they can be styled accordingly. - setElementShadowToken(elm, stylesheetToken); +function applyStyleScoping(elm: Element, vnode: VBaseElement) { + const { cmpTemplate, context, renderer, shadowMode } = vnode.owner; + + // Apply synthetic shadow dom style scoping. + if (shadowMode === ShadowMode.Synthetic) { + (elm as any).$shadowToken$ = context.stylesheetToken; } - if (process.env.NODE_ENV !== 'production') { - const { - data: { context }, - } = vnode; - const isPortal = - !isUndefined(context) && - !isUndefined(context.lwc) && - context.lwc.dom === LWCDOMMode.manual; - const isLight = owner.renderMode === RenderMode.Light; - patchElementWithRestrictions(elm, { isPortal, isLight }); + + // Apply light DOM style scoping. + const scopedStyleToken = cmpTemplate!.stylesheetToken; + if (context.hasScopedStyles && !isUndefined(scopedStyleToken)) { + renderer.getClassList(elm).add(scopedStyleToken); } } @@ -491,13 +424,6 @@ function createViewModelHook(elm: HTMLElement, vnode: VCustomElement) { return; } const { sel, mode, ctor, owner } = vnode; - setScopeTokenClassIfNecessary(elm, owner); - if (owner.shadowMode === ShadowMode.Synthetic) { - const { stylesheetToken } = owner.context; - // when running in synthetic shadow mode, we need to set the shadowToken value - // into each element from the template, so they can be styled accordingly. - setElementShadowToken(elm, stylesheetToken); - } const def = getComponentInternalDef(ctor); createVM(elm, def, { mode, @@ -505,12 +431,6 @@ function createViewModelHook(elm: HTMLElement, vnode: VCustomElement) { tagName: sel, renderer: owner.renderer, }); - if (process.env.NODE_ENV !== 'production') { - assert.isTrue( - isArray(vnode.children), - `Invalid vnode for a custom element, it must have children defined.` - ); - } } function mountChildren(children: VNodes, parent: Element) { @@ -610,7 +530,7 @@ function updateDynamicChildren(parentElm: ParentNode, oldCh: VNodes, newCh: VNod } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patch(oldStartVnode, newEndVnode, parentElm, null); - insertNodeHook( + insertVNode( oldStartVnode, parentElm, oldEndVnode.owner.renderer.nextSibling(oldEndVnode.elm!) @@ -620,7 +540,7 @@ function updateDynamicChildren(parentElm: ParentNode, oldCh: VNodes, newCh: VNod } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patch(oldEndVnode, newStartVnode, parentElm, null); - insertNodeHook(oldEndVnode, parentElm, oldStartVnode.elm!); + insertVNode(oldEndVnode, parentElm, oldStartVnode.elm!); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { @@ -641,7 +561,7 @@ function updateDynamicChildren(parentElm: ParentNode, oldCh: VNodes, newCh: VNod } else { patch(elmToMove, newStartVnode, parentElm, null); oldCh[idxInOld] = undefined as any; - insertNodeHook(elmToMove, parentElm, oldStartVnode.elm!); + insertVNode(elmToMove, parentElm, oldStartVnode.elm!); } } newStartVnode = newCh[++newStartIdx]; @@ -705,3 +625,43 @@ function updateStaticChildren(parentElm: ParentNode, oldCh: VNodes, newCh: VNode } } } + +function updateTextContent(vnode: VNode) { + const { + elm, + text, + owner: { renderer }, + } = vnode; + + if (process.env.NODE_ENV !== 'production') { + unlockDomMutation(); + } + renderer.setText(elm, text!); + if (process.env.NODE_ENV !== 'production') { + lockDomMutation(); + } +} + +function insertVNode(vnode: VNode, parentNode: Node, referenceNode: Node | null) { + const { renderer } = vnode.owner; + + if (process.env.NODE_ENV !== 'production') { + unlockDomMutation(); + } + renderer.insert(vnode.elm!, parentNode, referenceNode); + if (process.env.NODE_ENV !== 'production') { + lockDomMutation(); + } +} + +function removeVNode(vnode: VNode, parentNode: Node) { + const { renderer } = vnode.owner; + + if (process.env.NODE_ENV !== 'production') { + unlockDomMutation(); + } + renderer.remove(vnode.elm!, parentNode); + if (process.env.NODE_ENV !== 'production') { + lockDomMutation(); + } +} From c0f066df57eb289d5f4c0d3eeed799f79f92ecda Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Thu, 16 Dec 2021 07:36:28 +0100 Subject: [PATCH 11/17] chore: inline createVMHook --- .../engine-core/src/framework/rendering.ts | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/rendering.ts b/packages/@lwc/engine-core/src/framework/rendering.ts index 63b004f9dd..81dc943881 100644 --- a/packages/@lwc/engine-core/src/framework/rendering.ts +++ b/packages/@lwc/engine-core/src/framework/rendering.ts @@ -203,20 +203,32 @@ function mountCustomElement(vnode: VCustomElement, parent: ParentNode, anchor: N const { sel, owner } = vnode; const { renderer } = owner; + let vm: VM | undefined; const UpgradableConstructor = getUpgradableConstructor(sel, renderer); - /** - * Note: if the upgradable constructor does not expect, or throw when we new it - * with a callback as the first argument, we could implement a more advanced - * mechanism that only passes that argument if the constructor is known to be - * an upgradable custom element. - */ + + // Note: if the upgradable constructor does not expect, or throw when we new it with a callback + // as the first argument, we could implement a more advanced mechanism that only passes that + // argument if the constructor is known to be an upgradable custom element. const elm = (vnode.elm = new UpgradableConstructor((elm: HTMLElement) => { - // the custom element from the registry is expecting an upgrade callback - createViewModelHook(elm, vnode); + if (!isUndefined(getAssociatedVMIfPresent(elm))) { + // There is a possibility that a custom element is registered under tagName, in which + // case, the initialization is already carry on, and there is nothing else to do here + // since this hook is called right after invoking `document.createElement`. + return; + } + + const { mode, ctor } = vnode; + const def = getComponentInternalDef(ctor); + + vm = createVM(elm, def, { + mode, + owner, + tagName: sel, + renderer, + }); })); - linkNodeToShadow(elm, owner); - const vm = getAssociatedVMIfPresent(elm); + linkNodeToShadow(elm, owner); if (!isUndefined(vm)) { allocateCustomElementChildren(vnode, vm); @@ -416,23 +428,6 @@ function allocateInSlot(vm: VM, children: VNodes) { } } -function createViewModelHook(elm: HTMLElement, vnode: VCustomElement) { - if (!isUndefined(getAssociatedVMIfPresent(elm))) { - // There is a possibility that a custom element is registered under tagName, - // in which case, the initialization is already carry on, and there is nothing else - // to do here since this hook is called right after invoking `document.createElement`. - return; - } - const { sel, mode, ctor, owner } = vnode; - const def = getComponentInternalDef(ctor); - createVM(elm, def, { - mode, - owner, - tagName: sel, - renderer: owner.renderer, - }); -} - function mountChildren(children: VNodes, parent: Element) { for (let i = 0; i < children.length; ++i) { const child = children[i]; From 878b6e425864f0b3e517fe5d54d1c0a85489e1a0 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Thu, 16 Dec 2021 07:56:26 +0100 Subject: [PATCH 12/17] chore: remove need to lookup vm --- packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts | 1 + packages/@lwc/engine-core/src/framework/api.ts | 3 ++- packages/@lwc/engine-core/src/framework/rendering.ts | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts b/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts index 27666c9934..725b6c434e 100644 --- a/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts +++ b/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts @@ -58,6 +58,7 @@ export interface VCustomElement extends VBaseElement { // copy of the last allocated children. aChildren?: VNodes; type: VNodeType.CustomElement; + vm: VM | undefined; } export interface VText extends VBaseNode { diff --git a/packages/@lwc/engine-core/src/framework/api.ts b/packages/@lwc/engine-core/src/framework/api.ts index 64e518ccb3..11558fa388 100644 --- a/packages/@lwc/engine-core/src/framework/api.ts +++ b/packages/@lwc/engine-core/src/framework/api.ts @@ -214,7 +214,7 @@ function c( } } const { key } = data; - let text, elm; + let text, elm, vm; const vnode: VCustomElement = { type: VNodeType.CustomElement, sel, @@ -226,6 +226,7 @@ function c( ctor: Ctor, owner: vmBeingRendered, mode: 'open', // TODO [#1294]: this should be defined in Ctor + vm, }; ArrayPush.call(vmBeingRendered.velements, vnode); diff --git a/packages/@lwc/engine-core/src/framework/rendering.ts b/packages/@lwc/engine-core/src/framework/rendering.ts index 81dc943881..1a304e700e 100644 --- a/packages/@lwc/engine-core/src/framework/rendering.ts +++ b/packages/@lwc/engine-core/src/framework/rendering.ts @@ -220,7 +220,7 @@ function mountCustomElement(vnode: VCustomElement, parent: ParentNode, anchor: N const { mode, ctor } = vnode; const def = getComponentInternalDef(ctor); - vm = createVM(elm, def, { + vm = vnode.vm = createVM(elm, def, { mode, owner, tagName: sel, @@ -257,7 +257,7 @@ function mountCustomElement(vnode: VCustomElement, parent: ParentNode, anchor: N function patchCustomElement(n1: VCustomElement, n2: VCustomElement) { const elm = (n2.elm = n1.elm!); - const vm = getAssociatedVMIfPresent(elm); + const vm = (n2.vm = n1.vm); patchElementAttrsAndProps(n1, n2); @@ -293,7 +293,7 @@ function unmount(vnode: VNode, parent: ParentNode) { break; case VNodeType.CustomElement: { - const vm = getAssociatedVMIfPresent(vnode.elm); + const { vm } = vnode; // No need to unmount the children here, `removeVM` will take care of removing the // children. From 0b9e7f896e4cb3a82da5831362d93bda04fe8954 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Thu, 16 Dec 2021 15:08:18 +0100 Subject: [PATCH 13/17] chore: move the vnode types under the main directory --- .../@lwc/engine-core/src/framework/api.ts | 8 +++++--- .../engine-core/src/framework/component.ts | 5 +++-- .../engine-core/src/framework/hydration.ts | 20 +++++++++---------- .../@lwc/engine-core/src/framework/invoker.ts | 2 +- .../src/framework/modules/attrs.ts | 3 ++- .../framework/modules/computed-class-attr.ts | 2 +- .../framework/modules/computed-style-attr.ts | 2 +- .../src/framework/modules/events.ts | 2 +- .../src/framework/modules/props.ts | 2 +- .../framework/modules/static-class-attr.ts | 2 +- .../framework/modules/static-style-attr.ts | 2 +- .../engine-core/src/framework/rendering.ts | 16 +++++++-------- .../engine-core/src/framework/services.ts | 2 +- .../engine-core/src/framework/stylesheet.ts | 2 +- .../engine-core/src/framework/template.ts | 2 +- packages/@lwc/engine-core/src/framework/vm.ts | 2 +- .../snabbdom/types.ts => framework/vnode.ts} | 2 +- 17 files changed, 39 insertions(+), 37 deletions(-) rename packages/@lwc/engine-core/src/{3rdparty/snabbdom/types.ts => framework/vnode.ts} (98%) diff --git a/packages/@lwc/engine-core/src/framework/api.ts b/packages/@lwc/engine-core/src/framework/api.ts index 11558fa388..799a23b31d 100644 --- a/packages/@lwc/engine-core/src/framework/api.ts +++ b/packages/@lwc/engine-core/src/framework/api.ts @@ -22,11 +22,15 @@ import { StringReplace, toString, } from '@lwc/shared'; + import { logError } from '../shared/logger'; + import { invokeEventListener } from './invoker'; import { getVMBeingRendered } from './template'; import { EmptyArray, EmptyObject } from './utils'; import { ShadowMode, SlotSet, VM, RenderMode } from './vm'; +import { LightningElementConstructor } from './base-lightning-element'; +import { isComponentConstructor } from './def'; import { VNode, VNodes, @@ -36,9 +40,7 @@ import { VComment, VElementData, VNodeType, -} from '../3rdparty/snabbdom/types'; -import { LightningElementConstructor } from './base-lightning-element'; -import { isComponentConstructor } from './def'; +} from './vnode'; const SymbolIterator: typeof Symbol.iterator = Symbol.iterator; diff --git a/packages/@lwc/engine-core/src/framework/component.ts b/packages/@lwc/engine-core/src/framework/component.ts index ae5066b2da..ba99f43b21 100644 --- a/packages/@lwc/engine-core/src/framework/component.ts +++ b/packages/@lwc/engine-core/src/framework/component.ts @@ -5,9 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { assert, isFalse, isFunction, isUndefined } from '@lwc/shared'; -import { invokeComponentRenderMethod, isInvokingRender, invokeEventListener } from './invoker'; + +import { VNodes } from './vnode'; import { VM, scheduleRehydration } from './vm'; -import { VNodes } from '../3rdparty/snabbdom/types'; +import { invokeComponentRenderMethod, isInvokingRender, invokeEventListener } from './invoker'; import { ReactiveObserver } from '../libs/mutation-tracker'; import { LightningElementConstructor } from './base-lightning-element'; import { Template, isUpdatingTemplate, getVMBeingRendered } from './template'; diff --git a/packages/@lwc/engine-core/src/framework/hydration.ts b/packages/@lwc/engine-core/src/framework/hydration.ts index 55c1cbc633..07ff3f4104 100644 --- a/packages/@lwc/engine-core/src/framework/hydration.ts +++ b/packages/@lwc/engine-core/src/framework/hydration.ts @@ -8,16 +8,6 @@ import { ArrayFilter, ArrayJoin, assert, isUndefined, keys } from '@lwc/shared'; import { logError, logWarn } from '../shared/logger'; -import { - VBaseElement, - VComment, - VCustomElement, - VElement, - VNode, - VNodes, - VNodeType, - VText, -} from '../3rdparty/snabbdom/types'; import { parseStyleText } from './utils'; import { getComponentInternalDef } from './def'; @@ -34,6 +24,16 @@ import { VM, VMState, } from './vm'; +import { + VBaseElement, + VComment, + VCustomElement, + VElement, + VNode, + VNodes, + VNodeType, + VText, +} from './vnode'; function hydrate(vnode: VNode, node: Node) { switch (vnode.type) { diff --git a/packages/@lwc/engine-core/src/framework/invoker.ts b/packages/@lwc/engine-core/src/framework/invoker.ts index 13f42d5397..ae505d8f16 100644 --- a/packages/@lwc/engine-core/src/framework/invoker.ts +++ b/packages/@lwc/engine-core/src/framework/invoker.ts @@ -11,7 +11,7 @@ import { VM, runWithBoundaryProtection } from './vm'; import { LightningElement, LightningElementConstructor } from './base-lightning-element'; import { logOperationStart, logOperationEnd, OperationId } from './profiler'; -import { VNodes } from '../3rdparty/snabbdom/types'; +import { VNodes } from './vnode'; import { addErrorComponentStack } from '../shared/error'; export let isInvokingRender: boolean = false; diff --git a/packages/@lwc/engine-core/src/framework/modules/attrs.ts b/packages/@lwc/engine-core/src/framework/modules/attrs.ts index 2f2ab7a613..feb9ce0ba4 100644 --- a/packages/@lwc/engine-core/src/framework/modules/attrs.ts +++ b/packages/@lwc/engine-core/src/framework/modules/attrs.ts @@ -5,9 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { assert, isNull, isUndefined, keys, StringCharCodeAt } from '@lwc/shared'; + import { unlockAttribute, lockAttribute } from '../attributes'; import { EmptyObject } from '../utils'; -import { VBaseElement } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../vnode'; const xlinkNS = 'http://www.w3.org/1999/xlink'; const xmlNS = 'http://www.w3.org/XML/1998/namespace'; diff --git a/packages/@lwc/engine-core/src/framework/modules/computed-class-attr.ts b/packages/@lwc/engine-core/src/framework/modules/computed-class-attr.ts index 570b1dd09d..fb9f94d0c9 100644 --- a/packages/@lwc/engine-core/src/framework/modules/computed-class-attr.ts +++ b/packages/@lwc/engine-core/src/framework/modules/computed-class-attr.ts @@ -14,7 +14,7 @@ import { StringSlice, } from '@lwc/shared'; import { EmptyObject, SPACE_CHAR } from '../utils'; -import { VBaseElement } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../vnode'; const classNameToClassMap = create(null); diff --git a/packages/@lwc/engine-core/src/framework/modules/computed-style-attr.ts b/packages/@lwc/engine-core/src/framework/modules/computed-style-attr.ts index 6d68470a55..468f37a63e 100644 --- a/packages/@lwc/engine-core/src/framework/modules/computed-style-attr.ts +++ b/packages/@lwc/engine-core/src/framework/modules/computed-style-attr.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { isNull, isString } from '@lwc/shared'; -import { VBaseElement } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../vnode'; // The style property is a string when defined via an expression in the template. export function patchStyleAttribute(oldVnode: VBaseElement | null, vnode: VBaseElement) { diff --git a/packages/@lwc/engine-core/src/framework/modules/events.ts b/packages/@lwc/engine-core/src/framework/modules/events.ts index a8f24953b2..7c4b91344c 100644 --- a/packages/@lwc/engine-core/src/framework/modules/events.ts +++ b/packages/@lwc/engine-core/src/framework/modules/events.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { isUndefined } from '@lwc/shared'; -import { VBaseElement } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../vnode'; export function applyEventListeners(vnode: VBaseElement) { const { diff --git a/packages/@lwc/engine-core/src/framework/modules/props.ts b/packages/@lwc/engine-core/src/framework/modules/props.ts index c35418a90a..e3162757c6 100644 --- a/packages/@lwc/engine-core/src/framework/modules/props.ts +++ b/packages/@lwc/engine-core/src/framework/modules/props.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { assert, isNull, isUndefined, keys } from '@lwc/shared'; -import { VBaseElement } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../vnode'; function isLiveBindingProp(sel: string, key: string): boolean { // For properties with live bindings, we read values from the DOM element diff --git a/packages/@lwc/engine-core/src/framework/modules/static-class-attr.ts b/packages/@lwc/engine-core/src/framework/modules/static-class-attr.ts index b1b7cf972e..6d6329165c 100644 --- a/packages/@lwc/engine-core/src/framework/modules/static-class-attr.ts +++ b/packages/@lwc/engine-core/src/framework/modules/static-class-attr.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { isUndefined } from '@lwc/shared'; -import { VBaseElement } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../vnode'; // The HTML class property becomes the vnode.data.classMap object when defined as a string in the template. // The compiler takes care of transforming the inline classnames into an object. It's faster to set the diff --git a/packages/@lwc/engine-core/src/framework/modules/static-style-attr.ts b/packages/@lwc/engine-core/src/framework/modules/static-style-attr.ts index b97660454b..93272d4fb0 100644 --- a/packages/@lwc/engine-core/src/framework/modules/static-style-attr.ts +++ b/packages/@lwc/engine-core/src/framework/modules/static-style-attr.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { isUndefined } from '@lwc/shared'; -import { VBaseElement } from '../../3rdparty/snabbdom/types'; +import { VBaseElement } from '../vnode'; // The HTML style property becomes the vnode.data.styleDecls object when defined as a string in the template. // The compiler takes care of transforming the inline style into an object. It's faster to set the diff --git a/packages/@lwc/engine-core/src/framework/rendering.ts b/packages/@lwc/engine-core/src/framework/rendering.ts index 1a304e700e..bbcffe6da1 100644 --- a/packages/@lwc/engine-core/src/framework/rendering.ts +++ b/packages/@lwc/engine-core/src/framework/rendering.ts @@ -5,8 +5,6 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { - // ArrayFilter, - // ArrayJoin, ArrayPush, assert, create, @@ -17,6 +15,12 @@ import { keys, KEY__SHADOW_RESOLVER, } from '@lwc/shared'; + +import { hasDynamicChildren } from './api'; +import { getComponentInternalDef } from './def'; +import { markComponentAsDirty } from './component'; +import { getUpgradableConstructor } from './upgradable-element'; +import { patchElementWithRestrictions, unlockDomMutation, lockDomMutation } from './restrictions'; import { EmptyArray } from './utils'; import { createVM, @@ -42,7 +46,7 @@ import { VComment, VBaseElement, VNodeType, -} from '../3rdparty/snabbdom/types'; +} from './vnode'; import { applyEventListeners } from './modules/events'; import { patchAttributes } from './modules/attrs'; @@ -52,12 +56,6 @@ import { patchStyleAttribute } from './modules/computed-style-attr'; import { applyStaticClassAttribute } from './modules/static-class-attr'; import { applyStaticStyleAttribute } from './modules/static-style-attr'; -import { hasDynamicChildren } from './api'; -import { getComponentInternalDef } from './def'; -import { markComponentAsDirty } from './component'; -import { getUpgradableConstructor } from './upgradable-element'; -import { patchElementWithRestrictions, unlockDomMutation, lockDomMutation } from './restrictions'; - interface KeyToIndexMap { [key: string]: number; } diff --git a/packages/@lwc/engine-core/src/framework/services.ts b/packages/@lwc/engine-core/src/framework/services.ts index a3abb56bc9..9b5dc89930 100644 --- a/packages/@lwc/engine-core/src/framework/services.ts +++ b/packages/@lwc/engine-core/src/framework/services.ts @@ -6,7 +6,7 @@ */ import { ArrayPush, assert, create, isArray, isObject, isUndefined } from '@lwc/shared'; -import { VNodeData } from '../3rdparty/snabbdom/types'; +import { VNodeData } from './vnode'; import { ComponentDef } from './def'; import { VM, Context } from './vm'; diff --git a/packages/@lwc/engine-core/src/framework/stylesheet.ts b/packages/@lwc/engine-core/src/framework/stylesheet.ts index 4447d29769..c210996dea 100644 --- a/packages/@lwc/engine-core/src/framework/stylesheet.ts +++ b/packages/@lwc/engine-core/src/framework/stylesheet.ts @@ -7,7 +7,7 @@ import { ArrayJoin, ArrayPush, isArray, isNull, isUndefined, KEY__SCOPED_CSS } from '@lwc/shared'; import api from './api'; -import { VNode } from '../3rdparty/snabbdom/types'; +import { VNode } from './vnode'; import { RenderMode, ShadowMode, VM } from './vm'; import { Template } from './template'; import { getStyleOrSwappedStyle } from './hot-swaps'; diff --git a/packages/@lwc/engine-core/src/framework/template.ts b/packages/@lwc/engine-core/src/framework/template.ts index 666a13f84c..b4b8d3e2ad 100644 --- a/packages/@lwc/engine-core/src/framework/template.ts +++ b/packages/@lwc/engine-core/src/framework/template.ts @@ -18,7 +18,7 @@ import { KEY__SCOPED_CSS, } from '@lwc/shared'; import { logError } from '../shared/logger'; -import { VNode, VNodes } from '../3rdparty/snabbdom/types'; +import { VNode, VNodes } from './vnode'; import api, { RenderAPI } from './api'; import { resetComponentRoot, diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index 7ff15dcedc..ae2508a12c 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -40,8 +40,8 @@ import { connectWireAdapters, disconnectWireAdapters, installWireAdapters } from import { AccessorReactiveObserver } from './decorators/api'; import { Renderer, HostNode, HostElement } from './renderer'; import { removeActiveVM } from './hot-swaps'; +import { VNodes, VCustomElement, VNode } from './vnode'; -import { VNodes, VCustomElement, VNode } from '../3rdparty/snabbdom/types'; import { addErrorComponentStack } from '../shared/error'; type ShadowRootMode = 'open' | 'closed'; diff --git a/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts b/packages/@lwc/engine-core/src/framework/vnode.ts similarity index 98% rename from packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts rename to packages/@lwc/engine-core/src/framework/vnode.ts index 725b6c434e..9838ec5e12 100644 --- a/packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts +++ b/packages/@lwc/engine-core/src/framework/vnode.ts @@ -13,7 +13,7 @@ https://github.com/snabbdom/snabbdom/ */ -import { VM } from '../../framework/vm'; +import { VM } from './vm'; export type Key = string | number; From 372fb61d0f3fa082cd452a0690e93bec58777e74 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Thu, 16 Dec 2021 16:48:09 +0100 Subject: [PATCH 14/17] test: fix snapshot tests --- .../attribute-component-global-html/expected.html | 2 +- .../fixtures/attribute-global-html/expected.html | 2 +- .../src/__tests__/fixtures/attribute-static/expected.html | 2 +- .../__tests__/fixtures/if-conditional-slot/expected.html | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-component-global-html/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-component-global-html/expected.html index 88da2d4781..0c759c6a50 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-component-global-html/expected.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-component-global-html/expected.html @@ -1,6 +1,6 @@