diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs index b3295f8..5af6250 100644 --- a/.lintstagedrc.mjs +++ b/.lintstagedrc.mjs @@ -24,16 +24,16 @@ function test(files) { ) const other = Object.entries(packages) .filter(([key]) => key !== 'babel-plugin-jsx') - .map(([_, v]) => v) + .map(([_, file]) => file).flat(Infinity) return [ plugin.length - ? `jest --config packages/${plugin[0][0]}/jest.config.js ${plugin[0][1].join( + ? `jest --showConfig --config packages/${plugin[0][0]}/jest.config.js ${plugin[0][1].join( ' ' )}` : null, other.length - ? `jest --config jest.config.js --colors ${other.join(' ')}` + ? `jest --showConfig --config jest.config.js --colors ${other.join(' ')}` : null, ].filter(Boolean) } diff --git a/README_EN.md b/README_EN.md index f954886..48e015d 100644 --- a/README_EN.md +++ b/README_EN.md @@ -16,9 +16,9 @@ # Gyron -` Gyron` is a simple zero-dependency responsive framework . Core code size: npm bundle size (scoped)。 +`Gyron` is a simple zero-dependency responsive framework . Core code size: npm bundle size (scoped)。 -It also has a very good performance performance, details of which can be found in [js-framework-benchmark](https://krausest.github.io/js-framework-benchmark/current.html#eyJmcmFtZXdvcmtzIjpbImtleWVkL2FuZ3VsYXIiLCJrZXllZC9neXJvbiIsImtleWVkL3JlYWN0Iiwibm9uLWtleWVkL2d5cm9uIiwibm9uLWtleWVkL3JlYWN0Il0sImJlbmNobWFya3MiOlsiMDFfcnVuMWsiLCIwMl9yZXBsYWNlMWsiLCIwM191cGRhdGUxMHRoMWtfeDE2IiwiMDRfc2VsZWN0MWsiLCIwNV9zd2FwMWsiLCIwNl9yZW1vdmUtb25lLTFrIiwiMDdfY3JlYXRlMTBrIiwiMDhfY3JlYXRlMWstYWZ0ZXIxa194MiIsIjA5X2NsZWFyMWtfeDgiLCIyMV9yZWFkeS1tZW1vcnkiLCIyMl9ydW4tbWVtb3J5IiwiMjNfdXBkYXRlNS1tZW1vcnkiLCIyNV9ydW4tY2xlYXItbWVtb3J5IiwiMjZfcnVuLTEway1tZW1vcnkiLCIzMV9zdGFydHVwLWNpIiwiMzRfc3RhcnR1cC10b3RhbGJ5dGVzIl0sImRpc3BsYXlNb2RlIjoxLCJjYXRlZ29yaWVzIjpbMSwyLDMsNF19) 提供的结果。 +It also has a very good performance performance, details of which can be found in [js-framework-benchmark](https://krausest.github.io/js-framework-benchmark/current.html#eyJmcmFtZXdvcmtzIjpbImtleWVkL2FuZ3VsYXIiLCJrZXllZC9neXJvbiIsImtleWVkL3JlYWN0Iiwibm9uLWtleWVkL2d5cm9uIiwibm9uLWtleWVkL3JlYWN0Il0sImJlbmNobWFya3MiOlsiMDFfcnVuMWsiLCIwMl9yZXBsYWNlMWsiLCIwM191cGRhdGUxMHRoMWtfeDE2IiwiMDRfc2VsZWN0MWsiLCIwNV9zd2FwMWsiLCIwNl9yZW1vdmUtb25lLTFrIiwiMDdfY3JlYXRlMTBrIiwiMDhfY3JlYXRlMWstYWZ0ZXIxa194MiIsIjA5X2NsZWFyMWtfeDgiLCIyMV9yZWFkeS1tZW1vcnkiLCIyMl9ydW4tbWVtb3J5IiwiMjNfdXBkYXRlNS1tZW1vcnkiLCIyNV9ydW4tY2xlYXItbWVtb3J5IiwiMjZfcnVuLTEway1tZW1vcnkiLCIzMV9zdGFydHVwLWNpIiwiMzRfc3RhcnR1cC10b3RhbGJ5dGVzIl0sImRpc3BsYXlNb2RlIjoxLCJjYXRlZ29yaWVzIjpbMSwyLDMsNF19) - Readme - [中文](./README.md) diff --git a/jest.config.js b/jest.config.js index 383a387..e1cff7c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,7 +10,7 @@ module.exports = { : [`${process.cwd()}/tests/**/(*.)+(spec|test).[jt]s?(x)`], testEnvironment: 'jsdom', moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { - prefix: '../../', + prefix: process.env.PACKAGES ? '../../' : '', }), collectCoverageFrom: [`./src/**/*.ts`], globals: { diff --git a/packages/dom-client/package.json b/packages/dom-client/package.json index e4ccf70..519011f 100644 --- a/packages/dom-client/package.json +++ b/packages/dom-client/package.json @@ -17,7 +17,7 @@ "build:esm": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/esm/index.js --format=esm --platform=node --external:@gyron/* --define:__DEV__=false --define:__WARN__=true", "build:cjs": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/cjs/index.js --format=cjs --platform=node --external:@gyron/* --define:__DEV__=false --define:__WARN__=true", "build:dts": "rollup -c ../../rollup.config.js", - "test": "jest --config=../../jest.config.js" + "test": "cross-env PACKAGES=dom-client jest --config=../../jest.config.js" }, "devDependencies": { "@gyron/shared": "^0.0.29" diff --git a/packages/dom-server/package.json b/packages/dom-server/package.json index 1428288..f8c9349 100644 --- a/packages/dom-server/package.json +++ b/packages/dom-server/package.json @@ -17,7 +17,7 @@ "build:esm": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/esm/index.js --format=esm --platform=node --external:@gyron/* --define:__DEV__=false --define:__WARN__=true", "build:cjs": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/cjs/index.js --format=cjs --platform=node --external:@gyron/* --define:__DEV__=false --define:__WARN__=true", "build:dts": "rollup -c ../../rollup.config.js", - "test": "jest --config=../../jest.config.js" + "test": "cross-env PACKAGES=dom-server jest --config=../../jest.config.js" }, "devDependencies": { "@gyron/shared": "^0.0.29" diff --git a/packages/jsx-runtime/package.json b/packages/jsx-runtime/package.json index 1e285ee..c1beb7d 100644 --- a/packages/jsx-runtime/package.json +++ b/packages/jsx-runtime/package.json @@ -17,7 +17,7 @@ "build:esm": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/esm/index.js --format=esm --platform=node --external:@gyron/* --define:__DEV__=false --define:__WARN__=true", "build:cjs": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/cjs/index.js --format=cjs --platform=node --external:@gyron/* --define:__DEV__=false --define:__WARN__=true", "build:dts": "rollup -c ../../rollup.config.js", - "test": "jest --config=../../jest.config.js" + "test": "cross-env PACKAGES=jsx-runtime jest --config=../../jest.config.js" }, "dependencies": { "@gyron/runtime": "^0.0.29", diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index 780402a..5fb758b 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -17,7 +17,7 @@ "build:esm": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/esm/index.js --format=esm --platform=node --external:@gyron/*", "build:cjs": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/cjs/index.js --format=cjs --platform=node --external:@gyron/*", "build:dts": "rollup -c ../../rollup.config.js", - "test": "jest --config=../../jest.config.js" + "test": "cross-env PACKAGES=reactivity jest --config=../../jest.config.js" }, "dependencies": { "@gyron/shared": "^0.0.29" diff --git a/packages/redux/package.json b/packages/redux/package.json index 7362055..5134fd4 100644 --- a/packages/redux/package.json +++ b/packages/redux/package.json @@ -17,7 +17,7 @@ "build:esm": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/esm/index.js --format=esm --platform=node --external:@gyron/* --external:@reduxjs/toolkit", "build:cjs": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/cjs/index.js --format=cjs --platform=node --external:@gyron/* --external:@reduxjs/toolkit", "build:dts": "rollup -c ../../rollup.config.js", - "test": "jest --config=../../jest.config.js" + "test": "cross-env PACKAGES=redux jest --config=../../jest.config.js" }, "dependencies": { "@gyron/runtime": "^0.0.29", diff --git a/packages/router/package.json b/packages/router/package.json index 3a9da4c..2deebc9 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -18,7 +18,7 @@ "build:cjs": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/cjs/index.js --format=cjs --platform=node --external:@gyron/* --external:history --external:path-to-regexp --define:__DEV__=false --define:__WARN__=true", "build:browser": "esbuild src/index.ts --bundle --sourcemap --outfile=dist/browser/index.js --format=esm --platform=browser --define:__DEV__=false --define:__WARN__=true", "build:dts": "rollup -c ../../rollup.config.js", - "test": "jest --config=../../jest.config.js" + "test": "cross-env PACKAGES=router jest --config=../../jest.config.js" }, "dependencies": { "@gyron/runtime": "^0.0.29", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 0c0dbce..80fc135 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -17,7 +17,7 @@ "build:esm": "esbuild src/index.ts --bundle --sourcemap --outfile=dist/esm/index.js --format=esm --platform=node", "build:cjs": "esbuild src/index.ts --bundle --sourcemap --outfile=dist/cjs/index.js --format=cjs --platform=node", "build:dts": "cross-env RESPECT_EXTERNAL=runtime rollup -c ../../rollup.config.js", - "test": "jest --config=../../jest.config.js" + "test": "cross-env PACKAGES=runtime jest --config=../../jest.config.js" }, "devDependencies": { "@gyron/dom-client": "^0.0.29", diff --git a/packages/runtime/src/assert.ts b/packages/runtime/src/assert.ts index e4009f3..b66e6bc 100644 --- a/packages/runtime/src/assert.ts +++ b/packages/runtime/src/assert.ts @@ -1,5 +1,5 @@ import { Component } from './component' -import { getErrorBoundaryCtx } from './ErrorBoundary' +import { getErrorBoundaryCtx } from './internal' import { ErrorType, WarnType, BoundariesHandler } from './boundaries' export enum InnerCode { diff --git a/packages/runtime/src/boundaries.ts b/packages/runtime/src/boundaries.ts index 5ff806c..19927ca 100644 --- a/packages/runtime/src/boundaries.ts +++ b/packages/runtime/src/boundaries.ts @@ -1,5 +1,5 @@ import { Component, getCurrentComponent } from './component' -import { getErrorBoundaryCtx } from './ErrorBoundary' +import { getErrorBoundaryCtx } from './internal' export type BoundariesHandlerParamsType = 'Error' | 'Warn' export type BoundariesHandlerParams = Partial<{ diff --git a/packages/runtime/src/hydrate.ts b/packages/runtime/src/hydrate.ts index 652defb..6fa6752 100644 --- a/packages/runtime/src/hydrate.ts +++ b/packages/runtime/src/hydrate.ts @@ -22,7 +22,7 @@ import { normalizeChildrenVNode, normalizeVNodeWithLink, } from './vnode' -import { mountComponent, patch } from './render' +import { mountComponent, patch } from './renderer' import { setRef } from './ref' /** diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 4c603f0..6b04c2b 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -12,8 +12,7 @@ export { isResponsive, toRaw, } from '@gyron/reactivity' -export { ErrorBoundary } from './ErrorBoundary' -export { Transition } from './Transition' +export { Transition, ErrorBoundary } from './internal' export { useWatch, createComponentInstance, @@ -29,6 +28,7 @@ export { FC, } from './component' export { createInstance, render, createContext } from './instance' +export { createInstance as createGyron } from './instance' export { createVNode, createVNodeComment, diff --git a/packages/runtime/src/instance.ts b/packages/runtime/src/instance.ts index 27a4945..c47c65e 100644 --- a/packages/runtime/src/instance.ts +++ b/packages/runtime/src/instance.ts @@ -1,5 +1,5 @@ import type { RenderElement, VNode } from './vnode' -import { patch, unmount } from './render' +import { patch, unmount } from './renderer' import { hydrate } from './hydrate' import { getUserContainer } from './shared' diff --git a/packages/runtime/src/ErrorBoundary.ts b/packages/runtime/src/internal/ErrorBoundary.ts similarity index 83% rename from packages/runtime/src/ErrorBoundary.ts rename to packages/runtime/src/internal/ErrorBoundary.ts index 8d99d54..9d3850d 100644 --- a/packages/runtime/src/ErrorBoundary.ts +++ b/packages/runtime/src/internal/ErrorBoundary.ts @@ -1,10 +1,10 @@ import { useReactive } from '@gyron/reactivity' import { isFunction } from '@gyron/shared' -import { Component, FC } from './component' -import { VNode } from './vnode' -import { h } from './h' -import { inject, useProvide } from './context' -import { BoundariesHandler, BoundariesHandlerParams } from './boundaries' +import { Component, FC } from '../component' +import { VNode } from '../vnode' +import { h } from '../h' +import { inject, useProvide } from '../context' +import { BoundariesHandler, BoundariesHandlerParams } from '../boundaries' export interface ErrorBoundaryProps { fallback: VNode diff --git a/packages/runtime/src/Transition.ts b/packages/runtime/src/internal/Transition.ts similarity index 97% rename from packages/runtime/src/Transition.ts rename to packages/runtime/src/internal/Transition.ts index b5da2c6..adda30a 100644 --- a/packages/runtime/src/Transition.ts +++ b/packages/runtime/src/internal/Transition.ts @@ -1,9 +1,9 @@ -import { VNode } from './vnode' -import { FC } from './component' +import { RenderElement, VNode } from '../vnode' +import { FC } from '../component' import { isFunction, isNumber, shouldValue } from '@gyron/shared' -import { isVNode, isVNodeComment, RenderElement, warn } from '.' import { Noop } from '@gyron/shared' -import { InnerCode } from './assert' +import { InnerCode, warn } from '../assert' +import { isVNode, isVNodeComment } from '../shared' interface TransitionPropsNormalize { cls: { diff --git a/packages/runtime/src/internal/index.ts b/packages/runtime/src/internal/index.ts new file mode 100644 index 0000000..347d762 --- /dev/null +++ b/packages/runtime/src/internal/index.ts @@ -0,0 +1,2 @@ +export { TransitionHooks, whenTransitionEnd, Transition } from './Transition' +export { getErrorBoundaryCtx, ErrorBoundary } from './ErrorBoundary' diff --git a/packages/runtime/src/render.ts b/packages/runtime/src/render.ts deleted file mode 100644 index 4da3c54..0000000 --- a/packages/runtime/src/render.ts +++ /dev/null @@ -1,721 +0,0 @@ -import { - createComment, - createElement, - createText, - insert, - isSelectElement, - mountProps, - nextSibling, - patchProps, - remove, -} from '@gyron/dom-client' -import { - asyncTrackEffect, - clearTrackEffect, - createEffect, -} from '@gyron/reactivity' -import { - extend, - isArray, - isBoolean, - isElement, - isEqual, - isFunction, - isObject, - isPromise, - keys, - Noop, - shouldValue, -} from '@gyron/shared' -import { warn } from './assert' -import { - Component, - ComponentSetupFunction, - createComponentInstance, - getCacheComponent, - isAsyncComponent, - isCacheComponent, - renderComponent, - normalizeComponent, - removeBuiltInProps, -} from './component' -import { collectHmrComponent, refreshComponentType } from './hmr' -import { hydrate } from './hydrate' -import { invokeLifecycle } from './lifecycle' -import { setRef } from './ref' -import { JobPriority, pushQueueJob, SchedulerJob } from './scheduler' -import { isVNode, isVNodeComponent } from './shared' -import { SSRMessage } from './ssr' -import { - Children, - Comment, - Element, - Fragment, - mergeVNodeWith, - normalizeChildrenVNode, - normalizeVNode, - RenderElement, - Text, - VNode, -} from './vnode' - -function shouldUpdate(result: any) { - return !(isBoolean(result) && !result) -} - -export function isSameVNodeType(n1: VNode, n2: VNode) { - return n1.type === n2.type && n1.key === n2.key -} - -function isKeyPatch(n1: VNode[], n2: VNode[]) { - if (n1 && n2 && n1[0] && n2[0] && isObject(n1[0]) && isObject(n2[0])) { - return shouldValue(n1[0].key) && shouldValue(n2[0].key) - } - return false -} - -function getNextSibling(vnode: VNode) { - if (vnode.component) { - return getNextSibling(vnode.component.subTree) - } - if (vnode.el || vnode.anchor) { - return nextSibling(vnode.el || vnode.anchor) - } - return null -} - -function mountChildren( - nodes: VNode[] | Children[], - container: RenderElement, - anchor: RenderElement, - start = 0, - parentComponent: Component | null = null, - isSvg: boolean -) { - for (let i = start; i < nodes.length; i++) { - const node = normalizeVNode(nodes[i]) - patch(null, node, container, anchor, parentComponent, isSvg) - } -} - -function removeInvoke(_el: RenderElement, vnode: VNode, done: Noop) { - const { transition } = vnode - const el = _el as Element - if (transition) { - transition.onLeave(el, () => { - remove(el) - done() - }) - } else { - remove(el) - done() - } -} - -export function unmount(vnode: VNode) { - if (!isVNode(vnode)) { - return null - } - - function reset() { - vnode.el = null - } - const { el, component, children, transition } = vnode - - if (component) { - if (!isCacheComponent(component.type)) { - component.effect.stop() - } - if (component.subTree) { - unmount(component.subTree) - } - invokeLifecycle(component, 'destroyed') - if (component.$el) { - removeInvoke(component.$el, vnode, reset) - if (!isCacheComponent(component.type)) { - component.$el = null - } - } - component.destroyed = true - component.mounted = false - } else { - if (!transition) { - if (isArray(children) && children.length > 0) { - unmountChildren(children as VNode[]) - } else { - unmount(children as VNode) - } - } - if (isElement(el)) { - removeInvoke(el, vnode, reset) - } - } -} - -function unmountChildren(c1: VNode[], start = 0) { - for (let i = start; i < c1.length; i++) { - unmount(c1[i]) - } -} - -function patchNonKeyed( - c1: VNode[], - c2: VNode[], - container: RenderElement, - anchor: RenderElement, - parentComponent: Component, - isSvg: boolean -) { - const c1length = c1.length - const c2length = c2.length - const minLength = Math.min(c1length, c2length) - for (let i = 0; i < minLength; i++) { - const prevChild = c1[i] - const nextChild = c2[i] - patch(prevChild, nextChild, container, anchor, parentComponent) - } - if (c1length > c2length) { - unmountChildren(c1, minLength) - } else { - mountChildren(c2, container, anchor, minLength, parentComponent, isSvg) - } -} - -function patchKeyed( - c1: VNode[], - c2: VNode[], - container: RenderElement, - anchor: RenderElement | null, - parentComponent: Component | null, - isSvg: boolean -) { - const o1: Record< - string | symbol, - VNode & { index: number; inserted: boolean } - > = c1.reduce((nodeMap, node, index) => { - nodeMap[node.key] = extend(node, { index }) - return nodeMap - }, {}) - - const e2 = c2.length - let i = 0 - while (i < e2) { - const c2n = c2[i] - const c1n = o1[c2n.key] - if (c1n) { - // 1, find the same key value of the node, and then inserted into the corresponding location. (Do not delete add, move directly) - const el = mergeVNodeWith(c2n, c1n).el - if (c1n.index !== i) { - // insert to new position when node order is changed - const anchor = container.childNodes[i] - if (el !== anchor.nextSibling) { - insert(el, container, anchor.nextSibling) - } - } - // update props after migration is complete - // element update attribute - if (!isEqual(c1n.props, c2n.props)) { - const isComponent = isVNodeComponent(c2n) && isVNodeComponent(c1n) - if (isComponent) { - patchComponent(c1n, c2n, container, anchor, parentComponent) - } else { - patchProps( - el as HTMLElement, - c1n, - extend({}, c2n, { props: removeBuiltInProps(c2n.props) }) - ) - } - } - // 1, end - - if (c1n.children || c2n.children) { - // 2, update the nodes with the same key value, including the child nodes. - // sub-level nodes need to be patched again - patchChildren(c1n, c2n, container, anchor, parentComponent, isSvg) - // 2, end - } - - // mark nodes that have been moved and do not need to be uninstalled in the third step - c1n.inserted = true - } else { - patch(null, c2n, container, anchor, parentComponent, isSvg) - } - i++ - } - - for (const node of Object.values(o1)) { - if (!node.inserted) { - // 3, uninstall the old nodes that are not used. - unmount(node) - // 3, end - } - } -} - -function patchChildren( - n1: VNode, - n2: VNode, - container: RenderElement, - anchor: RenderElement, - parentComponent: Component, - isSvg: boolean -) { - if (isFunction(n1.children) && isFunction(n2.children)) { - // when the child nodes are all functions, they should be called by the parent node. - // {count => }</Parent> - return - } - - const c1memo = n1.props.memo - const c2memo = n2.props.memo - if (isArray(c1memo) && isArray(c2memo)) { - const index = c1memo.findIndex((item, index) => { - return c2memo[index] !== item - }) - if (index < 0) { - n2.children = n1.children - return - } - } - - const c1 = n1.children as VNode[] - const c2: VNode[] = (n2.children = normalizeChildrenVNode(n2)) - - if (c1?.length || c2?.length) { - if (isKeyPatch(c1, c2)) { - const el = (n2.el = n1.el) - patchKeyed(c1, c2, el || container, anchor, parentComponent, isSvg) - } else { - // if the fragment node does not have a dom instance, use the container - const el = (n2.el = n1.el) - if (c1) { - patchNonKeyed(c1, c2, el || container, anchor, parentComponent, isSvg) - } else { - mountChildren(c2, el || container, anchor, 0, parentComponent, isSvg) - } - } - } -} - -function mountElement( - vnode: VNode, - container: RenderElement, - anchor: RenderElement, - parentComponent: Component, - isSvg: boolean -) { - const { tag, is, transition } = vnode - const el = (vnode.el = createElement(tag, isSvg, is) as RenderElement) - el.__vnode__ = vnode - - if (vnode.props.ref) { - setRef(el, vnode.props.ref) - } - - const props = removeBuiltInProps(vnode.props) - if (shouldValue(keys(props))) { - mountProps(el as HTMLElement, extend({}, vnode, { props: props })) - } - - if (shouldValue(vnode.children)) { - vnode.children = normalizeChildrenVNode(vnode) - mountChildren(vnode.children, vnode.el, anchor, 0, parentComponent, isSvg) - } - - insert(el, container, anchor) - - if (transition) { - transition.onActive(el as Element) - } -} - -function patchElement( - n1: VNode, - n2: VNode, - container: RenderElement, - anchor: RenderElement, - parentComponent: Component, - isSvg: boolean -) { - const el = (n2.el = n1.el) as Element - if (el.nodeName === n2.tag.toLocaleUpperCase()) { - if (!isEqual(n1.props, n2.props) || isSelectElement(n2)) { - patchProps( - el, - n1, - extend({}, n2, { props: removeBuiltInProps(n2.props) }) - ) - } - if (n1.children || n2.children) { - patchChildren(n1, n2, container, anchor, parentComponent, isSvg) - } - } else { - anchor = getNextSibling(n1) - unmount(n1) - patch(null, n2, container, anchor, parentComponent, isSvg) - } -} - -function patchSubTree(component: Component, prevTree: VNode, nextTree: VNode) { - component.subTree = nextTree - if (component.mounted) { - const { anchor } = prevTree - component.subTree.anchor = anchor - patch(prevTree, nextTree, component.$parent, anchor, component) - // onAfterUpdate - invokeLifecycle(component, 'afterUpdates') - component.$el = nextTree.el - } else { - // mount - patch(null, nextTree, component.$parent, component.vnode.anchor, component) - // after the render is complete, set el to the vnode for comparison - // dummy ? <componentA /> : <componentB /> - component.vnode.el = nextTree.el - component.$el = nextTree.el - component.mounted = true - // onAfterMount - component.effect.allowEffect = true - invokeLifecycle(component, 'afterMounts') - component.effect.allowEffect = false - } -} - -function updateComponentEffect( - component: Component, - ssrMessage: SSRMessage = null -) { - if (component.mounted) { - // if the onBeforeUpdate callback function returns falsy - // no update of the component is performed - if ( - shouldUpdate(invokeLifecycle(component, 'beforeUpdates')) && - shouldUpdate(!component.props.static) - ) { - if (__DEV__) { - refreshComponentType(component.vnode, component) - } - - const prevTree = component.subTree - const nextTree = renderComponent(component) - if (isPromise(nextTree)) { - warn( - 'Asynchronous components without wrapping are not supported, please use FCA wrapping', - component, - 'UpdateComponent' - ) - } else { - patchSubTree(component, prevTree, nextTree) - } - } - } else if (!component.destroyed) { - if (component.vnode.el) { - function hydrateSubTree() { - const nextTree = renderComponent(component) - component.subTree = nextTree as VNode - hydrate(component.vnode.el, component.subTree, component, ssrMessage) - - component.mounted = true - // onAfterMount - invokeLifecycle(component, 'afterMounts') - } - // asynchronous component rendering in ssr mode - if (isAsyncComponent(component.vnode.type)) { - component.vnode.type.__loader(component.props, component).then(() => { - if (!component.destroyed) { - asyncTrackEffect(component.effect) - hydrateSubTree() - clearTrackEffect() - } - }) - } else { - hydrateSubTree() - } - } else { - const nextTree = renderComponent(component) - if (isPromise(nextTree)) { - warn( - 'Asynchronous components without wrapping are not supported, please use FCA wrapping', - component, - 'SetupPatch' - ) - } else { - nextTree.transition ||= component.vnode.transition - patchSubTree(component, null, nextTree) - } - } - } -} - -function renderComponentEffect( - component: Component, - ssrMessage: SSRMessage = null -) { - const effect = (component.effect = createEffect( - updateComponentEffect.bind(null, component, ssrMessage), - () => pushQueueJob(component.update) - )) - - const update = (component.update = effect.run.bind(effect) as SchedulerJob) - update.id = component.uid - update.component = component - update.priority = JobPriority.NORMAL - update() -} - -export function mountComponent( - vnode: VNode<ComponentSetupFunction>, - container: RenderElement, - anchor: RenderElement, - parentComponent: Component, - ssrMessage: SSRMessage = null -) { - vnode.anchor = anchor - - const component = (vnode.component = createComponentInstance( - vnode, - parentComponent - )) - component.$parent = container - - // if (vnode.transition) { - // const { innerCache, transitionLeaving } = vnode.transition.state - // const innerVNode = innerCache.get(vnode.type) - // if (innerVNode && transitionLeaving) { - // const subTree = getVNodeWithComponent(innerVNode) - // // when a component is wrapped by a Transition, mark the component as active - // component.mounted = true - // component.subTree = subTree - // } - // } - - if (__DEV__ && (component.type as any).__hmr_id) { - refreshComponentType(vnode, component) - - const parentId: string = parentComponent - ? (parentComponent.type as any).__hmr_id - : null - collectHmrComponent((component.type as any).__hmr_id, parentId, component) - } - - if (component.props.ref) { - setRef(component.exposed, component.props.ref) - } - - renderComponentEffect(component, ssrMessage) -} - -function patchComponent( - n1: VNode<ComponentSetupFunction>, - n2: VNode<ComponentSetupFunction>, - container: RenderElement, - anchor: RenderElement, - parentComponent: Component -) { - const component = (n2.component = n1.component) - if (component) { - normalizeComponent(n2, component, parentComponent) - if (isCacheComponent(n1.component.type)) { - if (!isEqual(n1.props, n2.props)) { - component.update() - } - } else { - component.update() - } - } else { - if (__WARN__) { - console.warn('Component update exception', n1) - } - mountComponent(n2, container, anchor, parentComponent) - } -} - -function enterComponent( - n1: VNode<ComponentSetupFunction> | null, - n2: VNode<ComponentSetupFunction>, - container: RenderElement, - anchor: RenderElement, - parentComponent: Component -) { - if (n1 === null) { - // clear the element of the next vnode to prevent access to the SSR hydrate logic. - n2.el = null - if (isCacheComponent(n2.type)) { - // update the DOM with the locally cached component state when a local component cache is found - const component = getCacheComponent(n2.type) - component.destroyed = false - component.mounted = true - component.vnode = n2 - component.$parent = container - if (isEqual(removeBuiltInProps(component.props), n2.props)) { - patch(null, component.subTree, container, anchor, parentComponent) - } else { - mountComponent(n2, container, anchor, parentComponent) - } - } else { - mountComponent(n2, container, anchor, parentComponent) - } - } else { - n2.anchor ||= n1.anchor - patchComponent(n1, n2, container, anchor, parentComponent) - } -} - -function transitionMove( - n1: VNode, - n2: VNode, - container: RenderElement, - anchor: RenderElement, - parentComponent: Component, - isSvg: boolean -) { - const { transition } = n1 - const el = n1.el as Element - transition.onLeaveFinish(el) - unmount(n1) - patch( - null, - n2, - container, - anchor || getNextSibling(n1), - parentComponent, - isSvg - ) -} - -function enterElement( - n1: VNode | null, - n2: VNode, - container: RenderElement, - anchor: RenderElement, - parentComponent: Component, - isSvg: boolean -) { - isSvg = isSvg || n2.tag === 'svg' - - if (n1 === null) { - mountElement(n2, container, anchor, parentComponent, isSvg) - } else if (!n2.props.static) { - if (n1.transition) { - transitionMove(n1, n2, container, anchor, parentComponent, isSvg) - } else { - patchElement(n1, n2, container, anchor, parentComponent, isSvg) - } - } -} - -function enterFragment( - n1: VNode | null, - n2: VNode, - container: RenderElement, - anchor: RenderElement, - parentComponent: Component, - isSvg: boolean -) { - const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : createText('')) - if (n1 === null) { - n2.anchor = fragmentEndAnchor - insert(fragmentEndAnchor, container, anchor) - - n2.children = normalizeChildrenVNode(n2) - mountChildren( - n2.children, - container, - fragmentEndAnchor, - 0, - parentComponent, - isSvg - ) - } else { - patchChildren(n1, n2, container, fragmentEndAnchor, parentComponent, isSvg) - } -} - -function enterComment( - n1: VNode | null, - n2: VNode, - container: RenderElement, - anchor: RenderElement -) { - if (n1 === null) { - const comment = createComment( - (n2.children as string) || '' - ) as RenderElement - comment.__vnode__ = n2 - - n2.el = comment as RenderElement - insert(comment, container, anchor) - } else { - n2.el = n1.el - n2.el.__vnode__ = n2 - } -} - -function enterText( - n1: VNode | null, - n2: VNode, - container: RenderElement, - anchor: RenderElement -) { - if (n1 === null || !n1.el) { - // when hydrating the code, since there is no empty text node on the server side, you need to execute mountText - const textNode = createText(n2.children as string) as RenderElement - - textNode.__vnode__ = n2 - n2.el = textNode - - insert(textNode, container, anchor) - } else { - const el = (n2.el = n1.el) - - const c1 = '' + n1.children - const c2 = '' + n2.children - - if (c1 !== c2) { - el.textContent = c2 - } - } -} - -export function patch( - n1: VNode | null, - n2: VNode, - container: RenderElement, - anchor: RenderElement | null = null, - parentComponent: Component | null = null, - isSvg = false -) { - if (!container) { - throw new Error( - 'The parent element is not found when updating, please check the code.' - ) - } - - if (n1 && !isSameVNodeType(n1, n2)) { - anchor = getNextSibling(n1) - unmount(n1) - n1 = null - } - - switch (n2.type) { - case Text: - enterText(n1, n2, container, anchor) - break - case Comment: - enterComment(n1, n2, container, anchor) - break - case Element: - enterElement(n1, n2, container, anchor, parentComponent, isSvg) - break - case Fragment: - enterFragment(n1, n2, container, anchor, parentComponent, isSvg) - break - default: - enterComponent( - n1 as VNode<ComponentSetupFunction>, - n2 as VNode<ComponentSetupFunction>, - container, - anchor, - parentComponent - ) - } -} diff --git a/packages/runtime/src/renderer/comment.ts b/packages/runtime/src/renderer/comment.ts new file mode 100644 index 0000000..00e8c87 --- /dev/null +++ b/packages/runtime/src/renderer/comment.ts @@ -0,0 +1,22 @@ +import { createComment, insert } from '@gyron/dom-client' +import { RenderElement, VNode } from '../vnode' + +export function enterComment( + n1: VNode | null, + n2: VNode, + container: RenderElement, + anchor: RenderElement +) { + if (n1 === null) { + const comment = createComment( + (n2.children as string) || '' + ) as RenderElement + comment.__vnode__ = n2 + + n2.el = comment as RenderElement + insert(comment, container, anchor) + } else { + n2.el = n1.el + n2.el.__vnode__ = n2 + } +} diff --git a/packages/runtime/src/renderer/component.ts b/packages/runtime/src/renderer/component.ts new file mode 100644 index 0000000..63c8380 --- /dev/null +++ b/packages/runtime/src/renderer/component.ts @@ -0,0 +1,223 @@ +import { isBoolean, isEqual, isPromise } from '@gyron/shared' +import { VNode, RenderElement } from '../vnode' +import { + isCacheComponent, + getCacheComponent, + ComponentSetupFunction, + Component, + removeBuiltInProps, + normalizeComponent, + createComponentInstance, + isAsyncComponent, + renderComponent, +} from '../component' +import { patch } from '.' +import { SSRMessage } from '../ssr' +import { refreshComponentType, collectHmrComponent } from '../hmr' +import { setRef } from '../ref' +import { + asyncTrackEffect, + clearTrackEffect, + createEffect, +} from '@gyron/reactivity' +import { JobPriority, pushQueueJob, SchedulerJob } from '../scheduler' +import { warn } from '../assert' +import { hydrate } from '../hydrate' +import { invokeLifecycle } from '../lifecycle' + +function shouldUpdate(result: any) { + return !(isBoolean(result) && !result) +} + +function patchSubTree(component: Component, prevTree: VNode, nextTree: VNode) { + component.subTree = nextTree + if (component.mounted) { + const { anchor } = prevTree + component.subTree.anchor = anchor + patch(prevTree, nextTree, component.$parent, anchor, component) + // onAfterUpdate + invokeLifecycle(component, 'afterUpdates') + component.$el = nextTree.el + } else { + // mount + patch(null, nextTree, component.$parent, component.vnode.anchor, component) + // after the render is complete, set el to the vnode for comparison + // dummy ? <componentA /> : <componentB /> + component.vnode.el = nextTree.el + component.$el = nextTree.el + component.mounted = true + // onAfterMount + component.effect.allowEffect = true + invokeLifecycle(component, 'afterMounts') + component.effect.allowEffect = false + } +} + +function updateComponentEffect( + component: Component, + ssrMessage: SSRMessage = null +) { + if (component.mounted) { + // if the onBeforeUpdate callback function returns falsy + // no update of the component is performed + if ( + shouldUpdate(invokeLifecycle(component, 'beforeUpdates')) && + shouldUpdate(!component.props.static) + ) { + if (__DEV__) { + refreshComponentType(component.vnode, component) + } + + const prevTree = component.subTree + const nextTree = renderComponent(component) + if (isPromise(nextTree)) { + warn( + 'Asynchronous components without wrapping are not supported, please use FCA wrapping', + component, + 'UpdateComponent' + ) + } else { + patchSubTree(component, prevTree, nextTree) + } + } + } else if (!component.destroyed) { + if (component.vnode.el) { + function hydrateSubTree() { + const nextTree = renderComponent(component) + component.subTree = nextTree as VNode + hydrate(component.vnode.el, component.subTree, component, ssrMessage) + + component.mounted = true + // onAfterMount + invokeLifecycle(component, 'afterMounts') + } + // asynchronous component rendering in ssr mode + if (isAsyncComponent(component.vnode.type)) { + component.vnode.type.__loader(component.props, component).then(() => { + if (!component.destroyed) { + asyncTrackEffect(component.effect) + hydrateSubTree() + clearTrackEffect() + } + }) + } else { + hydrateSubTree() + } + } else { + const nextTree = renderComponent(component) + if (isPromise(nextTree)) { + warn( + 'Asynchronous components without wrapping are not supported, please use FCA wrapping', + component, + 'SetupPatch' + ) + } else { + nextTree.transition ||= component.vnode.transition + patchSubTree(component, null, nextTree) + } + } + } +} + +function renderComponentEffect( + component: Component, + ssrMessage: SSRMessage = null +) { + const effect = (component.effect = createEffect( + updateComponentEffect.bind(null, component, ssrMessage), + () => pushQueueJob(component.update) + )) + + const update = (component.update = effect.run.bind(effect) as SchedulerJob) + update.id = component.uid + update.component = component + update.priority = JobPriority.NORMAL + update() +} + +export function mountComponent( + vnode: VNode<ComponentSetupFunction>, + container: RenderElement, + anchor: RenderElement, + parentComponent: Component, + ssrMessage: SSRMessage = null +) { + vnode.anchor = anchor + + const component = (vnode.component = createComponentInstance( + vnode, + parentComponent + )) + component.$parent = container + + if (__DEV__ && (component.type as any).__hmr_id) { + refreshComponentType(vnode, component) + + const parentId: string = parentComponent + ? (parentComponent.type as any).__hmr_id + : null + collectHmrComponent((component.type as any).__hmr_id, parentId, component) + } + + if (component.props.ref) { + setRef(component.exposed, component.props.ref) + } + + renderComponentEffect(component, ssrMessage) +} + +export function patchComponent( + n1: VNode<ComponentSetupFunction>, + n2: VNode<ComponentSetupFunction>, + container: RenderElement, + anchor: RenderElement, + parentComponent: Component +) { + const component = (n2.component = n1.component) + if (component) { + normalizeComponent(n2, component, parentComponent) + if (isCacheComponent(n1.component.type)) { + if (!isEqual(n1.props, n2.props)) { + component.update() + } + } else { + component.update() + } + } else { + if (__WARN__) { + console.warn('Component update exception', n1) + } + mountComponent(n2, container, anchor, parentComponent) + } +} + +export function enterComponent( + n1: VNode<ComponentSetupFunction> | null, + n2: VNode<ComponentSetupFunction>, + container: RenderElement, + anchor: RenderElement, + parentComponent: Component +) { + if (n1 === null) { + // clear the element of the next vnode to prevent access to the SSR hydrate logic. + n2.el = null + if (isCacheComponent(n2.type)) { + // update the DOM with the locally cached component state when a local component cache is found + const component = getCacheComponent(n2.type) + component.destroyed = false + component.mounted = true + component.vnode = n2 + component.$parent = container + if (isEqual(removeBuiltInProps(component.props), n2.props)) { + patch(null, component.subTree, container, anchor, parentComponent) + } else { + mountComponent(n2, container, anchor, parentComponent) + } + } else { + mountComponent(n2, container, anchor, parentComponent) + } + } else { + n2.anchor ||= n1.anchor + patchComponent(n1, n2, container, anchor, parentComponent) + } +} diff --git a/packages/runtime/src/renderer/element.ts b/packages/runtime/src/renderer/element.ts new file mode 100644 index 0000000..a4546d6 --- /dev/null +++ b/packages/runtime/src/renderer/element.ts @@ -0,0 +1,281 @@ +import { + createElement, + insert, + isSelectElement, + mountProps, + patchProps, +} from '@gyron/dom-client' +import { + shouldValue, + keys, + extend, + isEqual, + isArray, + isFunction, + isObject, +} from '@gyron/shared' +import { isVNodeComponent } from '..' +import { Component, removeBuiltInProps } from '../component' +import { setRef } from '../ref' +import { patch } from '.' +import { + mergeVNodeWith, + normalizeChildrenVNode, + RenderElement, + VNode, +} from '../vnode' +import { patchComponent } from './component' +import { + getNextSibling, + mountChildren, + unmount, + unmountChildren, +} from './shared' + +function transitionMove( + n1: VNode, + n2: VNode, + container: RenderElement, + anchor: RenderElement, + parentComponent: Component, + isSvg: boolean +) { + const { transition } = n1 + const el = n1.el as Element + transition.onLeaveFinish(el) + unmount(n1) + patch( + null, + n2, + container, + anchor || getNextSibling(n1), + parentComponent, + isSvg + ) +} + +function mountElement( + vnode: VNode, + container: RenderElement, + anchor: RenderElement, + parentComponent: Component, + isSvg: boolean +) { + const { tag, is, transition } = vnode + const el = (vnode.el = createElement(tag, isSvg, is) as RenderElement) + el.__vnode__ = vnode + + if (vnode.props.ref) { + setRef(el, vnode.props.ref) + } + + const props = removeBuiltInProps(vnode.props) + if (shouldValue(keys(props))) { + mountProps(el as HTMLElement, extend({}, vnode, { props: props })) + } + + if (shouldValue(vnode.children)) { + vnode.children = normalizeChildrenVNode(vnode) + mountChildren(vnode.children, vnode.el, anchor, 0, parentComponent, isSvg) + } + + insert(el, container, anchor) + + if (transition) { + transition.onActive(el as Element) + } +} + +function isKeyPatch(n1: VNode[], n2: VNode[]) { + if (n1 && n2 && n1[0] && n2[0] && isObject(n1[0]) && isObject(n2[0])) { + return shouldValue(n1[0].key) && shouldValue(n2[0].key) + } + return false +} + +function patchKeyed( + c1: VNode[], + c2: VNode[], + container: RenderElement, + anchor: RenderElement | null, + parentComponent: Component | null, + isSvg: boolean +) { + const o1: Record< + string | symbol, + VNode & { index: number; inserted: boolean } + > = c1.reduce((nodeMap, node, index) => { + nodeMap[node.key] = extend(node, { index }) + return nodeMap + }, {}) + + const e2 = c2.length + let i = 0 + while (i < e2) { + const c2n = c2[i] + const c1n = o1[c2n.key] + if (c1n) { + // 1, find the same key value of the node, and then inserted into the corresponding location. (Do not delete add, move directly) + const el = mergeVNodeWith(c2n, c1n).el + if (c1n.index !== i) { + // insert to new position when node order is changed + const anchor = container.childNodes[i] + if (el !== anchor.nextSibling) { + insert(el, container, anchor.nextSibling) + } + } + // update props after migration is complete + // element update attribute + if (!isEqual(c1n.props, c2n.props)) { + const isComponent = isVNodeComponent(c2n) && isVNodeComponent(c1n) + if (isComponent) { + patchComponent(c1n, c2n, container, anchor, parentComponent) + } else { + patchProps( + el as HTMLElement, + c1n, + extend({}, c2n, { props: removeBuiltInProps(c2n.props) }) + ) + } + } + // 1, end + + if (c1n.children || c2n.children) { + // 2, update the nodes with the same key value, including the child nodes. + // sub-level nodes need to be patched again + patchChildren(c1n, c2n, container, anchor, parentComponent, isSvg) + // 2, end + } + + // mark nodes that have been moved and do not need to be uninstalled in the third step + c1n.inserted = true + } else { + patch(null, c2n, container, anchor, parentComponent, isSvg) + } + i++ + } + + for (const node of Object.values(o1)) { + if (!node.inserted) { + // 3, uninstall the old nodes that are not used. + unmount(node) + // 3, end + } + } +} + +function patchNonKeyed( + c1: VNode[], + c2: VNode[], + container: RenderElement, + anchor: RenderElement, + parentComponent: Component, + isSvg: boolean +) { + const c1length = c1.length + const c2length = c2.length + const minLength = Math.min(c1length, c2length) + for (let i = 0; i < minLength; i++) { + const prevChild = c1[i] + const nextChild = c2[i] + patch(prevChild, nextChild, container, anchor, parentComponent) + } + if (c1length > c2length) { + unmountChildren(c1, minLength) + } else { + mountChildren(c2, container, anchor, minLength, parentComponent, isSvg) + } +} + +export function patchChildren( + n1: VNode, + n2: VNode, + container: RenderElement, + anchor: RenderElement, + parentComponent: Component, + isSvg: boolean +) { + if (isFunction(n1.children) && isFunction(n2.children)) { + // when the child nodes are all functions, they should be called by the parent node. + // <Parent>{count => <Title count={count} />}</Parent> + return + } + + const c1memo = n1.props.memo + const c2memo = n2.props.memo + if (isArray(c1memo) && isArray(c2memo)) { + const index = c1memo.findIndex((item, index) => { + return c2memo[index] !== item + }) + if (index < 0) { + n2.children = n1.children + return + } + } + + const c1 = n1.children as VNode[] + const c2: VNode[] = (n2.children = normalizeChildrenVNode(n2)) + + if (c1?.length || c2?.length) { + if (isKeyPatch(c1, c2)) { + const el = (n2.el = n1.el) + patchKeyed(c1, c2, el || container, anchor, parentComponent, isSvg) + } else { + // if the fragment node does not have a dom instance, use the container + const el = (n2.el = n1.el) + if (c1) { + patchNonKeyed(c1, c2, el || container, anchor, parentComponent, isSvg) + } else { + mountChildren(c2, el || container, anchor, 0, parentComponent, isSvg) + } + } + } +} + +function patchElement( + n1: VNode, + n2: VNode, + container: RenderElement, + anchor: RenderElement, + parentComponent: Component, + isSvg: boolean +) { + const el = (n2.el = n1.el) as Element + if (el.nodeName === n2.tag.toLocaleUpperCase()) { + if (!isEqual(n1.props, n2.props) || isSelectElement(n2)) { + patchProps( + el, + n1, + extend({}, n2, { props: removeBuiltInProps(n2.props) }) + ) + } + if (n1.children || n2.children) { + patchChildren(n1, n2, container, anchor, parentComponent, isSvg) + } + } else { + anchor = getNextSibling(n1) + unmount(n1) + patch(null, n2, container, anchor, parentComponent, isSvg) + } +} + +export function enterElement( + n1: VNode | null, + n2: VNode, + container: RenderElement, + anchor: RenderElement, + parentComponent: Component, + isSvg: boolean +) { + isSvg = isSvg || n2.tag === 'svg' + + if (n1 === null) { + mountElement(n2, container, anchor, parentComponent, isSvg) + } else if (!n2.props.static) { + if (n1.transition) { + transitionMove(n1, n2, container, anchor, parentComponent, isSvg) + } else { + patchElement(n1, n2, container, anchor, parentComponent, isSvg) + } + } +} diff --git a/packages/runtime/src/renderer/fragment.ts b/packages/runtime/src/renderer/fragment.ts new file mode 100644 index 0000000..c20df62 --- /dev/null +++ b/packages/runtime/src/renderer/fragment.ts @@ -0,0 +1,31 @@ +import { createText, insert } from '@gyron/dom-client' +import { VNode, RenderElement, Component, normalizeChildrenVNode } from '..' +import { patchChildren } from './element' +import { mountChildren } from './shared' + +export function enterFragment( + n1: VNode | null, + n2: VNode, + container: RenderElement, + anchor: RenderElement, + parentComponent: Component, + isSvg: boolean +) { + const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : createText('')) + if (n1 === null) { + n2.anchor = fragmentEndAnchor + insert(fragmentEndAnchor, container, anchor) + + n2.children = normalizeChildrenVNode(n2) + mountChildren( + n2.children, + container, + fragmentEndAnchor, + 0, + parentComponent, + isSvg + ) + } else { + patchChildren(n1, n2, container, fragmentEndAnchor, parentComponent, isSvg) + } +} diff --git a/packages/runtime/src/renderer/index.ts b/packages/runtime/src/renderer/index.ts new file mode 100644 index 0000000..b9c18fe --- /dev/null +++ b/packages/runtime/src/renderer/index.ts @@ -0,0 +1,65 @@ +import { unmount, getNextSibling } from './shared' +import { enterText } from './text' +import { enterFragment } from './fragment' +import { enterElement } from './element' +import { enterComment } from './comment' +import { enterComponent, mountComponent } from './component' +import { + VNode, + RenderElement, + Fragment, + Text, + Comment, + Element, +} from '../vnode' +import { Component, ComponentSetupFunction } from '../component' + +export { unmount, mountComponent } + +export function isSameVNodeType(n1: VNode, n2: VNode) { + return n1.type === n2.type && n1.key === n2.key +} + +export function patch( + n1: VNode | null, + n2: VNode, + container: RenderElement, + anchor: RenderElement | null = null, + parentComponent: Component | null = null, + isSvg = false +) { + if (!container) { + throw new Error( + 'The parent element is not found when updating, please check the code.' + ) + } + + if (n1 && !isSameVNodeType(n1, n2)) { + anchor = getNextSibling(n1) + unmount(n1) + n1 = null + } + + switch (n2.type) { + case Text: + enterText(n1, n2, container, anchor) + break + case Comment: + enterComment(n1, n2, container, anchor) + break + case Element: + enterElement(n1, n2, container, anchor, parentComponent, isSvg) + break + case Fragment: + enterFragment(n1, n2, container, anchor, parentComponent, isSvg) + break + default: + enterComponent( + n1 as VNode<ComponentSetupFunction>, + n2 as VNode<ComponentSetupFunction>, + container, + anchor, + parentComponent + ) + } +} diff --git a/packages/runtime/src/renderer/shared.ts b/packages/runtime/src/renderer/shared.ts new file mode 100644 index 0000000..9938c07 --- /dev/null +++ b/packages/runtime/src/renderer/shared.ts @@ -0,0 +1,91 @@ +import { isArray, isElement, Noop } from '@gyron/shared' +import { isVNode } from '../shared' +import { Component, isCacheComponent } from '../component' +import { invokeLifecycle } from '../lifecycle' +import { Children, normalizeVNode, RenderElement, VNode } from '../vnode' +import { nextSibling, remove } from '@gyron/dom-client' +import { patch } from '.' + +function removeInvoke(_el: RenderElement, vnode: VNode, done: Noop) { + const { transition } = vnode + const el = _el as Element + if (transition) { + transition.onLeave(el, () => { + remove(el) + done() + }) + } else { + remove(el) + done() + } +} + +export function unmountChildren(c1: VNode[], start = 0) { + for (let i = start; i < c1.length; i++) { + unmount(c1[i]) + } +} + +export function mountChildren( + nodes: VNode[] | Children[], + container: RenderElement, + anchor: RenderElement, + start = 0, + parentComponent: Component | null = null, + isSvg: boolean +) { + for (let i = start; i < nodes.length; i++) { + const node = normalizeVNode(nodes[i]) + patch(null, node, container, anchor, parentComponent, isSvg) + } +} + +export function getNextSibling(vnode: VNode) { + if (vnode.component) { + return getNextSibling(vnode.component.subTree) + } + if (vnode.el || vnode.anchor) { + return nextSibling(vnode.el || vnode.anchor) + } + return null +} + +export function unmount(vnode: VNode) { + if (!isVNode(vnode)) { + return null + } + + function reset() { + vnode.el = null + } + const { el, component, children, transition } = vnode + + if (component) { + if (!isCacheComponent(component.type)) { + component.effect.stop() + } + if (component.subTree) { + unmount(component.subTree) + } + invokeLifecycle(component, 'destroyed') + if (component.$el) { + removeInvoke(component.$el, vnode, reset) + if (!isCacheComponent(component.type)) { + component.$el = null + } + } + component.destroyed = true + component.mounted = false + } else { + if (!transition) { + if (isArray(children) && children.length > 0) { + unmountChildren(children as VNode[]) + } else { + unmount(children as VNode) + } + } + if (isElement(el)) { + removeInvoke(el, vnode, reset) + } + } +} diff --git a/packages/runtime/src/renderer/text.ts b/packages/runtime/src/renderer/text.ts new file mode 100644 index 0000000..3024786 --- /dev/null +++ b/packages/runtime/src/renderer/text.ts @@ -0,0 +1,28 @@ +import { createText, insert } from '@gyron/dom-client' +import { RenderElement, VNode } from '../vnode' + +export function enterText( + n1: VNode | null, + n2: VNode, + container: RenderElement, + anchor: RenderElement +) { + if (n1 === null || !n1.el) { + // when hydrating the code, since there is no empty text node on the server side, you need to execute mountText + const textNode = createText(n2.children as string) as RenderElement + + textNode.__vnode__ = n2 + n2.el = textNode + + insert(textNode, container, anchor) + } else { + const el = (n2.el = n1.el) + + const c1 = '' + n1.children + const c2 = '' + n2.children + + if (c1 !== c2) { + el.textContent = c2 + } + } +} diff --git a/packages/runtime/src/vnode.ts b/packages/runtime/src/vnode.ts index 5e80665..8552ae2 100644 --- a/packages/runtime/src/vnode.ts +++ b/packages/runtime/src/vnode.ts @@ -15,7 +15,7 @@ import { ComponentSetupFunction, } from './component' import { UserRef } from './ref' -import { TransitionHooks } from './Transition' +import { TransitionHooks } from './internal' export const Gyron = Symbol('gyron') export const Text = Symbol('gyron.text') diff --git a/packages/runtime/tests/handler.spec.ts b/packages/runtime/tests/handler.spec.ts index a7a5e91..c43a67b 100644 --- a/packages/runtime/tests/handler.spec.ts +++ b/packages/runtime/tests/handler.spec.ts @@ -9,7 +9,7 @@ import { getCurrentComponent, manualWarnHandler, } from '../src' -import { ErrorBoundary } from '../src/ErrorBoundary' +import { ErrorBoundary } from '../src/internal' describe('Handler Error', () => { const container = document.createElement('div') diff --git a/packages/runtime/tests/transition.spec.ts b/packages/runtime/tests/transition.spec.ts index ba1526d..c6d234f 100644 --- a/packages/runtime/tests/transition.spec.ts +++ b/packages/runtime/tests/transition.spec.ts @@ -1,6 +1,6 @@ import { sleep, sleepWithRequestFrame } from '@gyron/shared' import { createInstance, h, nextRender, Transition } from '../src' -import { whenTransitionEnd } from '../src/Transition' +import { whenTransitionEnd } from '../src/internal' describe('transition component', () => { const container = document.createElement('div') diff --git a/packages/shared/package.json b/packages/shared/package.json index 5233ea5..b1c9c2b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -17,6 +17,6 @@ "build:esm": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/esm/index.js --format=esm --platform=node", "build:cjs": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/cjs/index.js --format=cjs --platform=node", "build:dts": "rollup -c ../../rollup.config.js", - "test": "jest --config=../../jest.config.js" + "test": "cross-env PACKAGES=shared jest --config=../../jest.config.js" } } diff --git a/packages/sync/package.json b/packages/sync/package.json index 45ac908..a6609e6 100644 --- a/packages/sync/package.json +++ b/packages/sync/package.json @@ -17,7 +17,7 @@ "build:esm": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/esm/index.js --format=esm --external:@gyron/* --platform=node", "build:cjs": "esbuild src/index.ts --bundle --sourcemap --minify --outfile=dist/cjs/index.js --format=cjs --external:@gyron/* --platform=node", "build:dts": "rollup -c ../../rollup.config.js", - "test": "jest --config=../../jest.config.js" + "test": "cross-env PACKAGES=sync jest --config=../../jest.config.js" }, "devDependencies": { "@gyron/shared": "^0.0.29",