diff --git a/libs/horizon/src/renderer/ContextSaver.ts b/libs/horizon/src/renderer/ContextSaver.ts new file mode 100644 index 00000000..b5200eaa --- /dev/null +++ b/libs/horizon/src/renderer/ContextSaver.ts @@ -0,0 +1,147 @@ +/** + * 保存与深度遍历相关的一些context。 + * 在深度遍历过程中,begin阶段会修改一些全局的值,在complete阶段会恢复。 + */ + +import type {VNode, ContextType} from './Types'; +import type {Container} from '../dom/DOMOperator'; + +import {getNSCtx} from '../dom/DOMOperator'; +import {ContextProvider} from './vnode/VNodeTags'; + +// 保存的是“http://www.w3.org/1999/xhtml”或“http://www.w3.org/2000/svg”, +// 用于识别是使用document.createElement()还是使用document.createElementNS()创建DOM +const CTX_NAMESPACE = 'CTX_NAMESPACE'; + +// 保存的是Horizon.createContext()的值,或Provider重新设置的值 +const CTX_CONTEXT = 'CTX_CONTEXT'; + +// 旧版context API,是否更改。 +const CTX_OLD_CHANGE = 'CTX_OLD_CHANGE'; +// 旧版context API,保存的是的当前组件提供给子组件使用的context。 +const CTX_OLD_CONTEXT = 'CTX_OLD_CONTEXT'; +// 旧版context API,保存的是的上一个提供者提供给后代组件使用的context。 +const CTX_OLD_PREVIOUS_CONTEXT = 'CTX_OLD_PREVIOUS_CONTEXT'; + +let ctxNamespace: string = ''; + +let ctxOldContext: Object = {}; +let ctxOldChange: Boolean = false; +let ctxOldPreviousContext: Object = {}; + +// capture阶段设置 +function setNamespaceCtx(vNode: VNode, dom?: Container) { + const nextContext = getNSCtx(dom, ctxNamespace, vNode.type); + + vNode.setContext(CTX_NAMESPACE, ctxNamespace); + ctxNamespace = nextContext; +} + +// bubble阶段恢复 +function resetNamespaceCtx(vNode: VNode) { + ctxNamespace = vNode.getContext(CTX_NAMESPACE); +} + +function getNamespaceCtx(): string { + return ctxNamespace; +} + +function setContextCtx(providerVNode: VNode, nextValue: T) { + const context: ContextType = providerVNode.type._context; + + providerVNode.setContext(CTX_CONTEXT, context.value); + context.value = nextValue; +} + +function resetContextCtx(providerVNode: VNode) { + const context: ContextType = providerVNode.type._context; + + context.value = providerVNode.getContext(CTX_CONTEXT); +} + +// 在局部更新时,恢复父节点的context +function recoverParentsContextCtx(vNode: VNode) { + let parent = vNode.parent; + + while (parent !== null) { + if (parent.tag === ContextProvider) { + const newValue = parent.props.value; + setContextCtx(parent, newValue); + } + parent = parent.parent; + } +} + +// ctxOldContext是 旧context提供者的context +function setVNodeOldContext(providerVNode: VNode, context: Object) { + providerVNode.setContext(CTX_OLD_CONTEXT, context); +} + +function getVNodeOldContext(vNode: VNode) { + return vNode.getContext(CTX_OLD_CONTEXT); +} + +function setOldContextCtx(providerVNode: VNode, context: Object) { + setVNodeOldContext(providerVNode, context); + ctxOldContext = context; +} + +function getOldContextCtx() { + return ctxOldContext; +} + +function resetOldContextCtx(vNode: VNode) { + ctxOldContext = getVNodeOldContext(vNode); +} + +function setVNodeOldPreviousContext(providerVNode: VNode, context: Object) { + providerVNode.setContext(CTX_OLD_PREVIOUS_CONTEXT, context); +} + +function getVNodeOldPreviousContext(vNode: VNode) { + return vNode.getContext(CTX_OLD_PREVIOUS_CONTEXT); +} + +function setOldPreviousContextCtx(context: Object) { + ctxOldPreviousContext = context; +} + +function getOldPreviousContextCtx() { + return ctxOldPreviousContext; +} + +function setContextChangeCtx(providerVNode: VNode, didChange: boolean) { + providerVNode.setContext(CTX_OLD_CHANGE, didChange); + ctxOldChange = didChange; +} + +function getContextChangeCtx() { + return ctxOldChange; +} + +function resetContextChangeCtx(vNode: VNode) { + ctxOldChange = vNode.getContext(CTX_OLD_CHANGE); +} + +export { + getNamespaceCtx, + resetNamespaceCtx, + setNamespaceCtx, + setContextCtx, + resetContextCtx, + recoverParentsContextCtx, + setOldContextCtx, + getOldContextCtx, + resetOldContextCtx, + setContextChangeCtx, + getContextChangeCtx, + resetContextChangeCtx, + setOldPreviousContextCtx, + getOldPreviousContextCtx, + setVNodeOldContext, + getVNodeOldContext, + setVNodeOldPreviousContext, + getVNodeOldPreviousContext, +}; + + diff --git a/libs/horizon/src/renderer/ErrorHandler.ts b/libs/horizon/src/renderer/ErrorHandler.ts new file mode 100644 index 00000000..f4e66484 --- /dev/null +++ b/libs/horizon/src/renderer/ErrorHandler.ts @@ -0,0 +1,189 @@ +/** + * 异常错误处理 + */ + +import type {VNode} from './Types'; +import type {Update} from './UpdateHandler'; + +import {ClassComponent, TreeRoot} from './vnode/VNodeTags'; +import {FlagUtils, Interrupted} from './vnode/VNodeFlags'; +import {newUpdate, UpdateState, pushUpdate} from './UpdateHandler'; +import {launchUpdateFromVNode, setBuildResultError, tryRenderRoot} from './TreeBuilder'; +import {setRootThrowError} from './submit/Submit'; +import {handleSuspenseChildThrowError} from './render/SuspenseComponent'; +import {updateShouldUpdateOfTree} from './vnode/VNodeShouldUpdate'; + +// 处理capture和bubble阶段抛出的错误 +export function handleRenderThrowError( + sourceVNode: VNode, + error: any, +) { + // vNode抛出了异常,标记Interrupted中断 + FlagUtils.markInterrupted(sourceVNode); + // dirtyNodes 不再有效 + sourceVNode.dirtyNodes = []; + + // error是个promise + if (error !== null && typeof error === 'object' && typeof error.then === 'function') { + // 抛出异常的节点,向上寻找,是否有suspense组件 + const foundSuspense = handleSuspenseChildThrowError(sourceVNode.parent, sourceVNode, error); + if (foundSuspense) { + return; + } + } + + // 抛出错误无法作为suspense内容处理(或无suspense来处理),这次当成真的错误来处理 + setBuildResultError(); + + // 向上遍历寻找ClassComponent组件(同时也是Error Boundaries组件) 或者 TreeRoot + let vNode = sourceVNode.parent; + do { + switch (vNode.tag) { + case TreeRoot: { + vNode.shouldUpdate = true; + launchUpdateFromVNode(vNode); + handleRootError(error); + return; + } + case ClassComponent: + const ctor = vNode.type; + const instance = vNode.realNode; + if ( + !vNode.flags.DidCapture && + ( + typeof ctor.getDerivedStateFromError === 'function' || + (instance !== null && typeof instance.componentDidCatch === 'function') + ) + ) { + FlagUtils.markShouldCapture(vNode); + + // Class捕捉到异常,触发一次刷新 + const update = createClassErrorUpdate(vNode, error); + pushUpdate(vNode, update); + + launchUpdateFromVNode(vNode); + + // 有异常处理类,把抛出异常的节点的Interrupted标志去掉,继续走正常的绘制流程 + FlagUtils.removeFlag(sourceVNode, Interrupted); + + return; + } + break; + default: + break; + } + vNode = vNode.parent; + } while (vNode !== null); +} + +// 处理submit阶段的异常 +export function handleSubmitError(vNode: VNode, error: any) { + if (vNode.tag === TreeRoot) { + handleRootError(error); + return; + } + + let node = vNode.parent; + // 向上遍历 + while (node !== null) { + if (node.tag === TreeRoot) { + handleRootError(error); + return; + } else if (node.tag === ClassComponent) { // 只有 class 组件才可以成为错误边界组件 + const ctor = node.type; + const instance = node.realNode; + if ( + typeof ctor.getDerivedStateFromError === 'function' || + typeof instance.componentDidCatch === 'function' + ) { + const getDerivedStateFromError = node.type.getDerivedStateFromError; + if (typeof getDerivedStateFromError === 'function') { + // 打印错误 + consoleError(error); + + const retState = getDerivedStateFromError(error); + if (retState) { // 有返回值 + // 触发更新 + triggerUpdate(node, retState); + } + } + + // 处理componentDidCatch + if (instance !== null && typeof instance.componentDidCatch === 'function') { + if (typeof getDerivedStateFromError !== 'function') { // 没有getDerivedStateFromError + // 打印错误 + consoleError(error); + } + + instance.componentDidCatch(error, { + componentStack: '', + }); + } + + return; + } + } + node = node.parent; + } +} + +function createClassErrorUpdate( + vNode: VNode, + error: any, +): Update { + const update = newUpdate(); + update.type = UpdateState.Error; + + const getDerivedStateFromError = vNode.type.getDerivedStateFromError; + if (typeof getDerivedStateFromError === 'function') { + update.content = () => { + consoleError(error); + return getDerivedStateFromError(error); + }; + } + + const inst = vNode.realNode; + if (inst !== null && typeof inst.componentDidCatch === 'function') { + update.callback = function callback() { + if (typeof getDerivedStateFromError !== 'function') { + // 打印错误 + consoleError(error); + } + + // @ts-ignore + this.componentDidCatch(error, { + componentStack: '', + }); + }; + } + return update; +} + +// 新增一个update,并且触发调度 +function triggerUpdate(vNode, state) { + const update = newUpdate(); + update.content = state; + pushUpdate(vNode, update); + + const root = updateShouldUpdateOfTree(vNode); + if (root !== null) { + tryRenderRoot(root); + } +} + +function handleRootError( + error: any, +) { + // 注意:如果根节点抛出错误,不会销毁整棵树,只打印日志,抛出异常。 + setRootThrowError(error); + consoleError(error); +} + +function consoleError(error: any): void { + if (isDev) { + // 只打印message为了让测试用例能pass + console['error']('The codes throw the error: ' + error.message); + } else { + console['error'](error); + } +} diff --git a/libs/horizon/src/renderer/ExecuteMode.ts b/libs/horizon/src/renderer/ExecuteMode.ts new file mode 100644 index 00000000..880e9374 --- /dev/null +++ b/libs/horizon/src/renderer/ExecuteMode.ts @@ -0,0 +1,33 @@ + +export const ByAsync = 'BY_ASYNC'; +export const BySync = 'BY_SYNC'; +export const InRender = 'IN_RENDER'; + +type RenderMode = typeof ByAsync | typeof BySync | typeof InRender; + +// 当前执行阶段标记 +let executeMode = { + [ByAsync]: false, + [BySync]: false, + [InRender]: false, +}; + +export function changeMode(mode: RenderMode, state = true) { + executeMode[mode] = state; +} + +export function checkMode(mode: RenderMode) { + return executeMode[mode]; +} + +export function isExecuting() { + return executeMode[ByAsync] || executeMode[BySync] || executeMode[InRender]; +} + +export function copyExecuteMode() { + return {...executeMode}; +} + +export function setExecuteMode(mode: typeof executeMode) { + executeMode = mode; +} diff --git a/libs/horizon/src/renderer/Renderer.ts b/libs/horizon/src/renderer/Renderer.ts new file mode 100644 index 00000000..85fc3d4b --- /dev/null +++ b/libs/horizon/src/renderer/Renderer.ts @@ -0,0 +1,48 @@ +import type {VNode} from './Types'; +import type {Update} from './UpdateHandler'; + +import { + asyncUpdates, + syncUpdates, + runDiscreteUpdates, + launchUpdateFromVNode, +} from './TreeBuilder'; +import {runAsyncEffects} from './submit/HookEffectHandler'; +import {Callback, newUpdate, pushUpdate} from './UpdateHandler'; +import {getFirstChild} from './vnode/VNodeUtils'; + +export {createVNode} from './vnode/VNodeCreator'; +export {createPortal} from './components/CreatePortal'; +export { + asyncUpdates, + syncUpdates, + runDiscreteUpdates, + runAsyncEffects, +}; + +export function startUpdate( + element: any, + treeRoot: VNode, + callback?: Callback, +) { + const update: Update = newUpdate(); + update.content = {element}; + + if (typeof callback === 'function') { + update.callback = callback; + } + + pushUpdate(treeRoot, update); + + launchUpdateFromVNode(treeRoot); +} + +export function getFirstCustomDom(treeRoot: VNode): Element | Text | null { + const firstChild = getFirstChild(treeRoot); + if (!firstChild) { + return null; + } + + return firstChild.realNode; +} + diff --git a/libs/horizon/src/renderer/Types.ts b/libs/horizon/src/renderer/Types.ts new file mode 100644 index 00000000..b1732fd1 --- /dev/null +++ b/libs/horizon/src/renderer/Types.ts @@ -0,0 +1,58 @@ +export { VNode } from './vnode/VNode'; + +type Trigger = (A) => void; + +export type ReadContextHookType = { readContext(context: ContextType): T }; +export type UseStateHookType = { + useState( + initialState: (() => S) | S + ): [S, Trigger<((S) => S) | S>] +}; +export type UseReducerHookType = { + useReducer( + reducer: (S, A) => S, + initArg: P, init?: (P) => S, + ): [S, Trigger] +}; +export type UseContextHookType = { useContext(context: ContextType,): T }; + +export type HorizonElement = { + vtype: any, + type: any, + key: any, + ref: any, + props: any, + + _vNode: any, +}; + +export type ProviderType = { + vtype: number; + _context: ContextType; +}; + +export type ContextType = { + vtype: number; + Consumer: ContextType; + Provider: ProviderType; + value: T; +}; + +export type PortalType = { + vtype: number; + key: null | string; + outerDom: any; + children: any; +}; + +export type RefType = { + current: any; +}; + +export interface PromiseType { + then( + onFulfill: (value: R) => void | PromiseType | U, + onReject: (error: any) => void | PromiseType | U, + ): void | PromiseType; +} + diff --git a/libs/horizon/src/renderer/components/BaseClassComponent.ts b/libs/horizon/src/renderer/components/BaseClassComponent.ts index 12dab04e..ec141650 100644 --- a/libs/horizon/src/renderer/components/BaseClassComponent.ts +++ b/libs/horizon/src/renderer/components/BaseClassComponent.ts @@ -1,11 +1,12 @@ /** * Component的api setState和forceUpdate在实例生成阶段实现 */ -class Component { - props; - context; +class Component { + props: P; + context: C; + state: S; - constructor(props, context) { + constructor(props: P, context: C) { this.props = props; this.context = context; } @@ -16,8 +17,8 @@ Component.prototype.isReactComponent = true; /** * 支持PureComponent */ -class PureComponent extends Component { - constructor(props, context) { +class PureComponent extends Component { + constructor(props: P, context: C) { super(props, context); } } diff --git a/libs/horizon/src/renderer/components/Lazy.ts b/libs/horizon/src/renderer/components/Lazy.ts index 55a276c5..4b37091e 100644 --- a/libs/horizon/src/renderer/components/Lazy.ts +++ b/libs/horizon/src/renderer/components/Lazy.ts @@ -15,7 +15,7 @@ type LazyContent = { }; export type LazyComponent = { - vtype: Symbol | number, + vtype: number, _content: P, _load: (content: P) => T, }; diff --git a/libs/horizon/src/renderer/hooks/BaseHook.ts b/libs/horizon/src/renderer/hooks/BaseHook.ts index ffbd3e6a..18e59db1 100644 --- a/libs/horizon/src/renderer/hooks/BaseHook.ts +++ b/libs/horizon/src/renderer/hooks/BaseHook.ts @@ -3,7 +3,8 @@ import type {Hook} from './HookType'; let processingVNode: VNode = null; -let activatedHook: Hook | null = null; +// lastTimeHook是上一次执行func时产生的hooks中,与currentHook对应的hook +let lastTimeHook: Hook | null = null; // 当前hook函数对应的hook对象 let currentHook: Hook | null = null; @@ -16,12 +17,12 @@ export function setProcessingVNode(vNode: VNode) { processingVNode = vNode; } -export function getActivatedHook() { - return activatedHook; +export function getLastTimeHook() { + return lastTimeHook; } -export function setActivatedHook(hook: Hook) { - activatedHook = hook; +export function setLastTimeHook(hook: Hook) { + lastTimeHook = hook; } export function setCurrentHook(hook: Hook) { @@ -47,24 +48,32 @@ export function createHook(state: any = null): Hook { return currentHook; } -export function getNextHook(hook: Hook, vNode: VNode) { - return vNode.hooks[hook.hIndex + 1] || null; +export function getNextHook(hook: Hook, hooks: Array>) { + return hooks[hook.hIndex + 1] || null; } // 获取当前hook函数对应的hook对象。 -// processing中的hook和activated中的hook,需要同时往前走, -// 原因:1.比对hook的数量有没有变化(非必要);2.从activated中的hook获取removeEffect +// processing中的hook和上一次执行中的hook,需要同时往前走, +// 原因:1.比对hook的数量有没有变化(非必要);2.从上一次执行中的hook获取removeEffect export function getCurrentHook(): Hook { - currentHook = currentHook !== null ? getNextHook(currentHook, processingVNode) : (processingVNode.hooks[0] || null); - const activated = processingVNode.twins; - activatedHook = activatedHook !== null ? getNextHook(activatedHook, activated) : ((activated && activated.hooks[0]) || null); + currentHook = currentHook !== null ? getNextHook(currentHook, processingVNode.hooks) : (processingVNode.hooks[0] || null); + + if (lastTimeHook !== null) { + lastTimeHook = getNextHook(lastTimeHook, processingVNode.oldHooks); + } else { + if (processingVNode.oldHooks && processingVNode.oldHooks.length) { + lastTimeHook = processingVNode.oldHooks[0]; + } else { + lastTimeHook = null; + } + } if (currentHook === null) { - if (activatedHook === null) { + if (lastTimeHook === null) { throw Error('Hooks are more than expected, please check whether the hook is written in the condition.'); } - createHook(activatedHook.state); + createHook(lastTimeHook.state); } return currentHook; diff --git a/libs/horizon/src/renderer/hooks/HookMain.ts b/libs/horizon/src/renderer/hooks/HookMain.ts index 713696d3..e8992e5a 100644 --- a/libs/horizon/src/renderer/hooks/HookMain.ts +++ b/libs/horizon/src/renderer/hooks/HookMain.ts @@ -9,9 +9,9 @@ const { import {getNewContext} from '../components/context/Context'; import { - getActivatedHook, + getLastTimeHook, getProcessingVNode, - setActivatedHook, + setLastTimeHook, setProcessingVNode, setCurrentHook, getNextHook } from './BaseHook'; @@ -24,7 +24,6 @@ export function exeFunctionHook( funcComp: (props: Object, arg: Object) => any, props: Object, arg: Object, - activated: VNode | null, processing: VNode, ): any { // 重置全局变量 @@ -35,11 +34,12 @@ export function exeFunctionHook( setProcessingVNode(processing); + processing.oldHooks = processing.hooks; processing.hooks = []; processing.effectList = []; // 设置hook阶段 - if (activated === null || !activated.hooks.length) { + if (processing.isCreated || !processing.oldHooks.length) { setHookStage(HookStage.Init); } else { setHookStage(HookStage.Update); @@ -51,9 +51,9 @@ export function exeFunctionHook( setHookStage(null); // 判断hook是否写在了if条件中,如果在if中会出现数量不对等的情况 - const activatedHook = getActivatedHook(); - if (activatedHook !== null) { - if (getNextHook(getActivatedHook(), activated) !== null) { + const lastTimeHook = getLastTimeHook(); + if (lastTimeHook !== null) { + if (getNextHook(getLastTimeHook(), processing.oldHooks) !== null) { throw Error('Hooks are less than expected, please check whether the hook is written in the condition.'); } } @@ -67,7 +67,7 @@ export function exeFunctionHook( function resetGlobalVariable() { setHookStage(null); setProcessingVNode(null); - setActivatedHook(null); + setLastTimeHook(null); setCurrentHook(null); } diff --git a/libs/horizon/src/renderer/hooks/HookType.ts b/libs/horizon/src/renderer/hooks/HookType.ts index 984b5ecd..e965fdf1 100644 --- a/libs/horizon/src/renderer/hooks/HookType.ts +++ b/libs/horizon/src/renderer/hooks/HookType.ts @@ -1,5 +1,4 @@ import {EffectConstant} from './EffectConstant'; -import {VNode} from '../Types'; export interface Hook { state: Reducer | Effect | Memo | CallBack | Ref; diff --git a/libs/horizon/src/renderer/hooks/UseEffectHook.ts b/libs/horizon/src/renderer/hooks/UseEffectHook.ts index c777f860..ccd4e7d4 100644 --- a/libs/horizon/src/renderer/hooks/UseEffectHook.ts +++ b/libs/horizon/src/renderer/hooks/UseEffectHook.ts @@ -1,7 +1,7 @@ import { createHook, getCurrentHook, - getActivatedHook, + getLastTimeHook, getProcessingVNode, throwNotInFuncError } from './BaseHook'; import {FlagUtils} from '../vnode/VNodeFlags'; @@ -38,10 +38,10 @@ function useEffect( } export function useEffectForInit(effectFunc, deps, effectType): void { - const hook = createHook(); - const nextDeps = deps !== undefined ? deps : null; FlagUtils.markUpdate(getProcessingVNode()); + const hook = createHook(); + const nextDeps = deps !== undefined ? deps : null; // 初始阶段,设置DepsChange标记位; 构造EffectList数组,并赋值给state hook.state = createEffect(effectFunc, undefined, nextDeps, EffectConstant.DepsChange | effectType); } @@ -51,13 +51,13 @@ export function useEffectForUpdate(effectFunc, deps, effectType): void { const nextDeps = deps !== undefined ? deps : null; let removeFunc; - if (getActivatedHook() !== null) { - const effect = getActivatedHook().state as Effect; - // removeEffect是通过执行effect返回的,所以需要在activated中获取 + if (getLastTimeHook() !== null) { + const effect = getLastTimeHook().state as Effect; + // removeEffect是通过执行effect返回的,所以需要在上一次hook中获取 removeFunc = effect.removeEffect; const lastDeps = effect.dependencies; - // 判断dependencies是否相同,不同就不设置DepsChange标记位 + // 判断dependencies是否相同,同相同不需要设置DepsChange标记位 if (nextDeps !== null && isArrayEqual(nextDeps, lastDeps)) { hook.state = createEffect(effectFunc, removeFunc, nextDeps, effectType); return; diff --git a/libs/horizon/src/renderer/hooks/UseReducerHook.ts b/libs/horizon/src/renderer/hooks/UseReducerHook.ts index 844afb0a..4c241152 100644 --- a/libs/horizon/src/renderer/hooks/UseReducerHook.ts +++ b/libs/horizon/src/renderer/hooks/UseReducerHook.ts @@ -54,10 +54,9 @@ function insertUpdate(action: A, hook: Hook): Update { // setState, setReducer触发函数 export function TriggerAction(vNode: VNode, hook: Hook, action: A) { const newUpdate = insertUpdate(action, hook); - const twins = vNode.twins; // 判断是否需要刷新 - if (!vNode.shouldUpdate && (twins === null || !twins.shouldUpdate)) { + if (!vNode.shouldUpdate) { const reducerObj = hook.state as Reducer; const { stateValue, reducer } = reducerObj; diff --git a/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts b/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts index 49e0c98d..f5a8b986 100644 --- a/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts +++ b/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts @@ -80,7 +80,7 @@ function callTasks(initialTime) { task.expirationTime > currentTime && isOverTime() ) { - // 没到deadline + // 任务的过期时间大于当前时间(没达到此任务的过期时间)且超过了deadline break; } diff --git a/libs/horizon/src/renderer/utils/compare.ts b/libs/horizon/src/renderer/utils/compare.ts new file mode 100644 index 00000000..3849ec85 --- /dev/null +++ b/libs/horizon/src/renderer/utils/compare.ts @@ -0,0 +1,51 @@ +/** + * 兼容IE浏览器没有Object.is + */ +export function isSame(x: any, y: any) { + if (!(typeof Object.is === 'function')) { + if (x === y) { + // +0 != -0 + return x !== 0 || 1 / x === 1 / y; + } else { + // NaN == NaN + return x !== x && y !== y; + } + } else { + return Object.is(x, y); + } +} + +export function isArrayEqual(nextParam: Array, lastParam: Array | null) { + if (lastParam === null || lastParam.length !== nextParam.length) { + return false; + } + for (let i = 0; i < lastParam.length; i++) { + if (!isSame(nextParam[i], lastParam[i])) { + return false; + } + } + return true; +} + +export function shallowCompare(paramX: any, paramY: any): boolean { + if (isSame(paramX, paramY)) { + return true; + } + + // 对比对象 + if (typeof paramX === 'object' && typeof paramY === 'object' && paramX !== null && paramY !== null) { + const keysX = Object.keys(paramX); + const keysY = Object.keys(paramY); + + // key长度不相等时直接返回不相等 + if (keysX.length !== keysY.length) { + return false; + } + + return keysX.every((key, i) => { + return Object.prototype.hasOwnProperty.call(paramY, key) && isSame(paramX[key], paramY[keysX[i]]); + }); + } + + return false; +} diff --git a/libs/horizon/src/renderer/utils/elementType.ts b/libs/horizon/src/renderer/utils/elementType.ts new file mode 100644 index 00000000..b23ad4d6 --- /dev/null +++ b/libs/horizon/src/renderer/utils/elementType.ts @@ -0,0 +1,11 @@ +export const TYPE_ELEMENT = 1; +export const TYPE_PORTAL = 2; +export const TYPE_FRAGMENT = 3; +export const TYPE_STRICT_MODE = 4; +export const TYPE_PROVIDER = 5; +export const TYPE_CONTEXT = 6; +export const TYPE_FORWARD_REF = 7; +export const TYPE_SUSPENSE = 8; +export const TYPE_PROFILER = 9; +export const TYPE_MEMO = 10; +export const TYPE_LAZY = 11; diff --git a/libs/horizon/src/renderer/utils/throwIfTrue.ts b/libs/horizon/src/renderer/utils/throwIfTrue.ts new file mode 100644 index 00000000..5cf8522e --- /dev/null +++ b/libs/horizon/src/renderer/utils/throwIfTrue.ts @@ -0,0 +1,13 @@ +// 当条件不成立报错 +// 接收模板 +export function throwIfTrue(condition: boolean, errTemplate: string, ...errExpressions: string[]) { + if (condition) { + // 将%s 替换成对应的变量 + const msg = errTemplate.split('%s').reduce((prevSentence: string, part: string, idx: number) => { + // %s对应的变量 + const expression = idx < errExpressions.length ? errExpressions[idx] : '' ; + return prevSentence + part + expression; + }, ''); + throw Error(msg); + } +} diff --git a/libs/horizon/src/renderer/vnode/VNodeCreator.ts b/libs/horizon/src/renderer/vnode/VNodeCreator.ts deleted file mode 100644 index e648e3b1..00000000 --- a/libs/horizon/src/renderer/vnode/VNodeCreator.ts +++ /dev/null @@ -1,191 +0,0 @@ -import type { VNodeTag } from './VNodeTags'; -import { FlagUtils } from './VNodeFlags'; -import { - ClassComponent, - ContextConsumer, - ContextProvider, - ForwardRef, - Fragment, - FunctionComponent, - DomComponent, - DomPortal, - TreeRoot, - DomText, - ClsOrFunComponent, - LazyComponent, - MemoComponent, - SuspenseComponent, -} from './VNodeTags'; -import { createUpdateArray } from '../UpdateHandler'; -import { - TYPE_CONTEXT, - TYPE_FORWARD_REF, TYPE_FRAGMENT, - TYPE_LAZY, - TYPE_MEMO, TYPE_PROFILER, - TYPE_PROVIDER, TYPE_STRICT_MODE, - TYPE_SUSPENSE, -} from '../utils/elementType'; -import { VNode } from './VNode'; -import {HorizonElement} from '../Types'; - -const typeLazyMap = { - [TYPE_FORWARD_REF]: ForwardRef, - [TYPE_MEMO]: MemoComponent, -}; -const typeMap = { - ...typeLazyMap, - [TYPE_PROVIDER]: ContextProvider, - [TYPE_CONTEXT]: ContextConsumer, - [TYPE_LAZY]: LazyComponent, -}; - -const newVirtualNode = function(tag: VNodeTag, key?: null | string, vNodeProps?: any, outerDom?: any): VNode { - return new VNode(tag, vNodeProps, key, outerDom); -}; - -function isClassComponent(comp: Function) { - // 如果使用 getPrototypeOf 方法获取构造函数,不能兼容业务组组件继承组件的使用方式,会误认为是函数组件 - // 如果使用静态属性,部分函数高阶组件会将类组件的静态属性复制到自身,导致误判为类组件 - // 既然已经兼容使用了该标识符,那么继续使用 - return comp.prototype?.isReactComponent === true; -} - -// 解析懒组件的tag -export function getLazyVNodeTag(lazyComp: any): string { - let vNodeTag = ClsOrFunComponent; - if (typeof lazyComp === 'function') { - vNodeTag = isClassComponent(lazyComp) ? ClassComponent : FunctionComponent; - } else if (lazyComp !== undefined && lazyComp !== null && typeLazyMap[lazyComp.vtype]) { - vNodeTag = typeLazyMap[lazyComp.vtype]; - } - return vNodeTag; -} - -// 创建processing -export function updateVNode(vNode: VNode, vNodeProps?: any): VNode { - if (vNode.tag === ClassComponent) { - vNode.oldState = vNode.state; - } - - if (vNode.tag === SuspenseComponent) { - vNode.oldSuspenseChildStatus = vNode.suspenseChildStatus; - vNode.oldChildren = vNode.children; - } - - vNode.oldProps = vNode.props; - vNode.props = vNodeProps; - - vNode.oldRef = vNode.ref; - - FlagUtils.setNoFlags(vNode); - vNode.dirtyNodes = []; - vNode.isCreated = false; - - return vNode; -} - -function getVNodeTag(type: any) { - let vNodeTag = ClsOrFunComponent; - let isLazy = false; - - if (typeof type === 'function') { - if (isClassComponent(type)) { - vNodeTag = ClassComponent; - } - } else if (typeof type === 'string') { - vNodeTag = DomComponent; - } else if (type === TYPE_SUSPENSE) { - vNodeTag = SuspenseComponent; - } else if (typeof type === 'object' && type !== null && typeMap[type.vtype]) { - vNodeTag = typeMap[type.vtype]; - isLazy = type.vtype === TYPE_LAZY; - } else { - throw Error(`Component type is invalid, got: ${type == null ? type : typeof type}`); - } - return { vNodeTag, isLazy }; -} - -export function createVNode(tag: VNodeTag | string, ...secondArg) { - let vNode = null; - switch (tag) { - case Fragment: - const [fragmentKey, fragmentProps] = secondArg; - vNode = newVirtualNode(Fragment, fragmentKey, fragmentProps); - vNode.shouldUpdate = true; - break; - case DomText: - const content = secondArg[0]; - vNode = newVirtualNode(DomText, null, content); - vNode.shouldUpdate = true; - break; - case DomPortal: - const portal = secondArg[0]; - const children = portal.children ?? []; - vNode = newVirtualNode(DomPortal, portal.key, children); - vNode.shouldUpdate = true; - vNode.outerDom = portal.outerDom; - break; - case 'props': - const [type, key, props] = secondArg; - - const { vNodeTag, isLazy } = getVNodeTag(type); - - vNode = newVirtualNode(vNodeTag, key, props); - vNode.type = type; - vNode.shouldUpdate = true; - - // lazy类型的特殊处理 - vNode.isLazyComponent = isLazy; - if (isLazy) { - vNode.lazyType = type; - } - break; - case TreeRoot: - // 创建treeRoot - vNode = newVirtualNode(TreeRoot, null, null, secondArg[0]); - vNode.path.push(0); - - createUpdateArray(vNode); - break; - } - - return vNode; -} - -export function updateVNodePath(vNode: VNode) { - vNode.path = [...vNode.parent.path, vNode.cIndex]; -} - -export function createVNodeFromElement(element: HorizonElement): VNode { - const type = element.type; - const key = element.key; - const props = element.props; - - if (type === TYPE_STRICT_MODE || type === TYPE_FRAGMENT || type === TYPE_PROFILER) { - return createVNode(Fragment, key, props.children); - } else { - return createVNode('props', type, key, props); - } -} - -// 直接更新子节点属性即可,不需要diff -export function onlyUpdateChildVNodes(processing: VNode): Array | null { - // 检查子树是否需要更新 - if (processing.childShouldUpdate) { - // 此vNode无需更新,但是子树需要 - if (!processing.isCreated && processing.children && processing.children.length) { - // 更新子节点 - processing.children.forEach(child => { - updateVNode(child, child.props); - updateVNodePath(child); - }); - } - - // 返回子节点,继续遍历 - return processing.children; - } - - // 子树无需工作 - return null; -} - diff --git a/path.json b/path.json deleted file mode 100644 index 8540fb5e..00000000 --- a/path.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "horizon-test-path": "../horizon-test" -}