diff --git a/libs/horizon/src/renderer/components/BaseClassComponent.ts b/libs/horizon/src/renderer/components/BaseClassComponent.ts new file mode 100644 index 00000000..12dab04e --- /dev/null +++ b/libs/horizon/src/renderer/components/BaseClassComponent.ts @@ -0,0 +1,25 @@ +/** + * Component的api setState和forceUpdate在实例生成阶段实现 + */ +class Component { + props; + context; + + constructor(props, context) { + this.props = props; + this.context = context; + } +} + +// 兼容三方件 react-lifecycles-compat,它会读取 isReactComponent 属性值,不添加会导致 eview-ui 官网白屏 +Component.prototype.isReactComponent = true; +/** + * 支持PureComponent + */ +class PureComponent extends Component { + constructor(props, context) { + super(props, context); + } +} + +export { Component, PureComponent }; diff --git a/libs/horizon/src/renderer/components/CreatePortal.ts b/libs/horizon/src/renderer/components/CreatePortal.ts new file mode 100644 index 00000000..3bdb4b17 --- /dev/null +++ b/libs/horizon/src/renderer/components/CreatePortal.ts @@ -0,0 +1,15 @@ +import {TYPE_PORTAL} from '../utils/elementType'; +import type {PortalType} from '../Types'; + +export function createPortal( + children: any, + outerDom: any, + key: string = null, +): PortalType { + return { + vtype: TYPE_PORTAL, + key: key == null ? null : '' + key, + children, + outerDom, + }; +} diff --git a/libs/horizon/src/renderer/components/CreateRef.ts b/libs/horizon/src/renderer/components/CreateRef.ts new file mode 100644 index 00000000..38496640 --- /dev/null +++ b/libs/horizon/src/renderer/components/CreateRef.ts @@ -0,0 +1,7 @@ +import type {RefType} from '../Types'; + +export function createRef(): RefType { + return { + current: null, + }; +} diff --git a/libs/horizon/src/renderer/components/ForwardRef.ts b/libs/horizon/src/renderer/components/ForwardRef.ts new file mode 100644 index 00000000..5baef940 --- /dev/null +++ b/libs/horizon/src/renderer/components/ForwardRef.ts @@ -0,0 +1,8 @@ +import {TYPE_FORWARD_REF} from '../utils/elementType'; + +export function forwardRef(render: Function) { + return { + vtype: TYPE_FORWARD_REF, + render, + }; +} diff --git a/libs/horizon/src/renderer/components/Lazy.ts b/libs/horizon/src/renderer/components/Lazy.ts new file mode 100644 index 00000000..55a276c5 --- /dev/null +++ b/libs/horizon/src/renderer/components/Lazy.ts @@ -0,0 +1,66 @@ +import type {PromiseType} from '../Types'; + +import {TYPE_LAZY} from '../utils/elementType'; + +enum LayStatus { + UnProcessed = 'UnProcessed', + Pending = 'Pending', + Fulfilled = 'Fulfilled', + Rejected = 'Rejected', +} + +type LazyContent = { + _status: string, + _value: () => PromiseType<{default: T}> | PromiseType | T | any +}; + +export type LazyComponent = { + vtype: Symbol | number, + _content: P, + _load: (content: P) => T, +}; + +// lazyContent随着阶段改变,_value改变: +// 1. 未初始化 -> promiseCtor: () => promise +// 2. pending -> promise +// 3. fulfilled -> module +// 4. rejected -> error +function lazyLoader(lazyContent: LazyContent): any { + if (lazyContent._status === LayStatus.UnProcessed) { + // 执行动态导入组件import + const promise = lazyContent._value(); + lazyContent._status = LayStatus.Pending; + lazyContent._value = promise; + promise.then( + module => { + if (lazyContent._status === LayStatus.Pending) { + const defaultExport = module.default; + lazyContent._status = LayStatus.Fulfilled; + lazyContent._value = defaultExport; + } + }, + error => { + if (lazyContent._status === LayStatus.Pending) { + lazyContent._status = LayStatus.Rejected; + lazyContent._value = error; + } + }, + ); + } + if (lazyContent._status === LayStatus.Fulfilled) { + return lazyContent._value; + } else { + throw lazyContent._value; + } +} + +export function lazy(promiseCtor: () => PromiseType<{default: T}>): LazyComponent> { + return { + vtype: TYPE_LAZY, + _content: { + _status: LayStatus.UnProcessed, + _value: promiseCtor, + }, + _load: lazyLoader, + }; +} diff --git a/libs/horizon/src/renderer/components/Memo.ts b/libs/horizon/src/renderer/components/Memo.ts new file mode 100644 index 00000000..90d3c140 --- /dev/null +++ b/libs/horizon/src/renderer/components/Memo.ts @@ -0,0 +1,9 @@ +import {TYPE_MEMO} from '../utils/elementType'; + +export function memo(type, compare?: (oldProps: Props, newProps: Props) => boolean) { + return { + vtype: TYPE_MEMO, + type: type, + compare: compare === undefined ? null : compare, + }; +} diff --git a/libs/horizon/src/renderer/components/context/CompatibleContext.ts b/libs/horizon/src/renderer/components/context/CompatibleContext.ts new file mode 100644 index 00000000..0df1f5a2 --- /dev/null +++ b/libs/horizon/src/renderer/components/context/CompatibleContext.ts @@ -0,0 +1,101 @@ +import type {VNode} from '../../Types'; + +import { + setOldContextCtx, + setContextChangeCtx, + getOldContextCtx, + resetOldContextCtx, + resetContextChangeCtx, + setOldPreviousContextCtx, + getOldPreviousContextCtx, + setVNodeOldContext, + getVNodeOldContext, + setVNodeOldPreviousContext, + getVNodeOldPreviousContext, +} from '../../ContextSaver'; + +const emptyObject = {}; + +// 判断是否是过时的context的提供者 +export function isOldProvider(comp: Function): boolean { + // @ts-ignore + const childContextTypes = comp.childContextTypes; + return childContextTypes !== null && childContextTypes !== undefined; +} + +// 判断是否是过时的context的消费者 +export function isOldConsumer(comp: Function): boolean { + // @ts-ignore + const contextTypes = comp.contextTypes; + return contextTypes !== null && contextTypes !== undefined; +} + +// 如果是旧版context提供者,则缓存两个全局变量,上一个提供者提供的context和当前提供者提供的context +export function cacheOldCtx(processing: VNode, hasOldContext: any): void { + // 每一个context提供者都会更新ctxOldContext + if (hasOldContext) { + setOldPreviousContextCtx(getOldContextCtx()); + + const vNodeContext = getVNodeOldContext(processing) || emptyObject; + setOldContextCtx(processing, vNodeContext); + } +} + +// 获取当前组件可以消费的context +export function getOldContext(processing: VNode, clazz: Function, ifProvider: boolean) { + const type = processing.type; + // 不是context消费者, 则直接返回空对象 + if (!isOldConsumer(type)) { + return emptyObject; + } + + // 当组件既是提供者,也是消费者时,取上一个context,不能直接取最新context,因为已经被更新为当前组件的context; + // 当组件只是消费者时,则取最新context + const parentContext = ((ifProvider && isOldProvider(clazz))) ? + getOldPreviousContextCtx() : + getOldContextCtx(); + + // 除非父级context更改,否则不需要重新创建子context,直接取对应节点上存的。 + if (getVNodeOldPreviousContext(processing) === parentContext) { + return getVNodeOldContext(processing); + } + + // 从父的context中取出子定义的context + const context = {}; + for (const key in type.contextTypes) { + context[key] = parentContext[key]; + } + + // 缓存当前组件的context,最近祖先传递下来context,当前可消费的context + setVNodeOldPreviousContext(processing, parentContext); + setVNodeOldContext(processing, context); + + return context; +} + +// 重置context +export function resetOldCtx(vNode: VNode): void { + resetOldContextCtx(vNode); + resetContextChangeCtx(vNode); +} + +// 当前组件是提供者,则需要合并祖先context和当前组件提供的context +function handleContext(vNode: VNode, parentContext: Object): Object { + const instance = vNode.realNode; + + if (typeof instance.getChildContext !== 'function') { + return parentContext; + } + + // 合并祖先提供的context和当前组件提供的context + return {...parentContext, ...instance.getChildContext()}; +} + +// 当前组件是context提供者,更新时,需要合并祖先context和当前组件提供的context +export function updateOldContext(vNode: VNode): void { + const ctx = handleContext(vNode, getOldPreviousContextCtx()); + // 更新context,给子组件用的context + setOldContextCtx(vNode, ctx); + // 标记更改 + setContextChangeCtx(vNode, true); +} diff --git a/libs/horizon/src/renderer/components/context/Context.ts b/libs/horizon/src/renderer/components/context/Context.ts new file mode 100644 index 00000000..bcbc4f83 --- /dev/null +++ b/libs/horizon/src/renderer/components/context/Context.ts @@ -0,0 +1,31 @@ +import type {VNode, ContextType} from '../../Types'; +import {getHookStage} from '../../hooks/HookStage'; +import {throwNotInFuncError} from '../../hooks/BaseHook'; + +// 重置依赖 +export function resetDepContexts(vNode: VNode): void { + vNode.depContexts = []; +} + +// 收集依赖 +function collectDeps(vNode: VNode, context: ContextType) { + const depContexts = vNode.depContexts; + if (!depContexts.length) { + vNode.isDepContextChange = false; + } + if (!depContexts.includes(context)) { + depContexts.push(context); + } +} + +export function getNewContext(vNode: VNode, ctx: ContextType, isUseContext: boolean = false): T { + // 如果来自于useContext,则需要在函数组件中调用 + if (isUseContext && getHookStage() === null) { + throwNotInFuncError(); + } + + // 调用到这个方法,说明当前vNode依赖了这个context,所以需要收集起来 + collectDeps(vNode, ctx); + + return ctx.value; +} diff --git a/libs/horizon/src/renderer/components/context/CreateContext.ts b/libs/horizon/src/renderer/components/context/CreateContext.ts new file mode 100644 index 00000000..45d97039 --- /dev/null +++ b/libs/horizon/src/renderer/components/context/CreateContext.ts @@ -0,0 +1,20 @@ +import type {ContextType} from '../../Types'; +import {TYPE_PROVIDER, TYPE_CONTEXT} from '../../utils/elementType'; + +export function createContext(val: T): ContextType { + const context: ContextType = { + vtype: TYPE_CONTEXT, + value: val, + Provider: null, + Consumer: null, + }; + + context.Provider = { + vtype: TYPE_PROVIDER, + _context: context, + }; + + context.Consumer = context; + + return context; +} diff --git a/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts b/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts new file mode 100644 index 00000000..185ec51d --- /dev/null +++ b/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts @@ -0,0 +1,66 @@ +/** + * 浏览器相关实现 + */ + +export let now; + +if (typeof performance === 'object' && typeof performance.now === 'function') { + const localPerformance = performance; + now = () => localPerformance.now(); +} else { + const localDate = Date; + const initialTime = localDate.now(); + now = () => localDate.now() - initialTime; +} + +let isMessageLoopRunning = false; +let browserCallback = null; + +// 默认每次只运行5ms +const runTime = 5; +let deadline = 0; + +export function isOverTime() { + return now() >= deadline; +} + +// 1、设置deadline;2、回调TaskExecutor传过来的browserCallback +const callRenderTasks = () => { + if (browserCallback == null) { + return; + } + + const currentTime = now(); + // 计算deadline + deadline = currentTime + runTime; + try { + // 执行callback + const hasMoreTask = browserCallback( + currentTime, + ); + + if (!hasMoreTask) { // 没有更多task + isMessageLoopRunning = false; + browserCallback = null; + } else { + // 还有task,继续调用 + port.postMessage(null); + } + } catch (error) { + port.postMessage(null); + throw error; + } +}; + +const channel = new MessageChannel(); +const port = channel.port2; +channel.port1.onmessage = callRenderTasks; + +export function requestBrowserCallback(callback) { + browserCallback = callback; + + if (!isMessageLoopRunning) { + isMessageLoopRunning = true; + port.postMessage(null); + } +} diff --git a/libs/horizon/src/renderer/taskExecutor/RenderQueue.ts b/libs/horizon/src/renderer/taskExecutor/RenderQueue.ts new file mode 100644 index 00000000..125f0a17 --- /dev/null +++ b/libs/horizon/src/renderer/taskExecutor/RenderQueue.ts @@ -0,0 +1,68 @@ +/** + * 利用TaskExecutor的异步任务,封装一个renderQueue来执行同步的渲染callback + */ + +import {runAsync, cancelTask, ImmediatePriority} from './TaskExecutor'; + +type RenderCallback = () => RenderCallback | null; + +let renderQueue: Array | null = null; +// 保存正在等待的异步Task,可以用于取消 +let callingQueueTask: any | null = null; +// 防止重入 +let isCallingRenderQueue = false; + +export function pushRenderCallback(callback: RenderCallback) { + if (renderQueue === null) { + renderQueue = [callback]; + // 高优先级的异步调度 + callingQueueTask = runAsync(callRenderQueue, ImmediatePriority); + } else { + // 不需要调度,在syncQueue创建的时候已经调度了 + renderQueue.push(callback); + } + + // 返回一个空对象,用于区别null + return {}; +} + +export function callRenderQueueImmediate() { + if (callingQueueTask !== null) { + // 取消异步调度 + cancelTask(callingQueueTask); + callingQueueTask = null; + } + + callRenderQueue(); +} + +// 执行render回调 +function callRenderQueue() { + if (!isCallingRenderQueue && renderQueue !== null) { + // 防止重入 + isCallingRenderQueue = true; + + let i = 0; + try { + for (; i < renderQueue.length; i++) { + let callback = renderQueue[i]; + do { + callback = callback(); + } while (callback !== null); + } + renderQueue = null; + } catch (error) { + // 如果有异常抛出,请将剩余的回调留在队列中 + if (renderQueue !== null) { + renderQueue = renderQueue.slice(i + 1); + } + + // 在下一个异步中再调用 + runAsync(callRenderQueueImmediate, ImmediatePriority); + + throw error; + } finally { + isCallingRenderQueue = false; + } + } +} diff --git a/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts b/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts new file mode 100644 index 00000000..49e0c98d --- /dev/null +++ b/libs/horizon/src/renderer/taskExecutor/TaskExecutor.ts @@ -0,0 +1,135 @@ +/** + * 调度器的核心实现 + */ + +import { + requestBrowserCallback, + isOverTime, + now, +} from './BrowserAsync'; + +import {add, shift, first} from './TaskQueue'; + +const ImmediatePriority = 1; +const NormalPriority = 10; + +// 用于控制插入任务的顺序 +let idCounter = 1; + +let currentPriorityLevel = NormalPriority; + +// 正在执行task +let isProcessing = false; + +// 调度中,等待浏览器回调 +let isScheduling = false; + +function runSync(callback, priorityLevel = NormalPriority) { + const previousPriorityLevel = currentPriorityLevel; + currentPriorityLevel = priorityLevel; + + try { + return callback(); + } finally { + currentPriorityLevel = previousPriorityLevel; + } +} + +function runAsync(callback, priorityLevel= NormalPriority ) { + let timeout; + switch (priorityLevel) { + case ImmediatePriority: + timeout = -1; + break; + case NormalPriority: + default: + timeout = 5000; + break; + } + + const task = { + id: idCounter++, + callback, + priorityLevel, + expirationTime: now() + timeout, + }; + + add(task); + + if (!isScheduling && !isProcessing) { + isScheduling = true; + requestBrowserCallback(callTasks); + } + + return task; +} + +function callTasks(initialTime) { + isScheduling = false; + isProcessing = true; + + let task = null; + const previousPriorityLevel = currentPriorityLevel; + try { + let currentTime = initialTime; + task = first(); + + // 循环执行task + while (task !== null) { + if ( + task.expirationTime > currentTime && + isOverTime() + ) { + // 没到deadline + break; + } + + const callback = task.callback; + if (typeof callback === 'function') { + task.callback = null; + currentPriorityLevel = task.priorityLevel; + const didUserCallbackTimeout = task.expirationTime <= currentTime; + + const continuationCallback = callback(didUserCallbackTimeout); + currentTime = now(); + // 执行callback返回函数,重置callback + if (typeof continuationCallback === 'function') { + task.callback = continuationCallback; + } else { + if (task === first()) { + shift(); + } + } + } else { + shift(); + } + + task = first(); + } + + // 返回是否还有任务,如果有,说明是被中断了 + return task !== null; + } finally { + task = null; + currentPriorityLevel = previousPriorityLevel; + isProcessing = false; + } +} + +function cancelTask(task) { + task.callback = null; +} + +function getCurrentPriorityLevel() { + return currentPriorityLevel; +} + +export { + ImmediatePriority, + NormalPriority, + runSync, + runAsync, + cancelTask, + getCurrentPriorityLevel, + now, +}; diff --git a/libs/horizon/src/renderer/taskExecutor/TaskQueue.ts b/libs/horizon/src/renderer/taskExecutor/TaskQueue.ts new file mode 100644 index 00000000..708e9bdf --- /dev/null +++ b/libs/horizon/src/renderer/taskExecutor/TaskQueue.ts @@ -0,0 +1,58 @@ +/** + * 任务列表的实现 + */ + +type Queue = Array; +type Node = { + id: number; + expirationTime: number; +}; + +// 任务队列 +const taskQueue: Queue = []; + +export function add(node: Node): void { + // 查找第一个大于等于 value 的下标,都比 value 小则返回 -1 + const idx = getBiggerIdx(node); + + if (idx === 0) { + taskQueue.unshift(node); + } else if (idx === -1) { + taskQueue.push(node); + } else { + taskQueue.splice(idx, 0, node); + } +} + +// 二分法查找第一个大于等于 value 的下标,都比 value 小则返回 -1,时间复杂度O(logn) +function getBiggerIdx(node: Node) { + let left = 0; + let right = taskQueue.length - 1; + + while (left <= right) { + const middle = left + ((right - left) >> 1); + + if (compare(taskQueue[middle], node) > 0) + right = middle - 1; + else + left = middle + 1; + } + + return (left < taskQueue.length) ? left : -1; +} + +export function first(): Node | null { + const val = taskQueue[0]; + return val !== undefined ? val : null; +} + +export function shift(): Node | null { + const val = taskQueue.shift(); + return val !== undefined ? val : null; +} + +function compare(a: Node, b: Node) { + // 优先先用index排序,其次用id + const diff = a.expirationTime - b.expirationTime; + return diff !== 0 ? diff : a.id - b.id; +}