diff --git a/jest.config.js b/jest.config.js index 410e7c1c..69f84f2b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,8 +26,10 @@ module.exports = { testEnvironment: 'jest-environment-jsdom-sixteen', testMatch: [ + // '/scripts/__tests__/HorizonXTest/edgeCases/deepVariableObserver.test.tsx', + // '/scripts/__tests__/HorizonXTest/StateManager/StateMap.test.tsx', '/scripts/__tests__/**/*.test.js', - '/scripts/__tests__/**/*.test.tsx' + '/scripts/__tests__/**/*.test.tsx', ], timers: 'fake', diff --git a/libs/horizon/global.d.ts b/libs/horizon/global.d.ts index 7919828b..76352c78 100644 --- a/libs/horizon/global.d.ts +++ b/libs/horizon/global.d.ts @@ -19,3 +19,5 @@ declare var isDev: boolean; declare var isTest: boolean; declare const __VERSION__: string; +declare var setImmediate: Function; +declare var __HORIZON_DEV_HOOK__: any; diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts index 52829cc9..20e47409 100644 --- a/libs/horizon/index.ts +++ b/libs/horizon/index.ts @@ -18,6 +18,8 @@ import { TYPE_PROFILER as Profiler, TYPE_STRICT_MODE as StrictMode, TYPE_SUSPENSE as Suspense, + TYPE_FORWARD_REF as ForwardRef, + TYPE_MEMO as Memo, } from './src/external/JSXElementType'; import { Component, PureComponent } from './src/renderer/components/BaseClassComponent'; @@ -42,9 +44,6 @@ import { useState, useDebugValue, } from './src/renderer/hooks/HookExternal'; -import { asyncUpdates } from './src/renderer/TreeBuilder'; -import { callRenderQueueImmediate } from './src/renderer/taskExecutor/RenderQueue'; -import { runAsyncEffects } from './src/renderer/submit/HookEffectHandler'; import { isContextProvider, isContextConsumer, @@ -59,13 +58,7 @@ import { import { createStore, useStore, clearStore } from './src/horizonx/store/StoreHandler'; import * as reduxAdapter from './src/horizonx/adapters/redux'; import { watch } from './src/horizonx/proxy/watch'; - -// act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。 -const act = fun => { - asyncUpdates(fun); - callRenderQueueImmediate(); - runAsyncEffects(); -}; +import { act } from './src/external/TestUtil'; import { render, @@ -75,6 +68,9 @@ import { unmountComponentAtNode, } from './src/dom/DOMExternal'; +import { syncUpdates as flushSync } from './src/renderer/TreeBuilder'; +import { toRaw } from './src/horizonx/proxy/ProxyHandler'; + const Horizon = { Children, createRef, @@ -94,10 +90,6 @@ const Horizon = { useReducer, useRef, useState, - Fragment, - Profiler, - StrictMode, - Suspense, createElement, cloneElement, isValidElement, @@ -107,6 +99,7 @@ const Horizon = { findDOMNode, unmountComponentAtNode, act, + flushSync, createStore, useStore, clearStore, @@ -121,6 +114,12 @@ const Horizon = { isPortal, isContextProvider, isContextConsumer, + ForwardRef, + Memo, + Fragment, + Profiler, + StrictMode, + Suspense, }; export const version = __VERSION__; @@ -143,10 +142,6 @@ export { useReducer, useRef, useState, - Fragment, - Profiler, - StrictMode, - Suspense, createElement, cloneElement, isValidElement, @@ -156,12 +151,14 @@ export { findDOMNode, unmountComponentAtNode, act, + flushSync, // 状态管理器HorizonX接口 createStore, useStore, clearStore, reduxAdapter, watch, + toRaw, // 兼容ReactIs isFragment, isElement, @@ -172,6 +169,12 @@ export { isPortal, isContextProvider, isContextConsumer, + ForwardRef, + Memo, + Fragment, + Profiler, + StrictMode, + Suspense, }; export default Horizon; diff --git a/libs/horizon/jsx-dev-runtime.ts b/libs/horizon/jsx-dev-runtime.ts new file mode 100644 index 00000000..f768f977 --- /dev/null +++ b/libs/horizon/jsx-dev-runtime.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { + TYPE_FRAGMENT as Fragment, +} from './src/external/JSXElementType'; +import { jsx as jsxDEV } from './src/external/JSXElement'; + +export { + jsxDEV, + Fragment +}; diff --git a/libs/horizon/jsx-runtime.ts b/libs/horizon/jsx-runtime.ts new file mode 100644 index 00000000..f130dc06 --- /dev/null +++ b/libs/horizon/jsx-runtime.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { + TYPE_FRAGMENT as Fragment, +} from './src/external/JSXElementType'; +import { jsx, jsx as jsxs } from './src/external/JSXElement'; + +export { + jsx, + jsxs, + Fragment +}; diff --git a/libs/horizon/package.json b/libs/horizon/package.json index 8b7332a0..f64cd277 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.33", + "version": "0.0.52", "homepage": "", "bugs": "", "main": "index.js", diff --git a/libs/horizon/src/dom/DOMExternal.ts b/libs/horizon/src/dom/DOMExternal.ts index bbbece59..b8ca5cb4 100644 --- a/libs/horizon/src/dom/DOMExternal.ts +++ b/libs/horizon/src/dom/DOMExternal.ts @@ -19,6 +19,7 @@ import type { Container } from './DOMOperator'; import { isElement } from './utils/Common'; import { findDOMByClassInst } from '../renderer/vnode/VNodeUtils'; import { Callback } from '../renderer/UpdateHandler'; +import { listenSimulatedDelegatedEvents } from '../event/EventBinding'; function createRoot(children: any, container: Container, callback?: Callback) { // 清空容器 @@ -31,6 +32,7 @@ function createRoot(children: any, container: Container, callback?: Callback) { // 调度器创建根节点,并给容器dom赋vNode结构体 const treeRoot = createTreeRootVNode(container); container._treeRoot = treeRoot; + listenSimulatedDelegatedEvents(treeRoot); // 执行回调 if (typeof callback === 'function') { @@ -71,7 +73,7 @@ function executeRender(children: any, container: Container, callback?: Callback) } function findDOMNode(domOrEle?: Element): null | Element | Text { - if (domOrEle == null) { + if (domOrEle === null || domOrEle === undefined) { return null; } @@ -101,7 +103,7 @@ function removeRootEventLister(container: Container) { // 卸载入口 function destroy(container: Container): boolean { - if (container && container._treeRoot) { + if (container._treeRoot) { syncUpdates(() => { executeRender(null, container, () => { removeRootEventLister(container); diff --git a/libs/horizon/src/dom/DOMOperator.ts b/libs/horizon/src/dom/DOMOperator.ts index cf0fd739..ca952804 100644 --- a/libs/horizon/src/dom/DOMOperator.ts +++ b/libs/horizon/src/dom/DOMOperator.ts @@ -16,7 +16,7 @@ import { saveVNode, updateVNodeProps } from './DOMInternalKeys'; import { createDom } from './utils/DomCreator'; import { getSelectionInfo, resetSelectionRange, SelectionData } from './SelectionRangeHandler'; -import { shouldAutoFocus } from './utils/Common'; +import { isDocument, shouldAutoFocus } from './utils/Common'; import { NSS } from './utils/DomCreator'; import { adjustStyleValue } from './DOMPropertiesHandler/StyleHandler'; import type { VNode } from '../renderer/Types'; @@ -26,6 +26,7 @@ import { isNativeElement, validateProps } from './validators/ValidateProps'; import { watchValueChange } from './valueHandler/ValueChangeHandler'; import { DomComponent, DomText } from '../renderer/vnode/VNodeTags'; import { updateCommonProp } from './DOMPropertiesHandler/UpdateCommonProp'; +import {getCurrentRoot} from '../renderer/RootStack'; export type Props = Record & { autoFocus?: boolean; @@ -45,7 +46,7 @@ function getChildNS(parentNS: string | null, tagName: string): string { return NSS.html; } - if (parentNS == null || parentNS === NSS.html) { + if (parentNS === null || parentNS === NSS.html) { // 没有父命名空间,或父命名空间为xhtml return NSS[tagName] ?? NSS.html; } @@ -70,7 +71,12 @@ export function resetAfterSubmit(): void { // 创建 DOM 对象 export function newDom(tagName: string, props: Props, parentNamespace: string, vNode: VNode): Element { - const dom: Element = createDom(tagName, parentNamespace); + // document取值于treeRoot对应的DOM的ownerDocument。 + // 解决:在iframe中使用top的horizon时,horizon在创建DOM时用到的document并不是iframe的document,而是top中的document的问题。 + const rootDom = getCurrentRoot().realNode; + const doc = isDocument(rootDom) ? rootDom : rootDom.ownerDocument; + + const dom: Element = createDom(tagName, parentNamespace, doc); // 将 vNode 节点挂到 DOM 对象上 saveVNode(vNode, dom); // 将属性挂到 DOM 对象上 @@ -124,7 +130,8 @@ export function isTextChild(type: string, props: Props): boolean { return ( props.dangerouslySetInnerHTML && typeof props.dangerouslySetInnerHTML === 'object' && - props.dangerouslySetInnerHTML.__html != null + props.dangerouslySetInnerHTML.__html !== null && + props.dangerouslySetInnerHTML.__html !== undefined ); } } @@ -135,14 +142,14 @@ export function newTextDom(text: string, processing: VNode): Text { return textNode; } -// 提交vNode的类型为Component或者Text的更新 +// 提交vNode的类型为DomComponent或者DomText的更新 export function submitDomUpdate(tag: string, vNode: VNode) { const newProps = vNode.props; const element: Element | null = vNode.realNode; if (tag === DomComponent) { // DomComponent类型 - if (element != null) { + if (element !== null && element !== undefined) { const type = vNode.type; const changeList = vNode.changeList; vNode.changeList = null; @@ -152,7 +159,14 @@ export function submitDomUpdate(tag: string, vNode: VNode) { updateVNodeProps(element, newProps); // 应用diff更新Properties. // 当一个选中的radio改变名称,浏览器使另一个radio的复选框为false. - if (type === 'input' && newProps.type === 'radio' && newProps.name != null && newProps.checked != null) { + if ( + type === 'input' + && newProps.type === 'radio' + && newProps.name !== null + && newProps.name !== undefined + && newProps.checked !== null + && newProps.checked !== undefined + ) { updateCommonProp(element, 'checked', newProps.checked, true); } const isNativeTag = isNativeElement(type, newProps); @@ -196,7 +210,7 @@ export function hideDom(tag: string, dom: Element | Text) { } // 不隐藏元素 -export function unHideDom(tag: string, dom: Element | Text, props: Props) { +export function unHideDom(tag: string, dom: Element | Text, props?: Props) { if (tag === DomComponent) { dom.style.display = adjustStyleValue('display', props?.style?.display ?? ''); } else if (tag === DomText) { diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts index 32427a36..f2657883 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts @@ -48,7 +48,7 @@ export function setDomProps(dom: Element, props: Object, isNativeTag: boolean, i } } else if (propName === 'dangerouslySetInnerHTML') { dom.innerHTML = propVal.__html; - } else if (!isInit || (isInit && propVal != null)) { + } else if (!isInit || (propVal !== null && propVal !== undefined)) { updateCommonProp(dom, propName, propVal, isNativeTag); } } @@ -70,7 +70,7 @@ export function compareProps(oldProps: Object, newProps: Object): Object { for (let i = 0; i < oldPropsLength; i++) { propName = keysOfOldProps[i]; // 新属性中包含该属性或者该属性为空值的属性不需要处理 - if (keysOfNewProps.includes(propName) || oldProps[propName] == null) { + if ( oldProps[propName] === null || oldProps[propName] === undefined || keysOfNewProps.includes(propName)) { continue; } @@ -103,9 +103,13 @@ export function compareProps(oldProps: Object, newProps: Object): Object { for (let i = 0; i < keysOfNewProps.length; i++) { propName = keysOfNewProps[i]; newPropValue = newProps[propName]; - oldPropValue = oldProps != null ? oldProps[propName] : null; + oldPropValue = oldProps !== null && oldProps !== undefined ? oldProps[propName] : null; - if (newPropValue === oldPropValue || (newPropValue == null && oldPropValue == null)) { + if ( + newPropValue === oldPropValue + || ((newPropValue === null || newPropValue === undefined) + && (oldPropValue === null || oldPropValue === undefined)) + ) { // 新旧属性值未发生变化,或者新旧属性皆为空值,不需要进行处理 continue; } @@ -140,7 +144,7 @@ export function compareProps(oldProps: Object, newProps: Object): Object { } else if (propName === 'dangerouslySetInnerHTML') { newHTML = newPropValue ? newPropValue.__html : undefined; oldHTML = oldPropValue ? oldPropValue.__html : undefined; - if (newHTML != null) { + if (newHTML !== null && newHTML !== undefined) { if (oldHTML !== newHTML) { toUpdateProps[propName] = newPropValue; } diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts index fa442f1d..def4be1b 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts @@ -13,6 +13,37 @@ * See the Mulan PSL v2 for more details. */ +/** + * 不需要加长度单位的 css 属性 + */ +const noUnitCSS = [ + 'animationIterationCount', + 'columnCount', + 'columns', + 'gridArea', + 'fontWeight', + 'lineClamp', + 'lineHeight', + 'opacity', + 'order', + 'orphans', + 'tabSize', + 'widows', + 'zIndex', + 'zoom', +]; + +const length = noUnitCSS.length; +for (let i = 0; i < length; i++) { + const cssKey = noUnitCSS[i]; + const attributeKey = cssKey.charAt(0).toUpperCase() + cssKey.slice(1); + + // css 兼容性前缀 webkit: chrome, mo: IE或者Edge, Moz: 火狐 + noUnitCSS.push('Webkit' + attributeKey); + noUnitCSS.push('mo' + attributeKey); + noUnitCSS.push('Moz' + attributeKey); +} + function isNeedUnitCSS(styleName: string) { return !( noUnitCSS.includes(styleName) || @@ -36,7 +67,7 @@ export function adjustStyleValue(name, value) { if (typeof value === 'number' && value !== 0 && isNeedUnitCSS(name)) { validValue = `${value}px`; - } else if (value === '' || value == null || typeof value === 'boolean') { + } else if (value === '' || value === null || value === undefined || typeof value === 'boolean') { validValue = ''; } @@ -54,27 +85,12 @@ export function setStyles(dom, styles) { const style = dom.style; Object.keys(styles).forEach(name => { const styleVal = styles[name]; - - style[name] = adjustStyleValue(name, styleVal); + // 以--开始的样式直接设置即可 + if (name.indexOf('--') === 0) { + style.setProperty(name, styleVal); + } else { + // 使用这种赋值方式,浏览器可以将'WebkitLineClamp', 'backgroundColor'分别识别为'-webkit-line-clamp'和'backgroud-color' + style[name] = adjustStyleValue(name, styleVal); + } }); } - -/** - * 不需要加长度单位的 css 属性 - */ -const noUnitCSS = [ - 'animationIterationCount', - 'columnCount', - 'columns', - 'gridArea', - 'fontWeight', - 'lineClamp', - 'lineHeight', - 'opacity', - 'order', - 'orphans', - 'tabSize', - 'widows', - 'zIndex', - 'zoom', -]; diff --git a/libs/horizon/src/dom/SelectionRangeHandler.ts b/libs/horizon/src/dom/SelectionRangeHandler.ts index 62e8c6d2..b0b8c88f 100644 --- a/libs/horizon/src/dom/SelectionRangeHandler.ts +++ b/libs/horizon/src/dom/SelectionRangeHandler.ts @@ -30,7 +30,7 @@ function setSelectionRange(dom: HTMLInputElement | HTMLTextAreaElement, range) { const { start, end } = range; let realEnd = end; - if (realEnd == null) { + if (realEnd === null || realEnd === undefined) { realEnd = start; } diff --git a/libs/horizon/src/dom/utils/Common.ts b/libs/horizon/src/dom/utils/Common.ts index 4105ade0..7b2af2b5 100644 --- a/libs/horizon/src/dom/utils/Common.ts +++ b/libs/horizon/src/dom/utils/Common.ts @@ -84,3 +84,7 @@ const types = ['button', 'input', 'select', 'textarea']; export function shouldAutoFocus(tagName: string, props: Props): boolean { return types.includes(tagName) ? Boolean(props.autoFocus) : false; } + +export function isNotNull(object: any): boolean { + return object !== null && object !== undefined; +} diff --git a/libs/horizon/src/dom/utils/DomCreator.ts b/libs/horizon/src/dom/utils/DomCreator.ts index 611a49e6..92c8ec90 100644 --- a/libs/horizon/src/dom/utils/DomCreator.ts +++ b/libs/horizon/src/dom/utils/DomCreator.ts @@ -20,15 +20,15 @@ export const NSS = { }; // 创建DOM元素 -export function createDom(tagName: string, parentNamespace: string): Element { +export function createDom(tagName: string, parentNamespace: string, doc: Document): Element { let dom: Element; const selfNamespace = NSS[tagName] || NSS.html; const ns = parentNamespace !== NSS.html ? parentNamespace : selfNamespace; if (ns !== NSS.html) { - dom = document.createElementNS(ns, tagName); + dom = doc.createElementNS(ns, tagName); } else { - dom = document.createElement(tagName); + dom = doc.createElement(tagName); } return dom; } diff --git a/libs/horizon/src/dom/validators/ValidateProps.ts b/libs/horizon/src/dom/validators/ValidateProps.ts index 8481d84b..1a328a33 100644 --- a/libs/horizon/src/dom/validators/ValidateProps.ts +++ b/libs/horizon/src/dom/validators/ValidateProps.ts @@ -73,7 +73,7 @@ export function isInvalidValue( propDetails: PropDetails | null, isNativeTag: boolean ): boolean { - if (value == null) { + if (value === null || value === undefined) { return true; } @@ -98,13 +98,13 @@ export function validateProps(type, props) { return; } - // 非内置的变迁 + // 非内置的元素 if (!isNativeElement(type, props)) { return; } // style属性必须是对象 - if (props.style != null && typeof props.style !== 'object') { + if (props.style !== null && props.style !== undefined && typeof props.style !== 'object') { throw new Error('style should be a object.'); } diff --git a/libs/horizon/src/dom/valueHandler/InputValueHandler.ts b/libs/horizon/src/dom/valueHandler/InputValueHandler.ts index cbee328c..0b29bcfd 100644 --- a/libs/horizon/src/dom/valueHandler/InputValueHandler.ts +++ b/libs/horizon/src/dom/valueHandler/InputValueHandler.ts @@ -29,7 +29,7 @@ function getInitValue(dom: HTMLInputElement, props: Props) { export function getInputPropsWithoutValue(dom: HTMLInputElement, props: Props) { // checked属于必填属性,无法置 let { checked } = props; - if (checked == null) { + if (checked === undefined) { checked = getInitValue(dom, props).initChecked; } @@ -45,12 +45,12 @@ export function getInputPropsWithoutValue(dom: HTMLInputElement, props: Props) { export function updateInputValue(dom: HTMLInputElement, props: Props) { const { value, checked } = props; - if (value != null) { + if (value !== undefined) { // 处理 dom.value 逻辑 if (dom.value !== String(value)) { dom.value = String(value); } - } else if (checked != null) { + } else if (checked !== undefined) { updateCommonProp(dom, 'checked', checked, true); } } @@ -60,7 +60,7 @@ export function setInitInputValue(dom: HTMLInputElement, props: Props) { const { value, defaultValue } = props; const { initValue, initChecked } = getInitValue(dom, props); - if (value != null || defaultValue != null) { + if (value !== undefined || defaultValue !== undefined) { // value 的使用优先级 value 属性 > defaultValue 属性 > 空字符串 const initValueStr = String(initValue); diff --git a/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts b/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts index d20dfee7..ecb767a6 100644 --- a/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts +++ b/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts @@ -69,18 +69,18 @@ export function updateSelectValue(dom: HorizonSelect, props: Props, isInit = fal dom._multiple = newMultiple; // 设置了 value 属性 - if (value != null) { + if (value !== null && value !== undefined) { updateValue(dom.options, value, newMultiple); } else if (oldMultiple !== newMultiple) { // 修改了 multiple 属性 // 切换 multiple 之后,如果设置了 defaultValue 需要重新应用 - if (defaultValue != null) { + if (defaultValue !== null && defaultValue !== undefined) { updateValue(dom.options, defaultValue, newMultiple); } else { // 恢复到未选定状态 updateValue(dom.options, newMultiple ? [] : '', newMultiple); } - } else if (isInit && defaultValue != null) { + } else if (isInit && defaultValue !== null && defaultValue !== undefined) { // 设置了 defaultValue 属性 updateValue(dom.options, defaultValue, newMultiple); } diff --git a/libs/horizon/src/dom/valueHandler/TextareaValueHandler.ts b/libs/horizon/src/dom/valueHandler/TextareaValueHandler.ts index a9052e70..e66cd28e 100644 --- a/libs/horizon/src/dom/valueHandler/TextareaValueHandler.ts +++ b/libs/horizon/src/dom/valueHandler/TextareaValueHandler.ts @@ -19,7 +19,7 @@ import { Props } from '../utils/Interface'; function getInitValue(props: Props) { const { value } = props; - if (value == null) { + if (value === undefined) { const { defaultValue, children } = props; let initValue = defaultValue; @@ -30,7 +30,7 @@ function getInitValue(props: Props) { } // defaultValue 属性未配置,置为空字符串 - initValue = initValue != null ? initValue : ''; + initValue = initValue ?? ''; return initValue; } else { return value; diff --git a/libs/horizon/src/event/EventBinding.ts b/libs/horizon/src/event/EventBinding.ts index 462137d2..8c81d763 100644 --- a/libs/horizon/src/event/EventBinding.ts +++ b/libs/horizon/src/event/EventBinding.ts @@ -16,7 +16,7 @@ /** * 事件绑定实现,分为绑定委托事件和非委托事件 */ -import { allDelegatedHorizonEvents, allDelegatedNativeEvents } from './EventHub'; +import { allDelegatedHorizonEvents, simulatedDelegatedEvents } from './EventHub'; import { isDocument } from '../dom/utils/Common'; import { getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys'; import { asyncUpdates, runDiscreteUpdates } from '../renderer/TreeBuilder'; @@ -24,8 +24,6 @@ import { handleEventMain } from './HorizonEventMain'; import { decorateNativeEvent } from './EventWrapper'; import { VNode } from '../renderer/vnode/VNode'; -const listeningMarker = '_horizonListening' + Math.random().toString(36).slice(4); - // 触发委托事件 function triggerDelegatedEvent( nativeEvtName: string, @@ -56,6 +54,14 @@ function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, i return listener; } +// 是否捕获事件 +function isCaptureEvent(horizonEventName) { + if (horizonEventName === 'onLostPointerCapture' || horizonEventName === 'onGotPointerCapture') { + return false; + } + return horizonEventName.slice(-7) === 'Capture'; +} + // 事件懒委托,当用户定义事件后,再进行委托到根节点 export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) { currentRoot.delegatedEvents.add(eventName); @@ -67,19 +73,22 @@ export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) { const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent; // 事件存储在DOM节点属性,避免多个VNode(root和portal)对应同一个DOM, 造成事件重复监听 - let events = currentRoot.realNode.$EV; - - if (!events) { - events = (currentRoot.realNode as any).$EV = {}; - } + currentRoot.realNode.$EV = currentRoot.realNode.$EV ?? {}; + const events = currentRoot.realNode.$EV; if (!events[nativeFullName]) { - const listener = listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture); - events[nativeFullName] = listener; + events[nativeFullName] = listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture); } }); } +// 利用冒泡事件模拟不冒泡事件,需要直接在根节点绑定 +export function listenSimulatedDelegatedEvents(root: VNode) { + for (let i = 0; i < simulatedDelegatedEvents.length; i++) { + lazyDelegateOnRoot(root, simulatedDelegatedEvents[i]); + } +} + // 通过horizon事件名获取到native事件名 function getNativeEvtName(horizonEventName, capture) { let nativeName; @@ -94,14 +103,6 @@ function getNativeEvtName(horizonEventName, capture) { return nativeName.toLowerCase(); } -// 是否捕获事件 -function isCaptureEvent(horizonEventName) { - if (horizonEventName === 'onLostPointerCapture' || horizonEventName === 'onGotPointerCapture') { - return false; - } - return horizonEventName.slice(-7) === 'Capture'; -} - // 封装监听函数 function getWrapperListener(horizonEventName, nativeEvtName, targetElement, listener) { return event => { diff --git a/libs/horizon/src/event/EventHub.ts b/libs/horizon/src/event/EventHub.ts index faf0a8de..992f1457 100644 --- a/libs/horizon/src/event/EventHub.ts +++ b/libs/horizon/src/event/EventHub.ts @@ -15,6 +15,9 @@ // 需要委托的horizon事件和原生事件对应关系 export const allDelegatedHorizonEvents = new Map(); + +// 模拟委托事件,不冒泡事件需要利用其他事件来触发冒泡过程 +export const simulatedDelegatedEvents = ['onMouseEnter', 'onMouseLeave']; // 所有委托的原生事件集合 export const allDelegatedNativeEvents = new Set(); @@ -49,6 +52,8 @@ export const horizonEventToNativeMap = new Map([ ['onCompositionUpdate', ['compositionupdate']], ['onChange', ['change', 'click', 'focusout', 'input']], ['onSelect', ['select']], + ['onMouseEnter', ['mouseout', 'mouseover']], + ['onMouseLeave', ['mouseout', 'mouseover']], ['onAnimationEnd', ['animationend']], ['onAnimationIteration', ['animationiteration']], diff --git a/libs/horizon/src/event/EventWrapper.ts b/libs/horizon/src/event/EventWrapper.ts index 72d848d7..2b6fa932 100644 --- a/libs/horizon/src/event/EventWrapper.ts +++ b/libs/horizon/src/event/EventWrapper.ts @@ -37,6 +37,9 @@ export class WrappedEvent { key: string; currentTarget: EventTarget | null = null; + target: HTMLElement; + relatedTarget: HTMLElement; + stopPropagation: () => void; preventDefault: () => void; diff --git a/libs/horizon/src/event/FormValueController.ts b/libs/horizon/src/event/FormValueController.ts index b1e8a74d..6a3d39a6 100644 --- a/libs/horizon/src/event/FormValueController.ts +++ b/libs/horizon/src/event/FormValueController.ts @@ -14,7 +14,7 @@ */ import { getVNodeProps } from '../dom/DOMInternalKeys'; -import { getDomTag } from '../dom/utils/Common'; +import { getDomTag, isNotNull } from '../dom/utils/Common'; import { Props } from '../dom/utils/Interface'; import { updateTextareaValue } from '../dom/valueHandler/TextareaValueHandler'; import { updateInputHandlerIfChanged } from '../dom/valueHandler/ValueChangeHandler'; @@ -37,15 +37,26 @@ export function shouldControlValue(): boolean { return changeEventTargets !== null && changeEventTargets.length > 0; } -// 从缓存队列中对受控组件进行赋值 -export function tryControlValue() { - if (!changeEventTargets) { - return; +function controlInputValue(inputDom: HTMLInputElement, props: Props) { + const { name, type } = props; + + // 如果是 radio,找出同一form内,name相同的Radio,更新它们Handler的Value + if (type === 'radio' && isNotNull(name)) { + const radioList = document.querySelectorAll(`input[type="radio"][name="${name}"]`); + for (let i = 0; i < radioList.length; i++) { + const radio = radioList[i]; + if (radio === inputDom) { + continue; + } + if (isNotNull(radio.form) && isNotNull(inputDom.form) && radio.form !== inputDom.form) { + continue; + } + + updateInputHandlerIfChanged(radio); + } + } else { + updateInputValue(inputDom, props); } - changeEventTargets.forEach(target => { - controlValue(target); - }); - changeEventTargets = null; } // 受控组件值重新赋值 @@ -66,24 +77,13 @@ function controlValue(target: Element) { } } -function controlInputValue(inputDom: HTMLInputElement, props: Props) { - const { name, type } = props; - - // 如果是 radio,找出同一form内,name相同的Radio,更新它们Handler的Value - if (type === 'radio' && name != null) { - const radioList = document.querySelectorAll(`input[type="radio"][name="${name}"]`); - for (let i = 0; i < radioList.length; i++) { - const radio = radioList[i]; - if (radio === inputDom) { - continue; - } - if (radio.form != null && inputDom.form != null && radio.form !== inputDom.form) { - continue; - } - - updateInputHandlerIfChanged(radio); - } - } else { - updateInputValue(inputDom, props); +// 从缓存队列中对受控组件进行赋值 +export function tryControlValue() { + if (!changeEventTargets) { + return; } + changeEventTargets.forEach(target => { + controlValue(target); + }); + changeEventTargets = null; } diff --git a/libs/horizon/src/event/HorizonEventMain.ts b/libs/horizon/src/event/HorizonEventMain.ts index 45dade43..9330af30 100644 --- a/libs/horizon/src/event/HorizonEventMain.ts +++ b/libs/horizon/src/event/HorizonEventMain.ts @@ -31,6 +31,7 @@ import { getDomTag } from '../dom/utils/Common'; import { updateInputHandlerIfChanged } from '../dom/valueHandler/ValueChangeHandler'; import { getDom } from '../dom/DOMInternalKeys'; import { recordChangeEventTargets, shouldControlValue, tryControlValue } from './FormValueController'; +import { getMouseEnterListeners } from './MouseEvent'; // web规范,鼠标右键key值 const RIGHT_MOUSE_BUTTON = 2; @@ -141,18 +142,26 @@ function triggerHorizonEvents( const target = nativeEvent.target || nativeEvent.srcElement!; // 触发普通委托事件 - let listenerList: ListenerUnitList = getCommonListeners(nativeEvtName, vNode, nativeEvent, target, isCapture); + const listenerList: ListenerUnitList = getCommonListeners(nativeEvtName, vNode, nativeEvent, target, isCapture); + let mouseEnterListeners: ListenerUnitList = []; + if (horizonEventToNativeMap.get('onMouseEnter')!.includes(nativeEvtName)) { + mouseEnterListeners = getMouseEnterListeners( + nativeEvtName, + vNode, + nativeEvent, + target, + ); + } + + let changeEvents: ListenerUnitList = []; // 触发特殊handler委托事件 if (!isCapture && horizonEventToNativeMap.get('onChange')!.includes(nativeEvtName)) { - const changeListeners = getChangeListeners(nativeEvtName, nativeEvent, vNode, target); - if (changeListeners.length) { - listenerList = listenerList.concat(changeListeners); - } + changeEvents = getChangeListeners(nativeEvtName, nativeEvent, vNode, target); } // 处理触发的事件队列 - processListeners(listenerList); + processListeners([...listenerList, ...mouseEnterListeners, ...changeEvents]); } // 其他事件正在执行中标记 diff --git a/libs/horizon/src/event/ListenerGetter.ts b/libs/horizon/src/event/ListenerGetter.ts index 57c2c82c..c6562d3c 100644 --- a/libs/horizon/src/event/ListenerGetter.ts +++ b/libs/horizon/src/event/ListenerGetter.ts @@ -86,3 +86,91 @@ export function getListenersFromTree( return listeners; } + + +// 获取父节点 +function getParent(inst: VNode | null): VNode | null { + if (inst === null) { + return null; + } + do { + inst = inst.parent; + } while (inst && inst.tag !== DomComponent); + return inst || null; +} + +// 寻找两个节点的共同最近祖先,如果没有则返回null +function getCommonAncestor(instA: VNode, instB: VNode): VNode | null { + const parentsSet = new Set(); + for (let tempA: VNode | null = instA; tempA; tempA = getParent(tempA)) { + parentsSet.add(tempA); + } + for (let tempB: VNode | null = instB; tempB; tempB = getParent(tempB)) { + if (parentsSet.has(tempB)) { + return tempB; + } + } + return null; +} + +function getMouseListenersFromTree( + event: WrappedEvent, + target: VNode, + commonParent: VNode | null, +): ListenerUnitList { + const registrationName = event.customEventName; + const listeners: ListenerUnitList = []; + + let vNode = target; + while (vNode !== null) { + // commonParent作为终点 + if (vNode === commonParent) { + break; + } + const {realNode, tag} = vNode; + if (tag === DomComponent && realNode !== null) { + const currentTarget = realNode; + const listener = getListenerFromVNode(vNode, registrationName); + if (listener) { + listeners.push({ + vNode, + listener, + currentTarget, + event, + }); + } + } + vNode = vNode.parent; + } + return listeners; +} + +// 获取enter和leave事件队列 +export function collectMouseListeners( + leaveEvent: null | WrappedEvent, + enterEvent: null | WrappedEvent, + from: VNode | null, + to: VNode | null, +): ListenerUnitList { + // 确定公共父节点,作为在树上遍历的终点 + const commonParent = from && to ? getCommonAncestor(from, to) : null; + let leaveEventList: ListenerUnitList = []; + if (from && leaveEvent) { + // 遍历树,获取绑定的leave事件 + leaveEventList = getMouseListenersFromTree( + leaveEvent, + from, + commonParent, + ); + } + let enterEventList: ListenerUnitList = []; + if (to && enterEvent) { + // 先触发父节点enter事件,所以需要逆序 + enterEventList = getMouseListenersFromTree( + enterEvent, + to, + commonParent, + ).reverse(); + } + return [...leaveEventList, ...enterEventList]; +} diff --git a/libs/horizon/src/event/MouseEvent.ts b/libs/horizon/src/event/MouseEvent.ts new file mode 100644 index 00000000..3eb38123 --- /dev/null +++ b/libs/horizon/src/event/MouseEvent.ts @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { getNearestVNode } from '../dom/DOMInternalKeys'; +import { WrappedEvent } from './EventWrapper'; +import { VNode } from '../renderer/vnode/VNode'; +import { AnyNativeEvent, ListenerUnitList } from './Types'; +import { DomComponent, DomText } from '../renderer/vnode/VNodeTags'; +import { collectMouseListeners } from './ListenerGetter'; +import { getNearestMountedVNode } from './utils'; + +/** + * 背景: mouseEnter和mouseLeave事件不冒泡,所以无法直接委托给根节点进行代理 + * 实现方案:利用mouseout、mouseover事件的,找到事件触发的起点和终点,判断出鼠标移动轨迹,在轨迹中的节点触发mouseEnter和mouseLeave事件 + * 步骤: + * 1. 根节点绑定mouseout和mouseover事件 + * 2. 事件触发后找到事件的起点和终点 + * 3. 封装装enter和leave事件 + * 4. 根据起止点找到公共父节点,作为事件冒泡的终点 + * 5. 遍历treeNode,找到每个节点绑定的mouseEnter和mouseLeave监听方法 + * 例如: mouseOut事件由D->C, A节点作为公共父节点,将触发 D、B的mouseLeave事件和C节点的mouseEnter事件 + * A + * / \ + * B C + * / \ + * D E + * + */ + +function getWrapperEvents(nativeEventTarget, fromInst, toInst, nativeEvent, targetInst): (WrappedEvent | null)[] { + const vWindow = nativeEventTarget.window === nativeEventTarget ? nativeEventTarget : nativeEventTarget.ownerDocument.defaultView; + + // 起点或者终点为空的话默认值为所在window + const fromNode = fromInst?.realNode || vWindow; + const toNode = toInst?.realNode || vWindow; + let leave: WrappedEvent | null = null; + let enter: WrappedEvent | null = null; + const nativeTargetInst = getNearestVNode(nativeEventTarget); + + // 在Mounted的dom节点上render一个子组件,系统中存在两个根节点,子节点的mouseout事件触发两次,取离target近的根节点生效 + if (nativeTargetInst === targetInst) { + leave = new WrappedEvent('onMouseLeave', 'mouseleave', nativeEvent); + leave.target = fromNode; + leave.relatedTarget = toNode; + + enter = new WrappedEvent('onMouseEnter', 'mouseenter', nativeEvent); + enter.target = toNode; + enter.relatedTarget = fromNode; + } + return [leave, enter]; +} + +function getEndpointVNode( + domEventName: string, + targetInst: null | VNode, + nativeEvent: AnyNativeEvent, +): (VNode | null)[] { + let fromVNode; + let toVNode; + if (domEventName === 'mouseover') { + fromVNode = null; + toVNode = targetInst; + } else { + const related = nativeEvent.relatedTarget || nativeEvent.toElement; + fromVNode = targetInst; + toVNode = related ? getNearestVNode(related) : null; + if (toVNode !== null) { + const nearestMounted = getNearestMountedVNode(toVNode); + if (toVNode !== nearestMounted || (toVNode.tag !== DomComponent && toVNode.tag !== DomText)) { + toVNode = null; + } + } + } + return [fromVNode, toVNode]; +} + +export function getMouseEnterListeners( + domEventName: string, + targetInst: null | VNode, + nativeEvent: AnyNativeEvent, + nativeEventTarget: null | EventTarget, +): ListenerUnitList { + + // 获取起点和终点的VNode + const [fromVNode, toVNode] = getEndpointVNode(domEventName, targetInst, nativeEvent); + if (fromVNode === toVNode) { + return []; + } + + // 获取包装后的leave和enter事件 + const [leave, enter] = getWrapperEvents(nativeEventTarget, fromVNode, toVNode, nativeEvent, targetInst); + + // 收集事件的监听方法 + return collectMouseListeners(leave, enter, fromVNode, toVNode); +} + + diff --git a/libs/horizon/src/event/utils.ts b/libs/horizon/src/event/utils.ts index c5bbb267..594a70b2 100644 --- a/libs/horizon/src/event/utils.ts +++ b/libs/horizon/src/event/utils.ts @@ -13,6 +13,10 @@ * See the Mulan PSL v2 for more details. */ +import { VNode } from '../renderer/vnode/VNode'; +import { Addition, FlagUtils } from '../renderer/vnode/VNodeFlags'; +import { TreeRoot } from '../renderer/vnode/VNodeTags'; + export function isInputElement(dom?: HTMLElement): boolean { return dom instanceof HTMLInputElement || dom instanceof HTMLTextAreaElement; } @@ -20,6 +24,26 @@ export function isInputElement(dom?: HTMLElement): boolean { export function setPropertyWritable(obj, propName) { const desc = Object.getOwnPropertyDescriptor(obj, propName); if (!desc || !desc.writable) { - Object.defineProperty(obj, propName, { writable: true }); + Object.defineProperty(obj, propName, {writable: true}); } } + +// 获取离 vNode 最近的已挂载 vNode,包含它自己 +export function getNearestMountedVNode(vNode: VNode): null | VNode { + let node = vNode; + let target = vNode; + // 如果没有alternate,说明是可能是未插入的新树,需要处理插入的副作用。 + while (node.parent) { + // 存在更新,节点未挂载,查找父节点,但是父节点也可能未挂载,需要继续往上查找无更新节点 + if (FlagUtils.hasFlag(node, Addition)) { + target = node.parent; + } + node = node.parent; + } + // 如果根节点是 Dom 类型节点,表示已经挂载 + if (node.tag === TreeRoot) { + return target; + } + // 如果没有找到根节点,意味着Tree已经卸载或者未挂载 + return null; +} diff --git a/libs/horizon/src/external/ChildrenUtil.ts b/libs/horizon/src/external/ChildrenUtil.ts index 58f435ae..96e41bdc 100644 --- a/libs/horizon/src/external/ChildrenUtil.ts +++ b/libs/horizon/src/external/ChildrenUtil.ts @@ -17,16 +17,51 @@ import { throwIfTrue } from '../renderer/utils/throwIfTrue'; import { TYPE_COMMON_ELEMENT, TYPE_PORTAL } from './JSXElementType'; import { isValidElement, JSXElement } from './JSXElement'; +import { BELONG_CLASS_VNODE_KEY } from '../renderer/vnode/VNode'; // 生成key function getItemKey(item: any, index: number): string { - if (typeof item === 'object' && item !== null && item.key != null) { + if (typeof item === 'object' && item !== null && item.key !== null && item.key !== undefined) { return '.$' + item.key; } // 使用36进制减少生成字符串的长度以节省空间 return '.' + index.toString(36); } +function processArrayChildren(children: any, arr: Array, prefix: string, callback: Function) { + for (let i = 0; i < children.length; i++) { + const childItem = children[i]; + const nextPrefix = prefix + getItemKey(childItem, i); + mapChildrenToArray(childItem, arr, nextPrefix, callback); + } +} + +function callMapFun(children: any, arr: Array, prefix: string, callback: Function) { + let mappedChild = callback(children); + if (Array.isArray(mappedChild)) { + // 维持原有规格,如果callback返回结果是数组,处理函数修改为返回数组item + processArrayChildren(mappedChild, arr, prefix + '/', subChild => subChild); + } else if (mappedChild !== null && mappedChild !== undefined) { + // 给一个key值,确保返回的对象一定带有key + if (isValidElement(mappedChild)) { + const childKey = prefix === '' ? getItemKey(children, 0) : ''; + const mappedKey = getItemKey(mappedChild, 0); + const newKey = + prefix + childKey + (mappedChild.key && mappedKey !== getItemKey(children, 0) ? '.$' + mappedChild.key : ''); + // 返回一个修改key的children + mappedChild = JSXElement( + mappedChild.type, + newKey, + mappedChild.ref, + mappedChild[BELONG_CLASS_VNODE_KEY], + mappedChild.props, + mappedChild.src + ); + } + arr.push(mappedChild); + } +} + function mapChildrenToArray(children: any, arr: Array, prefix: string, callback?: Function): number | void { const type = typeof children; switch (type) { @@ -58,40 +93,6 @@ function mapChildrenToArray(children: any, arr: Array, prefix: string, call } } -function processArrayChildren(children: any, arr: Array, prefix: string, callback: Function) { - for (let i = 0; i < children.length; i++) { - const childItem = children[i]; - const nextPrefix = prefix + getItemKey(childItem, i); - mapChildrenToArray(childItem, arr, nextPrefix, callback); - } -} - -function callMapFun(children: any, arr: Array, prefix: string, callback: Function) { - let mappedChild = callback(children); - if (Array.isArray(mappedChild)) { - // 维持原有规格,如果callback返回结果是数组,处理函数修改为返回数组item - processArrayChildren(mappedChild, arr, prefix + '/', subChild => subChild); - } else if (mappedChild !== null && mappedChild !== undefined) { - // 给一个key值,确保返回的对象一定带有key - if (isValidElement(mappedChild)) { - const childKey = prefix === '' ? getItemKey(children, 0) : ''; - const mappedKey = getItemKey(mappedChild, 0); - const newKey = - prefix + childKey + (mappedChild.key && mappedKey !== getItemKey(children, 0) ? '.$' + mappedChild.key : ''); - // 返回一个修改key的children - mappedChild = JSXElement( - mappedChild.type, - newKey, - mappedChild.ref, - mappedChild.belongClassVNode, - mappedChild.props, - mappedChild.src - ); - } - arr.push(mappedChild); - } -} - // 在 children 里的每个直接子节点上调用一个函数,并将 this 设置为 thisArg function mapChildren(children: any, func: Function, context?: any): Array { if (children === null || children === undefined) { diff --git a/libs/horizon/src/external/JSXElement.ts b/libs/horizon/src/external/JSXElement.ts index 69de1d39..f3920254 100644 --- a/libs/horizon/src/external/JSXElement.ts +++ b/libs/horizon/src/external/JSXElement.ts @@ -16,6 +16,7 @@ import { TYPE_COMMON_ELEMENT } from './JSXElementType'; import { getProcessingClassVNode } from '../renderer/GlobalVar'; import { Source } from '../renderer/Types'; +import { BELONG_CLASS_VNODE_KEY } from '../renderer/vnode/VNode'; /** * vtype 节点的类型,这里固定是element @@ -25,10 +26,10 @@ import { Source } from '../renderer/Types'; * props 其他常规属性 */ export function JSXElement(type, key, ref, vNode, props, source: Source | null) { - return { + const ele = { // 元素标识符 vtype: TYPE_COMMON_ELEMENT, - src: isDev ? source : null, + src: null, // 属于元素的内置属性 type: type, @@ -36,14 +37,28 @@ export function JSXElement(type, key, ref, vNode, props, source: Source | null) ref: ref, props: props, - // 所属的class组件 - belongClassVNode: vNode, + // 所属的class组件,clonedeep jsxElement时需要防止无限循环 + [BELONG_CLASS_VNODE_KEY]: vNode, }; -} + // 兼容IE11不支持Symbol + if (typeof BELONG_CLASS_VNODE_KEY === 'string') { + Object.defineProperty(ele, BELONG_CLASS_VNODE_KEY, { + configurable: false, + enumerable: false, + value: vNode, + }); + } + if (isDev) { + // 为了test判断两个 JSXElement 对象是否相等时忽略src属性,需要设置src的enumerable为false + Object.defineProperty(ele, 'src', { + configurable: false, + enumerable: false, + writable: false, + value: source, + }); + } -function isValidKey(key) { - const keyArray = ['key', 'ref', '__source', '__self']; - return !keyArray.includes(key); + return ele; } function mergeDefault(sourceObj, defaultObj) { @@ -54,19 +69,20 @@ function mergeDefault(sourceObj, defaultObj) { }); } +// ['key', 'ref', '__source', '__self']属性不从setting获取 +const keyArray = ['key', 'ref', '__source', '__self']; + function buildElement(isClone, type, setting, children) { // setting中的值优先级最高,clone情况下从 type 中取值,创建情况下直接赋值为 null - const key = setting && setting.key !== undefined ? String(setting.key) : isClone ? type.key : null; - const ref = setting && setting.ref !== undefined ? setting.ref : isClone ? type.ref : null; + const key = (setting && setting.key !== undefined) ? String(setting.key) : (isClone ? type.key : null); + const ref = (setting && setting.ref !== undefined) ? setting.ref : (isClone ? type.ref : null); const props = isClone ? { ...type.props } : {}; - let vNode = isClone ? type.belongClassVNode : getProcessingClassVNode(); + let vNode = isClone ? type[BELONG_CLASS_VNODE_KEY] : getProcessingClassVNode(); if (setting !== null && setting !== undefined) { - const keys = Object.keys(setting); - const keyLength = keys.length; - for (let i = 0; i < keyLength; i++) { - const k = keys[i]; - if (isValidKey(k)) { + + for (const k in setting) { + if (!keyArray.includes(k)) { props[k] = setting[k]; } } @@ -90,7 +106,6 @@ function buildElement(isClone, type, setting, children) { lineNumber: setting.__source.lineNumber, }; } - return JSXElement(element, key, ref, vNode, props, src); } @@ -107,3 +122,12 @@ export function cloneElement(element, setting, ...children) { export function isValidElement(element) { return !!(element && element.vtype === TYPE_COMMON_ELEMENT); } + +// 兼容高版本的babel编译方式 +export function jsx(type, setting, key) { + if (setting.key === undefined && key !== undefined) { + setting.key = key; + } + + return buildElement(false, type, setting, []); +} diff --git a/libs/horizon/src/external/TestUtil.ts b/libs/horizon/src/external/TestUtil.ts new file mode 100644 index 00000000..3b5b24ca --- /dev/null +++ b/libs/horizon/src/external/TestUtil.ts @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + + +import {asyncUpdates} from '../renderer/TreeBuilder'; +import {callRenderQueueImmediate} from '../renderer/taskExecutor/RenderQueue'; +import {hasAsyncEffects, runAsyncEffects} from '../renderer/submit/HookEffectHandler'; +import {isPromise} from '../renderer/ErrorHandler'; + +interface Thenable { + then(resolve: (val?: any) => void, reject: (err: any) => void): void; +} + +// 防止死循环 +const LOOPING_LIMIT = 50; +let loopingCount = 0; +function callRenderQueue() { + callRenderQueueImmediate(); + + while (hasAsyncEffects() && loopingCount < LOOPING_LIMIT) { + loopingCount++; + runAsyncEffects(); + // effects可能产生刷新任务,这里再执行一次 + callRenderQueueImmediate(); + } +} + +// act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。 +function act(fun: () => void | Thenable): Thenable { + const funRet = asyncUpdates(fun); + + callRenderQueue(); + + // 如果fun返回的是Promise + if (isPromise(funRet)) { + // testing-library会返回Promise + return { + then(resolve, reject) { + funRet.then( + () => { + if (typeof setImmediate === 'function') { + // 通过setImmediate回调,用于等待业务的setTimeout完成 + setImmediate(() => { + callRenderQueue(); + resolve(); + }); + } else { + callRenderQueue(); + resolve(); + } + }, + err => { + reject(err); + }, + ); + }, + }; + } else { + return { + then(resolve) { + resolve(); + }, + }; + } +} + +export { + act +}; diff --git a/libs/horizon/src/horizonx/CommonUtils.ts b/libs/horizon/src/horizonx/CommonUtils.ts index fcf60054..46f82ba5 100644 --- a/libs/horizon/src/horizonx/CommonUtils.ts +++ b/libs/horizon/src/horizonx/CommonUtils.ts @@ -84,7 +84,6 @@ export function isSame(x, y) { export function getDetailedType(val: any) { if (val === undefined) return 'undefined'; if (val === null) return 'null'; - if (isCollection(val)) return 'collection'; if (isPromise(val)) return 'promise'; if (isArray(val)) return 'array'; if (isWeakMap(val)) return 'weakMap'; @@ -116,12 +115,30 @@ export function resolveMutation(from, to) { if (res[i].mutation) found = true; } } - // TODO: resolve shifts + + // need to resolve shifts return { mutation: found, items: res, from, to }; } case 'object': { - let keys = Object.keys({ ...from, ...to }); + if (from._type && from._type === to._type) { + if (from._type === 'Map') { + const entries = resolveMutation(from.entries, to.entries); + return { + mutation: entries.items.some(item => item.mutation), + from, + to, + entries: entries.items, + }; + } + + if (from._type === 'Set') { + const values = resolveMutation(from.values, to.values); + return { mutation: values.items.some(item => item.mutation), from, to, values: values.items }; + } + } + + let keys = Object.keys({ ...from, ...to }).filter(key => key !== '_horizonObserver'); const res = {}; let found = false; keys.forEach(key => { @@ -142,8 +159,6 @@ export function resolveMutation(from, to) { return { mutation: found, attributes: res, from, to }; } - // TODO: implement collections - default: { if (from === to) return { mutation: false }; @@ -151,3 +166,9 @@ export function resolveMutation(from, to) { } } } + +export function omit(obj, ...attrs) { + let res = { ...obj }; + attrs.forEach(attr => delete res[attr]); + return res; +} diff --git a/libs/horizon/src/horizonx/Constants.ts b/libs/horizon/src/horizonx/Constants.ts index 92b87923..c18ce664 100644 --- a/libs/horizon/src/horizonx/Constants.ts +++ b/libs/horizon/src/horizonx/Constants.ts @@ -13,4 +13,6 @@ * See the Mulan PSL v2 for more details. */ -export const OBSERVER_KEY = '_horizonObserver'; +export const OBSERVER_KEY = typeof Symbol === 'function' ? Symbol('_horizonObserver') : '_horizonObserver'; + +export const RAW_VALUE = '_rawValue'; diff --git a/libs/horizon/src/horizonx/adapters/redux.ts b/libs/horizon/src/horizonx/adapters/redux.ts index e4d8292e..6930ecd9 100644 --- a/libs/horizon/src/horizonx/adapters/redux.ts +++ b/libs/horizon/src/horizonx/adapters/redux.ts @@ -53,6 +53,40 @@ export type ReduxMiddleware = ( type Reducer = (state: any, action: ReduxAction) => any; +function mergeData(state, data) { + if (!data) { + state.stateWrapper = data; + return; + } + + if (Array.isArray(data) && Array.isArray(state?.stateWrapper)) { + state.stateWrapper.length = data.length; + data.forEach((item, idx) => { + if (item != state.stateWrapper[idx]) { + state.stateWrapper[idx] = item; + } + }); + return; + } + + if (typeof data === 'object' && typeof state?.stateWrapper === 'object') { + Object.keys(state.stateWrapper).forEach(key => { + if (!Object.prototype.hasOwnProperty.call(data, key)) { + delete state.stateWrapper[key]; + } + }); + + Object.entries(data).forEach(([key, value]) => { + if (state.stateWrapper[key] !== value) { + state.stateWrapper[key] = value; + } + }); + return; + } + + state.stateWrapper = data; +} + export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): ReduxStoreHandler { const store = createStoreX({ id: 'defaultStore', @@ -106,7 +140,8 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): } export function combineReducers(reducers: { [key: string]: Reducer }): Reducer { - return (state = {}, action) => { + return (state, action) => { + state = state || {}; const newState = {}; Object.entries(reducers).forEach(([key, reducer]) => { newState[key] = reducer(state[key], action); @@ -115,12 +150,6 @@ export function combineReducers(reducers: { [key: string]: Reducer }): Reducer { }; } -export function applyMiddleware(...middlewares: ReduxMiddleware[]): (store: ReduxStoreHandler) => void { - return store => { - return applyMiddlewares(store, middlewares); - }; -} - function applyMiddlewares(store: ReduxStoreHandler, middlewares: ReduxMiddleware[]): void { middlewares = middlewares.slice(); middlewares.reverse(); @@ -131,6 +160,12 @@ function applyMiddlewares(store: ReduxStoreHandler, middlewares: ReduxMiddleware store.dispatch = dispatch; } +export function applyMiddleware(...middlewares: ReduxMiddleware[]): (store: ReduxStoreHandler) => void { + return store => { + return applyMiddlewares(store, middlewares); + }; +} + type ActionCreator = (...params: any[]) => ReduxAction; type ActionCreators = { [key: string]: ActionCreator }; export type BoundActionCreator = (...params: any[]) => void; diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 35f924e6..711c0d62 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -1,3 +1,5 @@ +import { isDomVNode } from '../../renderer/vnode/VNodeUtils'; +import { isMap, isSet, isWeakMap, isWeakSet } from '../CommonUtils'; import { getStore, getAllStores } from '../store/StoreHandler'; import { OBSERVED_COMPONENTS } from './constants'; @@ -8,6 +10,94 @@ export function isPanelActive() { return window['__HORIZON_DEV_HOOK__']; } +// safely serializes variables containing values wrapped in Proxy object +function getType(value) { + if (!value) return 'nullish'; + if (value.nativeEvent) return 'event'; + if (typeof value === 'function') return 'function'; + if (value.constructor?.name === 'VNode') return 'vnode'; + if (isWeakMap(value)) return 'weakMap'; + if (isWeakSet(value)) return 'weakSet'; + if (isMap(value)) return 'map'; + if (isSet(value)) return 'set'; + if (Array.isArray(value)) return 'array'; + if (typeof value === 'object') return 'object'; + return 'primitive'; +} + +function makeProxySnapshot(obj, visited: any[] = []) { + const type = getType(obj); + let clone; + + try { + //NULLISH VALUE + if (type === 'nullish') { + return obj; + } + //EVENT + if (type === 'event') return obj.type + 'Event'; + // FUNCTION + if (type === 'function') { + return obj.toString(); + } + // VNODE + if (type === 'vnode') { + return { + _type: 'VNode', + id: window['__HORIZON_DEV_HOOK__'].getVnodeId(obj), + tag: obj.tag, + }; + } + // WEAK MAP + if (type === 'weakMap') { + return { + _type: 'WeakMap', + }; + } + // WEAK SET + if (type === 'weakSet') { + return { + _type: 'WeakSet', + }; + } + // MAP + if (type === 'map') { + return { + _type: 'Map', + entries: Array.from(obj.entries()).map(([key, value]) => ({ + key: makeProxySnapshot(key), + value: makeProxySnapshot(value), + })), + }; + } + // SET + if (type === 'set') { + return { + _type: 'Set', + values: Array.from(obj).map(value => makeProxySnapshot(value)), + }; + } + // ARRAY + if (type === 'array') { + if (visited.some(item => item === obj)) return ``; + clone = []; + obj.forEach(item => clone.push(makeProxySnapshot(item, visited.concat([obj])))); + return clone; + } + // OBJECT + if (type === 'object') { + if (visited.some(item => item === obj)) return ``; + clone = {}; + Object.entries(obj).forEach(([id, value]) => (clone[id] = makeProxySnapshot(value, visited.concat([obj])))); + return clone; + } + // PRIMITIVE + return obj; + } catch (err) { + console.error('cannot serialize object. ', { err, obj, type }); + } +} + // serializes store and creates expanded object with baked-in containing current computed values function makeStoreSnapshot({ type, data }) { const expanded = {}; @@ -23,47 +113,25 @@ function makeStoreSnapshot({ type, data }) { return snapshot; } -// safely serializes variables containing values wrapped in Proxy object -function makeProxySnapshot(obj) { - let clone; - try { - if (!obj) { - return obj; - } - if (obj.nativeEvent) return obj.type + 'Event'; - if (typeof obj === 'function') { - return obj.toString(); - } - if (Array.isArray(obj)) { - clone = []; - obj.forEach(item => clone.push(makeProxySnapshot(item))); - return clone; - } else if (typeof obj === 'object') { - clone = {}; - Object.entries(obj).forEach(([id, value]) => (clone[id] = makeProxySnapshot(value))); - return clone; - } - return obj; - } catch (err) { - throw console.log('cannot serialize object. ' + err); - } -} - export const devtools = { // returns vNode id from horizon devtools getVNodeId: vNode => { - if (!isPanelActive()) return; + if (!isPanelActive()) { + return null; + } window['__HORIZON_DEV_HOOK__'].send(); // update list first return window['__HORIZON_DEV_HOOK__'].getVnodeId(vNode); }, // sends horizonx devtool message to extension emit: (type, data) => { - if (!isPanelActive()) return; + if (!isPanelActive()) { + return; + } window.postMessage({ type: 'HORIZON_DEV_TOOLS', payload: makeStoreSnapshot({ type, data }), from: 'dev tool hook', - }); + }, ''); }, }; @@ -71,22 +139,26 @@ export const devtools = { function getAffectedComponents() { const allStores = getAllStores(); const keys = Object.keys(allStores); - let res = {}; + const res = {}; keys.forEach(key => { + if (!allStores[key].$config.state._horizonObserver.keyVNodes) { + res[key] = []; + return; + } const subRes = new Set(); const process = Array.from(allStores[key].$config.state._horizonObserver.keyVNodes.values()); while (process.length) { - let pivot = process.shift(); - if (pivot?.tag) subRes.add(pivot); - if (pivot?.toString() === '[object Set]') Array.from(pivot).forEach(item => process.push(item)); + const pivot = process.shift() as { tag: 'string' }; + if (pivot.tag) subRes.add(pivot); + if (pivot.toString() === '[object Set]') Array.from(pivot).forEach(item => process.push(item)); } - res[key] = Array.from(subRes).map(vnode => { + res[key] = Array.from(subRes).map(vNode => { return { - name: vnode?.type + name: vNode?.type .toString() .replace(/\{.*\}/, '{...}') .replace('function ', ''), - nodeId: window.__HORIZON_DEV_HOOK__.getVnodeId(vnode), + nodeId: window.__HORIZON_DEV_HOOK__.getVnodeId(vNode), }; }); }); @@ -95,7 +167,7 @@ function getAffectedComponents() { } // listens to messages from background -window.addEventListener('message', messageEvent => { +window.addEventListener('message', (messageEvent?) => { if (messageEvent?.data?.payload?.type === 'horizonx request observed components') { // get observed components setTimeout(() => { @@ -103,7 +175,7 @@ window.addEventListener('message', messageEvent => { type: 'HORIZON_DEV_TOOLS', payload: { type: OBSERVED_COMPONENTS, data: getAffectedComponents() }, from: 'dev tool hook', - }); + }, ''); }, 100); } @@ -117,4 +189,38 @@ window.addEventListener('message', messageEvent => { const params = data.params; action(...params); } + + // queues store action + if (messageEvent?.data?.payload?.type === 'horizonx queue action') { + const data = messageEvent.data.payload.data; + const store = getStore(data.storeId); + if (!store?.[data.action]) return; + + const action = store.$queue[data.action]; + const params = data.params; + action(...params); + } + + // queues change store state + if (messageEvent?.data?.payload?.type === 'horizonx change state') { + const data = messageEvent.data.payload; + const store = getStore(data.storeId); + if (!store) return; + let parent = store.$s; + if (data.operation === 'edit') { + try { + const path = messageEvent.data.payload.path; + + while (path.length > 1) { + parent = parent[path.pop()]; + } + + parent[path[0]] = messageEvent.data.payload.value; + } catch (err) { + console.error(err); + } + } + + // need to implement add and delete element + } }); diff --git a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts index 5bb8a9b9..aea366d9 100644 --- a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts +++ b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts @@ -31,15 +31,16 @@ export class HooklessObserver implements IObserver { this.listeners = this.listeners.filter(item => item != listener); } + getListeners() { + return this.listeners; + } + setProp(key: string | symbol, mutation: any): void { this.triggerChangeListeners(mutation); } triggerChangeListeners(mutation: any): void { this.listeners.forEach(listener => { - if (!listener) { - return; - } listener(mutation); }); } diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index 56ffd78c..286c2410 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -81,7 +81,7 @@ export class Observer implements IObserver { const vNodes = this.keyVNodes.get(key); //NOTE: using Set directly can lead to deadlock const vNodeArray = Array.from(vNodes || []); - vNodeArray?.forEach((vNode: VNode) => { + vNodeArray.forEach((vNode: VNode) => { if (vNode.isStoreChange) { // VNode已经被触发过,不再重复触发 return; @@ -97,10 +97,6 @@ export class Observer implements IObserver { } triggerUpdate(vNode: VNode): void { - if (!vNode) { - return; - } - // 触发VNode更新 launchUpdateFromVNode(vNode); } diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts index 5d29f34b..c06c7d71 100644 --- a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -20,14 +20,30 @@ import { isArray, isCollection, isObject } from '../CommonUtils'; import { createArrayProxy } from './handlers/ArrayProxyHandler'; import { createCollectionProxy } from './handlers/CollectionProxyHandler'; import type { IObserver } from '../types'; -import { OBSERVER_KEY } from '../Constants'; +import { OBSERVER_KEY, RAW_VALUE } from '../Constants'; // 保存rawObj -> Proxy const proxyMap = new WeakMap(); export const hookObserverMap = new WeakMap(); -export function createProxy(rawObj: any, id, isHookObserver = true): any { +export function getObserver(rawObj: any): Observer { + return rawObj[OBSERVER_KEY]; +} + +const setObserverKey = typeof OBSERVER_KEY === 'string' + ? (rawObj, observer) => { + Object.defineProperty(rawObj, OBSERVER_KEY, { + configurable: false, + enumerable: false, + value: observer, + }); + } + : (rawObj, observer) => { + rawObj[OBSERVER_KEY] = observer; + }; + +export function createProxy(rawObj: any, listener: { current: (...args) => any }, isHookObserver = true): any { // 不是对象(是原始数据类型)不用代理 if (!(rawObj && isObject(rawObj))) { return rawObj; @@ -48,7 +64,7 @@ export function createProxy(rawObj: any, id, isHookObserver = true): any { let observer: IObserver = getObserver(rawObj); if (!observer) { observer = isHookObserver ? new Observer() : new HooklessObserver(); - rawObj[OBSERVER_KEY] = observer; + setObserverKey(rawObj, observer); } hookObserverMap.set(rawObj, isHookObserver); @@ -56,16 +72,35 @@ export function createProxy(rawObj: any, id, isHookObserver = true): any { // 创建Proxy let proxyObj; if (!isHookObserver) { - proxyObj = createObjectProxy(rawObj, true); + proxyObj = createObjectProxy(rawObj, { + current: change => { + listener.current(change); + }, + }, + true); } else if (isArray(rawObj)) { // 数组 - proxyObj = createArrayProxy(rawObj as []); + proxyObj = createArrayProxy(rawObj as [], { + current: change => { + listener.current(change); + }, + }); } else if (isCollection(rawObj)) { // 集合 - proxyObj = createCollectionProxy(rawObj); + proxyObj = createCollectionProxy(rawObj, { + current: change => { + listener.current(change); + }, + }, + true); } else { // 原生对象 或 函数 - proxyObj = createObjectProxy(rawObj); + proxyObj = createObjectProxy(rawObj, { + current: change => { + listener.current(change); + }, + }, + false); } proxyMap.set(rawObj, proxyObj); @@ -74,6 +109,6 @@ export function createProxy(rawObj: any, id, isHookObserver = true): any { return proxyObj; } -export function getObserver(rawObj: any): Observer { - return rawObj[OBSERVER_KEY]; +export function toRaw(observed: T): T { + return observed && (observed)[RAW_VALUE]; } diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index 45526106..ce6c73c3 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -13,42 +13,11 @@ * See the Mulan PSL v2 for more details. */ -import { getObserver } from '../ProxyHandler'; +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { isSame, isValidIntegerKey } from '../../CommonUtils'; -import { get as objectGet } from './ObjectProxyHandler'; import { resolveMutation } from '../../CommonUtils'; import { isPanelActive } from '../../devtools'; - -export function createArrayProxy(rawObj: any[]): any[] { - const handle = { - get, - set, - }; - - return new Proxy(rawObj, handle); -} - -function get(rawObj: any[], key: string, receiver: any) { - if (key === 'watch') { - const observer = getObserver(rawObj); - - return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { - if (!observer.watchers[prop]) { - observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; - } - observer.watchers[prop].push(handler); - return () => { - observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); - }; - }; - } - - if (isValidIntegerKey(key) || key === 'length') { - return objectGet(rawObj, key, receiver); - } - - return Reflect.get(rawObj, key, receiver); -} +import { OBSERVER_KEY, RAW_VALUE } from '../../Constants'; function set(rawObj: any[], key: string, value: any, receiver: any) { const oldValue = rawObj[key]; @@ -62,7 +31,7 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const newLength = rawObj.length; const observer = getObserver(rawObj); - const mutation = isPanelActive() ? resolveMutation(oldArray, rawObj) : { mutation: true, from: [], to: rawObj }; + const mutation = isPanelActive() ? resolveMutation(oldArray, rawObj) : resolveMutation(null, rawObj); if (!isSame(newValue, oldValue)) { // 值不一样,触发监听器 @@ -83,3 +52,109 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { return ret; } + +export function createArrayProxy(rawObj: any[], listener: { current: (...args) => any }): any[] { + let listeners = [] as ((...args) => void)[]; + + function objectGet(rawObj: object, key: string | symbol, receiver: any, singleLevel = false): any { + // The observer object of symbol ('_horizonObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep. + if (key === OBSERVER_KEY) { + return undefined; + } + + const observer = getObserver(rawObj); + + if (key === 'watch') { + return (prop, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (key === 'addListener') { + return listener => { + listeners.push(listener); + }; + } + + if (key === 'removeListener') { + return listener => { + listeners = listeners.filter(item => item != listener); + }; + } + + observer.useProp(key); + + const value = Reflect.get(rawObj, key, receiver); + + // 对于prototype不做代理 + if (key !== 'prototype') { + // 对于value也需要进一步代理 + const valProxy = singleLevel + ? value + : createProxy(value, { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [key]: change.mutation.from }, + { ...rawObj, [key]: change.mutation.to } + ); + listener.current(mutation); + listeners.forEach(lst => lst(mutation)); + }, + }, + hookObserverMap.get(rawObj) + ); + + return valProxy; + } + + return value; + } + + function get(rawObj: any[], key: string, receiver: any) { + if (key === 'watch') { + const observer = getObserver(rawObj); + + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (isValidIntegerKey(key) || key === 'length') { + return objectGet(rawObj, key, receiver); + } + + if (key === RAW_VALUE) { + return rawObj; + } + + return Reflect.get(rawObj, key, receiver); + } + + const handle = { + get, + set, + }; + + getObserver(rawObj).addListener(change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + listener.current(change); + listeners.forEach(lst => lst(change)); + }); + + return new Proxy(rawObj, handle); +} diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index f351065b..d2f3cbb7 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -13,223 +13,25 @@ * See the Mulan PSL v2 for more details. */ -import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; -import { isMap, isWeakMap, isSame } from '../../CommonUtils'; -import { resolveMutation } from '../../CommonUtils'; -import { isPanelActive } from '../../devtools'; +import { isWeakMap, isWeakSet, isSet } from '../../CommonUtils'; +import { createWeakSetProxy } from './WeakSetProxy'; +import { createSetProxy } from './SetProxy'; +import { createWeakMapProxy } from './WeakMapProxy'; +import { createMapProxy } from './MapProxy'; -const COLLECTION_CHANGE = '_collectionChange'; -const handler = { - get, - set, - add, - delete: deleteFun, - clear, - has, - entries, - forEach, - keys, - values, - // 判断Symbol类型,兼容IE - [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf, -}; - -export function createCollectionProxy(rawObj: Object, hookObserver = true): Object { - const boundHandler = {}; - Object.entries(handler).forEach(([id, val]) => { - boundHandler[id] = (...args: any[]) => { - return (val as any)(...args, hookObserver); - }; - }); - return new Proxy(rawObj, { ...boundHandler }); -} - -function get(rawObj: { size: number }, key: any, receiver: any): any { - if (key === 'size') { - return size(rawObj); - } else if (key === 'get') { - return getFun.bind(null, rawObj); - } else if (Object.prototype.hasOwnProperty.call(handler, key)) { - const value = Reflect.get(handler, key, receiver); - return value.bind(null, rawObj); - } else if (key === 'watch') { - const observer = getObserver(rawObj); - - return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { - if (!observer.watchers[prop]) { - observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; - } - observer.watchers[prop].push(handler); - return () => { - observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); - }; - }; +export function createCollectionProxy( + rawObj: Object, + listener: { current: (...args) => any }, + hookObserver = true +): Object { + if (isWeakSet(rawObj)) { + return createWeakSetProxy(rawObj, listener, hookObserver); } - - return Reflect.get(rawObj, key, receiver); -} - -function getFun(rawObj: { get: (key: any) => any }, key: any) { - const observer = getObserver(rawObj); - observer.useProp(key); - - const value = rawObj.get(key); - // 对于value也需要进一步代理 - const valProxy = createProxy(value, hookObserverMap.get(rawObj)); - - return valProxy; -} - -// Map的set方法 -function set( - rawObj: { get: (key: any) => any; set: (key: any, value: any) => any; has: (key: any) => boolean }, - key: any, - value: any -) { - const oldValue = rawObj.get(key); - const newValue = value; - rawObj.set(key, newValue); - const valChange = !isSame(newValue, oldValue); - const observer = getObserver(rawObj); - - const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : { mutation: true, from: null, to: rawObj }; - - if (valChange || !rawObj.has(key)) { - observer.setProp(COLLECTION_CHANGE, mutation); + if (isSet(rawObj)) { + return createSetProxy(rawObj, listener, hookObserver); } - - if (valChange) { - if (observer.watchers?.[key]) { - observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue, mutation); - }); - } - - observer.setProp(key, mutation); + if (isWeakMap(rawObj)) { + return createWeakMapProxy(rawObj, listener, hookObserver); } - - return rawObj; -} - -// Set的add方法 -function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object { - const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; - if (!rawObj.has(value)) { - rawObj.add(value); - - const observer = getObserver(rawObj); - const mutation = isPanelActive() - ? resolveMutation(oldCollection, rawObj) - : { mutation: true, from: null, to: rawObj }; - observer.setProp(value, mutation); - observer.setProp(COLLECTION_CHANGE, mutation); - } - - return rawObj; -} - -function has(rawObj: { has: (string) => boolean }, key: any): boolean { - const observer = getObserver(rawObj); - observer.useProp(key); - - return rawObj.has(key); -} - -function clear(rawObj: { size: number; clear: () => void }) { - const oldSize = rawObj.size; - rawObj.clear(); - - if (oldSize > 0) { - const observer = getObserver(rawObj); - observer.allChange(); - } -} - -function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) { - const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; - if (rawObj.has(key)) { - rawObj.delete(key); - - const observer = getObserver(rawObj); - const mutation = isPanelActive() - ? resolveMutation(oldCollection, rawObj) - : { mutation: true, from: null, to: rawObj }; - observer.setProp(key, mutation); - observer.setProp(COLLECTION_CHANGE, mutation); - - return true; - } - - return false; -} - -function size(rawObj: { size: number }) { - const observer = getObserver(rawObj); - observer.useProp(COLLECTION_CHANGE); - return rawObj.size; -} - -function keys(rawObj: { keys: () => { next: () => { value: any; done: boolean } } }) { - return wrapIterator(rawObj, rawObj.keys()); -} - -function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) { - return wrapIterator(rawObj, rawObj.values()); -} - -function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) { - return wrapIterator(rawObj, rawObj.entries(), true); -} - -function forOf(rawObj: { - entries: () => { next: () => { value: any; done: boolean } }; - values: () => { next: () => { value: any; done: boolean } }; -}) { - const isMapType = isMap(rawObj) || isWeakMap(rawObj); - const iterator = isMapType ? rawObj.entries() : rawObj.values(); - return wrapIterator(rawObj, iterator, isMapType); -} - -function forEach( - rawObj: { forEach: (callback: (value: any, key: any) => void) => void }, - callback: (valProxy: any, keyProxy: any, rawObj: any) => void -) { - const observer = getObserver(rawObj); - observer.useProp(COLLECTION_CHANGE); - rawObj.forEach((value, key) => { - const valProxy = createProxy(value, hookObserverMap.get(rawObj)); - const keyProxy = createProxy(key, hookObserverMap.get(rawObj)); - // 最后一个参数要返回代理对象 - return callback(valProxy, keyProxy, rawObj); - }); -} - -function wrapIterator(rawObj: Object, rawIt: { next: () => { value: any; done: boolean } }, isPair = false) { - const observer = getObserver(rawObj); - const hookObserver = hookObserverMap.get(rawObj); - observer.useProp(COLLECTION_CHANGE); - - return { - next() { - const { value, done } = rawIt.next(); - if (done) { - return { value: createProxy(value, hookObserver), done }; - } - - observer.useProp(COLLECTION_CHANGE); - - let newVal; - if (isPair) { - newVal = [createProxy(value[0], hookObserver), createProxy(value[1], hookObserver)]; - } else { - newVal = createProxy(value, hookObserver); - } - - return { value: newVal, done }; - }, - // 判断Symbol类型,兼容IE - [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']() { - return this; - }, - }; + return createMapProxy(rawObj, listener, hookObserver); } diff --git a/libs/horizon/src/horizonx/proxy/handlers/MapProxy.ts b/libs/horizon/src/horizonx/proxy/handlers/MapProxy.ts new file mode 100644 index 00000000..8529f35c --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers/MapProxy.ts @@ -0,0 +1,413 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; +import { isSame } from '../../CommonUtils'; +import { resolveMutation } from '../../CommonUtils'; +import { isPanelActive } from '../../devtools'; +import { RAW_VALUE } from '../../Constants'; + +const COLLECTION_CHANGE = '_collectionChange'; + +export function createMapProxy( + rawObj: Object, + listener: { current: (...args) => any }, + hookObserver = true +): Object { + let listeners: ((mutation) => {})[] = []; + let oldData: [any, any][] = []; + let proxies = new Map(); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function getFun(rawObj: { get: (key: any) => any; has: (key: any) => boolean }, key: any): any { + const keyProxy = rawObj.has(key) ? key : proxies.get(key); + if (!keyProxy) return; + const observer = getObserver(rawObj); + observer.useProp(key); + const value = rawObj.get(keyProxy); + + // 对于value也需要进一步代理 + const valProxy = createProxy(value, { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [key]: change.mutation.from }, + { ...rawObj, [key]: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }, + hookObserverMap.get(rawObj) + ); + + return valProxy; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Map的set方法 + function set( + rawObj: { + get: (key: any) => any; + set: (key: any, value: any) => any; + has: (key: any) => boolean; + entries: () => [any, any][]; + }, + key: any, + value: any + ): any { + if (rawObj.has(key) || rawObj.has(proxies.get(key))) { + // VALUE CHANGE (whole value for selected key is changed) + const oldValue = rawObj.get(proxies.get(key)); + if (isSame(value, oldValue)) return; + rawObj.set(proxies.get(key), value); + const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : resolveMutation(null, rawObj); + const observer = getObserver(rawObj); + observer.setProp(COLLECTION_CHANGE, mutation); + + if (observer.watchers[key]) { + observer.watchers[key].forEach(cb => { + cb(key, oldValue, value, mutation); + }); + } + + observer.setProp(key, mutation); + oldData = [...Array.from(rawObj.entries())]; + } else { + // NEW VALUE + const keyProxy = createProxy(key, { + current: change => { + // KEY CHANGE + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, ['_keyChange']: change.mutation.from }, + { ...rawObj, ['_keyChange']: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }, + hookObserverMap.get(rawObj) + ); + proxies.set(key, keyProxy); + + rawObj.set(keyProxy, value); + const observer = getObserver(rawObj); + const mutation = resolveMutation( + { + _type: 'Map', + entries: oldData, + }, + { + _type: 'Map', + entries: Array.from(rawObj.entries()), + } + ); + observer.setProp(COLLECTION_CHANGE, mutation); + + if (observer.watchers?.[key]) { + observer.watchers[key].forEach(cb => { + cb(key, null, value, mutation); + }); + } + observer.setProp(key, mutation); + oldData = [...Array.from(rawObj.entries())]; + } + + return rawObj; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function has(rawObj: { has: (any) => boolean }, key: any): boolean { + const observer = getObserver(rawObj); + observer.useProp(key); + if (rawObj.has(key)) { + return true; + } + return proxies.has(key); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function clear(rawObj: { size: number; clear: () => void; entries: () => [any, any][] }) { + const oldSize = rawObj.size; + rawObj.clear(); + + if (oldSize > 0) { + const observer = getObserver(rawObj); + observer.allChange(); + oldData = [...Array.from(rawObj.entries())]; + } + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function deleteFun( + rawObj: { has: (key: any) => boolean; delete: (key: any) => void; entries: () => [any, any][] }, + key: any + ) { + if (rawObj.has(key) || proxies.has(key)) { + rawObj.delete(key || proxies.get(key)); + + const observer = getObserver(rawObj); + const mutation = resolveMutation( + { + _type: 'Map', + entries: oldData, + }, + { + _type: 'Map', + entries: Array.from(rawObj.entries()), + } + ); + observer.setProp(key, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); + + oldData = [...Array.from(rawObj.entries())]; + return true; + } + + return false; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function forEach( + rawObj: { forEach: (callback: (value: any, key: any) => void) => void }, + callback: (valProxy: any, keyProxy: any, rawObj: any) => void + ) { + const observer = getObserver(rawObj); + observer.useProp(COLLECTION_CHANGE); + rawObj.forEach((value, key) => { + const keyProxy = createProxy(value, { + current: change => { + //KEY ATTRIBUTES CHANGED + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, ['_keyChange']: change.mutation.from }, + { ...rawObj, ['_keyChange']: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }, + hookObserverMap.get(rawObj) + ); + const valProxy = createProxy(key, { + current: change => { + // VALUE ATTRIBUTE CHANGED + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, key: change.mutation.from }, + { ...rawObj, key: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }, + hookObserverMap.get(rawObj) + ); + // 最后一个参数要返回代理对象 + return callback(keyProxy, valProxy, rawObj); + }); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function wrapIterator(rawObj: Object, rawIt: { next: () => { value: any; done: boolean } }, type) { + const observer = getObserver(rawObj); + const hookObserver = hookObserverMap.get(rawObj); + observer.useProp(COLLECTION_CHANGE); + + return { + next() { + const { value, done } = rawIt.next(); + if (done) { + return { + value: createProxy(value, { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [value]: change.mutation.from }, + { ...rawObj, [value]: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }, + hookObserver + ), + done, + }; + } + + observer.useProp(COLLECTION_CHANGE); + let newVal; + if (type === 'entries') { + //ENTRY CHANGED + newVal = [ + createProxy(value[0], { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, ['itemChange']: { key: change.mutation.from, value: value[1] } }, + { ...rawObj, ['itemChange']: { key: change.mutation.to, value: value[1] } } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }, + hookObserver + ), + createProxy(value[1], { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, item: { key: value[0], value: change.mutation.from } }, + { ...rawObj, item: { key: value[0], value: change.mutation.to } } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }, + hookObserver + ), + ]; + } else { + // SINGLE VALUE CHANGED + newVal = createProxy(value, { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [type === 'keys' ? 'key' : 'value']: change.mutation.from }, + { ...rawObj, [type === 'keys' ? 'key' : 'value']: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }, + hookObserver + ); + } + + return { value: newVal, done }; + }, + // 判断Symbol类型,兼容IE + [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']() { + return this; + }, + }; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function size(rawObj: { size: number }) { + const observer = getObserver(rawObj); + observer.useProp(COLLECTION_CHANGE); + return rawObj.size; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function keys(rawObj: { keys: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.keys(), 'keys'); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.values(), 'values'); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.entries(), 'entries'); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function forOf(rawObj: { + entries: () => { next: () => { value: any; done: boolean } }; + values: () => { next: () => { value: any; done: boolean } }; + }) { + return wrapIterator(rawObj, rawObj.entries(), 'entries'); + } + + const handler = { + get, + set, + delete: deleteFun, + clear, + has, + entries, + forEach, + keys, + values, + // 判断Symbol类型,兼容IE + [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf, + }; + + function get(rawObj: { size: number }, key: any, receiver: any): any { + if (key === 'size') { + return size(rawObj); + } + + if (key === 'get') { + return getFun.bind(null, rawObj); + } + + if (Object.prototype.hasOwnProperty.call(handler, key)) { + const value = Reflect.get(handler, key, receiver); + return value.bind(null, rawObj); + } + + if (key === 'watch') { + const observer = getObserver(rawObj); + + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (key === 'addListener') { + return listener => { + listeners.push(listener); + }; + } + + if (key === 'removeListener') { + return listener => { + listeners = listeners.filter(item => item != listener); + }; + } + + if (key === RAW_VALUE) { + return rawObj; + } + + return Reflect.get(rawObj, key, receiver); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const boundHandler = {}; + Object.entries(handler).forEach(([id, val]) => { + boundHandler[id] = (...args: any[]) => { + return (val as any)(...args, hookObserver); + }; + }); + + getObserver(rawObj).addListener(change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + listener.current(change); + listeners.forEach(lst => lst(change)); + }); + return new Proxy(rawObj, { ...boundHandler }); +} diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index 19ea5be2..88a0c93b 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -15,73 +15,18 @@ import { isSame, resolveMutation } from '../../CommonUtils'; import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; -import { OBSERVER_KEY } from '../../Constants'; +import { OBSERVER_KEY, RAW_VALUE } from '../../Constants'; import { isPanelActive } from '../../devtools'; -export function createObjectProxy(rawObj: T, singleLevel = false): ProxyHandler { - const proxy = new Proxy(rawObj, { - get: (...args) => get(...args, singleLevel), - set, - }); - - return proxy; -} - -export function get(rawObj: object, key: string | symbol, receiver: any, singleLevel = false): any { - // The observer object of symbol ('_horizonObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep. - if (key === OBSERVER_KEY) { - return undefined; - } - - const observer = getObserver(rawObj); - - if (key === 'watch') { - return (prop, handler: (key: string, oldValue: any, newValue: any) => void) => { - if (!observer.watchers[prop]) { - observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; - } - observer.watchers[prop].push(handler); - return () => { - observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); - }; - }; - } - - if (key === 'addListener') { - return observer.addListener.bind(observer); - } - - if (key === 'removeListener') { - return observer.removeListener.bind(observer); - } - - observer.useProp(key); - - const value = Reflect.get(rawObj, key, receiver); - - // 对于prototype不做代理 - if (key !== 'prototype') { - // 对于value也需要进一步代理 - const valProxy = singleLevel ? value : createProxy(value, hookObserverMap.get(rawObj)); - - return valProxy; - } - - return value; -} - -export function set(rawObj: object, key: string, value: any, receiver: any): boolean { +function set(rawObj: object, key: string, value: any, receiver: any): boolean { const oldObject = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; const observer = getObserver(rawObj); - if (value && key == 'removeListener') { - observer.removeListener(value); - } const oldValue = rawObj[key]; const newValue = value; const ret = Reflect.set(rawObj, key, newValue, receiver); - const mutation = isPanelActive() ? resolveMutation(oldObject, rawObj) : { mutation: true, from: null, to: rawObj }; + const mutation = isPanelActive() ? resolveMutation(oldObject, rawObj) : resolveMutation(null, rawObj); if (!isSame(newValue, oldValue)) { if (observer.watchers?.[key]) { @@ -93,3 +38,91 @@ export function set(rawObj: object, key: string, value: any, receiver: any): boo } return ret; } + +export function createObjectProxy( + rawObj: T, + listener: { current: (...args) => any }, + singleLevel = false +): ProxyHandler { + let listeners = [] as ((...args) => void)[]; + + function get(rawObj: object, key: string | symbol, receiver: any): any { + // The observer object of symbol ('_horizonObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep. + if (key === OBSERVER_KEY) { + return undefined; + } + + const observer = getObserver(rawObj); + + if (key === 'watch') { + return (prop, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (key === 'addListener') { + return listener => { + listeners.push(listener); + }; + } + + if (key === 'removeListener') { + return listener => { + listeners = listeners.filter(item => item != listener); + }; + } + + if (key === RAW_VALUE) { + return rawObj; + } + + observer.useProp(key); + + const value = Reflect.get(rawObj, key, receiver); + + // 对于prototype不做代理 + if (key !== 'prototype') { + // 对于value也需要进一步代理 + const valProxy = singleLevel + ? value + : createProxy(value, { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [key]: change.mutation.from }, + { ...rawObj, [key]: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }, + hookObserverMap.get(rawObj) + ); + + return valProxy; + } + + return value; + } + + const proxy = new Proxy(rawObj, { + get, + set, + }); + + getObserver(rawObj).addListener(change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + listener.current(change); + listeners.forEach(lst => lst(change)); + }); + + return proxy; +} diff --git a/libs/horizon/src/horizonx/proxy/handlers/SetProxy.ts b/libs/horizon/src/horizonx/proxy/handlers/SetProxy.ts new file mode 100644 index 00000000..933ad73c --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers/SetProxy.ts @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { resolveMutation } from '../../CommonUtils'; +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; +import { RAW_VALUE } from '../../Constants'; + +const COLLECTION_CHANGE = '_collectionChange'; + +export function createSetProxy( + rawObj: T, + listener: { current: (...args) => any }, + hookObserver = true +): ProxyHandler { + let listeners: ((mutation) => {})[] = []; + let proxies = new WeakMap(); + + // Set的add方法 + function add(rawObj: { add: (any) => void; has: (any) => boolean; values: () => any[] }, value: any): Object { + if (!rawObj.has(proxies.get(value))) { + const proxy = createProxy(value, { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, valueChange: change.mutation.from }, + { ...rawObj, valueChange: change.mutation.to } + ); + listener.current({ + ...change, + mutation, + }); + listeners.forEach(lst => + lst({ + ...change, + mutation, + }) + ); + }, + }, + hookObserverMap.get(rawObj) + ); + const oldValues = Array.from(rawObj.values()); + + proxies.set(value, proxy); + + rawObj.add(proxies.get(value)); + + const observer = getObserver(rawObj); + const mutation = resolveMutation( + { + _type: 'Set', + values: oldValues, + }, + { + _type: 'Set', + values: Array.from(rawObj.values()), + } + ); + + observer.setProp(value, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); + } + + return rawObj; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function has(rawObj: { has: (string) => boolean }, value: any): boolean { + const observer = getObserver(rawObj); + observer.useProp(value); + + return rawObj.has(proxies.get(value)); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function deleteFun( + rawObj: { has: (key: any) => boolean; delete: (value: any) => void; values: () => any[] }, + value: any + ) { + const val = rawObj.has(proxies.get(value)) ? proxies.get(value) : value; + if (rawObj.has(val)) { + const oldValues = Array.from(rawObj.values()); + rawObj.delete(val); + + proxies.delete(value); + + const observer = getObserver(rawObj); + const mutation = resolveMutation( + { + _type: 'Set', + values: oldValues, + }, + { + _type: 'Set', + values: Array.from(rawObj.values()), + } + ); + + observer.setProp(value, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); + + return true; + } + + return false; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function clear(rawObj: { size: number; clear: () => void }) { + const oldSize = rawObj.size; + rawObj.clear(); + + if (oldSize > 0) { + const observer = getObserver(rawObj); + observer.allChange(); + } + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function size(rawObj: { size: number }) { + const observer = getObserver(rawObj); + observer.useProp(COLLECTION_CHANGE); + return rawObj.size; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const handler = { + get, + add, + delete: deleteFun, + has, + clear, + forEach, + forOf, + entries, + keys, + values, + [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf, + }; + + function get(rawObj: { size: number }, key: any, receiver: any): any { + if (Object.prototype.hasOwnProperty.call(handler, key)) { + const value = Reflect.get(handler, key, receiver); + return value.bind(null, rawObj); + } + + if (key === 'size') { + return size(rawObj); + } + + if (key === 'addListener') { + return listener => { + listeners.push(listener); + }; + } + + if (key === 'removeListener') { + return listener => { + listeners = listeners.filter(item => item != listener); + }; + } + if (key === 'watch') { + const observer = getObserver(rawObj); + + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (key === RAW_VALUE) { + return rawObj; + } + + return Reflect.get(rawObj, key, receiver); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + function wrapIterator(rawObj: Object, rawIt: { next: () => { value: any; done: boolean } }) { + const observer = getObserver(rawObj); + const hookObserver = hookObserverMap.get(rawObj); + observer.useProp(COLLECTION_CHANGE); + + return { + next() { + const currentListener = { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, valueChange: change.mutation.from }, + { ...rawObj, valueChange: change.mutation.to } + ); + listener.current({ + ...change, + mutation, + }); + listeners.forEach(lst => + lst({ + ...change, + mutation, + }) + ); + }, + }; + const { value, done } = rawIt.next(); + if (done) { + return { value: createProxy(value, currentListener, hookObserver), done }; + } + + observer.useProp(COLLECTION_CHANGE); + + let newVal; + newVal = createProxy(value, currentListener, hookObserver); + + return { value: newVal, done }; + }, + // 判断Symbol类型,兼容IE + [typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']() { + return this; + }, + }; + } + + function keys(rawObj: { keys: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.keys()); + } + + function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.values()); + } + + function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) { + return wrapIterator(rawObj, rawObj.entries()); + } + + function forOf(rawObj: { + entries: () => { next: () => { value: any; done: boolean } }; + values: () => { next: () => { value: any; done: boolean } }; + }) { + const iterator = rawObj.values(); + return wrapIterator(rawObj, iterator); + } + + function forEach( + rawObj: { forEach: (callback: (value: any, key: any) => void) => void }, + callback: (valProxy: any, keyProxy: any, rawObj: any) => void + ) { + const observer = getObserver(rawObj); + observer.useProp(COLLECTION_CHANGE); + rawObj.forEach((value, key) => { + const currentListener = { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, valueChange: change.mutation.from }, + { ...rawObj, valueChange: change.mutation.to } + ); + listener.current({ + ...change, + mutation, + }); + listeners.forEach(lst => + lst({ + ...change, + mutation, + }) + ); + }, + }; + const valProxy = createProxy(value, currentListener, hookObserverMap.get(rawObj)); + const keyProxy = createProxy(key, currentListener, hookObserverMap.get(rawObj)); + // 最后一个参数要返回代理对象 + return callback(valProxy, keyProxy, rawObj); + }); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + getObserver(rawObj).addListener(change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + listener.current(change); + listeners.forEach(lst => lst(change)); + }); + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const boundHandler = {}; + Object.entries(handler).forEach(([id, val]) => { + boundHandler[id] = (...args: any[]) => { + return (val as any)(...args, hookObserver); + }; + }); + return new Proxy(rawObj, { ...boundHandler }); +} diff --git a/libs/horizon/src/horizonx/proxy/handlers/WeakMapProxy.ts b/libs/horizon/src/horizonx/proxy/handlers/WeakMapProxy.ts new file mode 100644 index 00000000..0c818b3c --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers/WeakMapProxy.ts @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; +import { isSame } from '../../CommonUtils'; +import { resolveMutation } from '../../CommonUtils'; +import { isPanelActive } from '../../devtools'; +import { RAW_VALUE } from '../../Constants'; + +const COLLECTION_CHANGE = '_collectionChange'; + +export function createWeakMapProxy( + rawObj: Object, + listener: { current: (...args) => any }, + hookObserver = true +): Object { + let listeners: ((mutation) => {})[] = []; + + const handler = { + get, + set, + add, + delete: deleteFun, + clear, + has, + }; + + function getFun(rawObj: { get: (key: any) => any }, key: any) { + const observer = getObserver(rawObj); + observer.useProp(key); + + const value = rawObj.get(key); + // 对于value也需要进一步代理 + const valProxy = createProxy(value, { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [key]: change.mutation.from }, + { ...rawObj, [key]: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }, + hookObserverMap.get(rawObj) + ); + + return valProxy; + } + + function get(rawObj: { size: number }, key: any, receiver: any): any { + if (key === 'get') { + return getFun.bind(null, rawObj); + } + + if (Object.prototype.hasOwnProperty.call(handler, key)) { + const value = Reflect.get(handler, key, receiver); + return value.bind(null, rawObj); + } + + if (key === 'watch') { + const observer = getObserver(rawObj); + + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (key === 'addListener') { + return listener => { + listeners.push(listener); + }; + } + + if (key === 'removeListener') { + return listener => { + listeners = listeners.filter(item => item != listener); + }; + } + + if (key === RAW_VALUE) { + return rawObj; + } + + return Reflect.get(rawObj, key, receiver); + } + + // Map的set方法 + function set( + rawObj: { get: (key: any) => any; set: (key: any, value: any) => any; has: (key: any) => boolean }, + key: any, + value: any + ) { + const oldValue = rawObj.get(key); + const newValue = value; + rawObj.set(key, newValue); + const valChange = !isSame(newValue, oldValue); + const observer = getObserver(rawObj); + + const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : resolveMutation(null, rawObj); + + if (valChange || !rawObj.has(key)) { + observer.setProp(COLLECTION_CHANGE, mutation); + } + + if (valChange) { + if (observer.watchers?.[key]) { + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue, mutation); + }); + } + + observer.setProp(key, mutation); + } + + return rawObj; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Set的add方法 + function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object { + const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; + if (!rawObj.has(value)) { + rawObj.add(value); + + const observer = getObserver(rawObj); + const mutation = isPanelActive() + ? resolveMutation(oldCollection, rawObj) + : { mutation: true, from: null, to: rawObj }; + observer.setProp(value, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); + } + + return rawObj; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function has(rawObj: { has: (string) => boolean }, key: any): boolean { + const observer = getObserver(rawObj); + observer.useProp(key); + + return rawObj.has(key); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function clear(rawObj: { size: number; clear: () => void }) { + const oldSize = rawObj.size; + rawObj.clear(); + + if (oldSize > 0) { + const observer = getObserver(rawObj); + observer.allChange(); + } + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) { + const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; + if (rawObj.has(key)) { + rawObj.delete(key); + + const observer = getObserver(rawObj); + const mutation = isPanelActive() + ? resolveMutation(oldCollection, rawObj) + : { mutation: true, from: null, to: rawObj }; + observer.setProp(key, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); + + return true; + } + + return false; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + getObserver(rawObj).addListener(change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + listener.current(change); + listeners.forEach(lst => lst(change)); + }); + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const boundHandler = {}; + Object.entries(handler).forEach(([id, val]) => { + boundHandler[id] = (...args: any[]) => { + return (val as any)(...args, hookObserver); + }; + }); + return new Proxy(rawObj, { ...boundHandler }); +} diff --git a/libs/horizon/src/horizonx/proxy/handlers/WeakSetProxy.ts b/libs/horizon/src/horizonx/proxy/handlers/WeakSetProxy.ts new file mode 100644 index 00000000..e1a843cd --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers/WeakSetProxy.ts @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { resolveMutation } from '../../CommonUtils'; +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; +import { RAW_VALUE } from '../../Constants'; + +export function createWeakSetProxy( + rawObj: T, + listener: { current: (...args) => any }, + hookObserver = true, +): ProxyHandler { + let listeners: ((mutation) => {})[] = []; + let proxies = new WeakMap(); + + const handler = { + get, + add, + delete: deleteFun, + has, + }; + + function get(rawObj: { size: number }, key: any, receiver: any): any { + if (Object.prototype.hasOwnProperty.call(handler, key)) { + const value = Reflect.get(handler, key, receiver); + return value.bind(null, rawObj); + } + if (key === 'addListener') { + return listener => { + listeners.push(listener); + }; + } + + if (key === 'removeListener') { + return listener => { + listeners = listeners.filter(item => item != listener); + }; + } + if (key === 'watch') { + const observer = getObserver(rawObj); + + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; + } + observer.watchers[prop].push(handler); + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; + } + + if (key === RAW_VALUE) { + return rawObj; + } + + return Reflect.get(rawObj, key, receiver); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Set的add方法 + function add(rawObj: { add: (any) => void; has: (any) => boolean }, value: any): Object { + if (!rawObj.has(proxies.get(value))) { + const proxy = createProxy(value, { + current: change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + let mutation = resolveMutation( + { ...rawObj, [value]: change.mutation.from }, + { ...rawObj, [value]: change.mutation.to } + ); + listener.current({ ...change, mutation }); + listeners.forEach(lst => lst({ ...change, mutation })); + }, + }, + hookObserverMap.get(rawObj) + ); + + proxies.set(value, proxy); + + rawObj.add(proxies.get(value)); + + const observer = getObserver(rawObj); + const mutation = { mutation: true, from: rawObj, to: value }; + + observer.setProp(value, mutation); + } + + return rawObj; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function has(rawObj: { has: (string) => boolean }, value: any): boolean { + const observer = getObserver(rawObj); + observer.useProp(value); + + return rawObj.has(proxies.get(value)); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + function deleteFun(rawObj: { has: (key: any) => boolean; delete: (value: any) => void }, value: any) { + if (rawObj.has(proxies.get(value))) { + rawObj.delete(proxies.get(value)); + + proxies.delete(value); + + const observer = getObserver(rawObj); + const mutation = { mutation: true, from: value, to: rawObj }; + + observer.setProp(value, mutation); + + return true; + } + + return false; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + getObserver(rawObj).addListener(change => { + if (!change.parents) change.parents = []; + change.parents.push(rawObj); + listener.current(change); + listeners.forEach(lst => lst(change)); + }); + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const boundHandler = {}; + Object.entries(handler).forEach(([id, val]) => { + boundHandler[id] = (...args: any[]) => { + return (val as any)(...args, hookObserver); + }; + }); + return new Proxy(rawObj, { ...boundHandler }); +} diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index 4956fe47..47099f98 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -38,6 +38,7 @@ import { ACTION_QUEUED, INITIALIZED, QUEUE_FINISHED, + QUEUE_PENDING, STATE_CHANGE, SUBSCRIBED, UNSUBSCRIBED, @@ -52,6 +53,100 @@ const idGenerator = { const storeMap = new Map>(); +// 通过该方法执行store.$queue中的action +function tryNextAction(storeObj, proxyObj, config, plannedActions) { + if (!plannedActions.length) { + if (proxyObj.$pending) { + const timestamp = Date.now(); + const duration = timestamp - proxyObj.$pending; + proxyObj.$pending = false; + devtools.emit(QUEUE_FINISHED, { + store: storeObj, + endedAt: timestamp, + duration, + }); + } + return; + } + + const nextAction = plannedActions.shift()!; + const result = config.actions + ? config.actions[nextAction.action].bind(storeObj, proxyObj)(...nextAction.payload) + : undefined; + + if (isPromise(result)) { + result.then(value => { + nextAction.resolve(value); + tryNextAction(storeObj, proxyObj, config, plannedActions); + }); + } else { + nextAction.resolve(result); + tryNextAction(storeObj, proxyObj, config, plannedActions); + } +} + +// 删除Observers中保存的这个VNode的相关数据 +export function clearVNodeObservers(vNode: VNode) { + if (!vNode.observers) { + return; + } + + vNode.observers.forEach(observer => { + observer.clearByVNode(vNode); + }); + + vNode.observers.clear(); +} + +// 注册VNode销毁时的清理动作 +function registerDestroyFunction() { + const processingVNode = getProcessingVNode(); + + // 获取不到当前运行的VNode,说明不在组件中运行,属于非法场景 + if (!processingVNode) { + return; + } + + if (!processingVNode.observers) { + processingVNode.observers = new Set(); + } + + // 函数组件 + if (processingVNode.tag === FunctionComponent) { + const vNodeRef = useRef(processingVNode); + + useEffect(() => { + return () => { + clearVNodeObservers(vNodeRef.current); + vNodeRef.current.observers = null; + }; + }, []); + } else if (processingVNode.tag === ClassComponent) { + // 类组件 + if (!processingVNode.classComponentWillUnmount) { + processingVNode.classComponentWillUnmount = vNode => { + clearVNodeObservers(vNode); + vNode.observers = null; + }; + } + } +} + +// createStore返回的是一个getStore的函数,这个函数必须要在组件(函数/类组件)里面被执行,因为要注册VNode销毁时的清理动作 +function createGetStore, C extends UserComputedValues>( + storeObj: StoreObj +): () => StoreObj { + const getStore = () => { + if (!storeObj.$config.options?.isReduxAdapter) { + registerDestroyFunction(); + } + + return storeObj; + }; + + return getStore; +} + export function createStore, C extends UserComputedValues>( config: StoreConfig ): () => StoreObj { @@ -62,7 +157,11 @@ export function createStore, C extend const id = config.id || idGenerator.get('UNNAMED_STORE'); - const proxyObj = createProxy(config.state, id, !config.options?.isReduxAdapter); + const listener = { + current: listener => {}, + }; + + const proxyObj = createProxy(config.state, listener, !config.options?.isReduxAdapter); proxyObj.$pending = false; @@ -76,16 +175,28 @@ export function createStore, C extend $c: $c as ComputedValues, $queue: $queue as QueuedStoreActions, $config: config, + $listeners: [ + change => { + devtools.emit(STATE_CHANGE, { + store: storeObj, + change, + }); + }, + ], $subscribe: listener => { devtools.emit(SUBSCRIBED, { store: storeObj, listener }); - proxyObj.addListener(listener); + storeObj.$listeners.push(listener); }, $unsubscribe: listener => { - devtools.emit(UNSUBSCRIBED, storeObj); - proxyObj.removeListener(listener); + devtools.emit(UNSUBSCRIBED, { store: storeObj }); + storeObj.$listeners = storeObj.$listeners.filter(item => item != listener); }, } as unknown as StoreObj; + listener.current = (...args) => { + storeObj.$listeners.forEach(listener => listener(...args)); + }; + const plannedActions: PlannedAction>[] = []; // 包装actions @@ -104,7 +215,11 @@ export function createStore, C extend }); return new Promise(resolve => { if (!proxyObj.$pending) { - proxyObj.$pending = true; + proxyObj.$pending = Date.now(); + devtools.emit(QUEUE_PENDING, { + store: storeObj, + startedAt: proxyObj.$pending, + }); const result = config.actions![action].bind(storeObj, proxyObj)(...payload); @@ -192,101 +307,9 @@ export function createStore, C extend store: storeObj, }); - proxyObj.addListener(change => { - devtools.emit(STATE_CHANGE, { - store: storeObj, - change, - }); - }); - return createGetStore(storeObj); } -// 通过该方法执行store.$queue中的action -function tryNextAction(storeObj, proxyObj, config, plannedActions) { - if (!plannedActions.length) { - proxyObj.$pending = false; - return; - } - - const nextAction = plannedActions.shift()!; - const result = config.actions - ? config.actions[nextAction.action].bind(storeObj, proxyObj)(...nextAction.payload) - : undefined; - - if (isPromise(result)) { - result.then(value => { - nextAction.resolve(value); - tryNextAction(storeObj, proxyObj, config, plannedActions); - }); - } else { - nextAction.resolve(result); - tryNextAction(storeObj, proxyObj, config, plannedActions); - } -} - -// createStore返回的是一个getStore的函数,这个函数必须要在组件(函数/类组件)里面被执行,因为要注册VNode销毁时的清理动作 -function createGetStore, C extends UserComputedValues>( - storeObj: StoreObj -): () => StoreObj { - const getStore = () => { - if (!storeObj.$config.options?.isReduxAdapter) { - registerDestroyFunction(); - } - - return storeObj; - }; - - return getStore; -} - -// 删除Observers中保存的这个VNode的相关数据 -export function clearVNodeObservers(vNode: VNode) { - if (!vNode.observers) { - return; - } - - vNode.observers.forEach(observer => { - observer.clearByVNode(vNode); - }); - - vNode.observers.clear(); -} - -// 注册VNode销毁时的清理动作 -function registerDestroyFunction() { - const processingVNode = getProcessingVNode(); - - // 获取不到当前运行的VNode,说明不在组件中运行,属于非法场景 - if (!processingVNode) { - return; - } - - if (!processingVNode.observers) { - processingVNode.observers = new Set(); - } - - // 函数组件 - if (processingVNode.tag === FunctionComponent) { - const vNodeRef = useRef(processingVNode); - - useEffect(() => { - return () => { - clearVNodeObservers(vNodeRef.current); - vNodeRef.current.observers = null; - }; - }, []); - } else if (processingVNode.tag === ClassComponent) { - // 类组件 - if (!processingVNode.classComponentWillUnmount) { - processingVNode.classComponentWillUnmount = vNode => { - clearVNodeObservers(vNode); - vNode.observers = null; - }; - } - } -} - // 函数组件中使用的hook export function useStore, C extends UserComputedValues>( id: string diff --git a/libs/horizon/src/horizonx/types.d.ts b/libs/horizon/src/horizonx/types.d.ts index 5c62a678..6c6b6cba 100644 --- a/libs/horizon/src/horizonx/types.d.ts +++ b/libs/horizon/src/horizonx/types.d.ts @@ -61,6 +61,7 @@ export type StoreObj, C extends UserC $a: StoreActions; $c: UserComputedValues; $queue: QueuedStoreActions; + $listeners; $subscribe: (listener: (mutation) => void) => void; $unsubscribe: (listener: (mutation) => void) => void; } & { [K in keyof S]: S[K] } & { [K in keyof A]: Action } & { [K in keyof C]: ReturnType }; diff --git a/libs/horizon/src/renderer/ErrorHandler.ts b/libs/horizon/src/renderer/ErrorHandler.ts index c075c05b..ce7ae550 100644 --- a/libs/horizon/src/renderer/ErrorHandler.ts +++ b/libs/horizon/src/renderer/ErrorHandler.ts @@ -72,7 +72,7 @@ function createClassErrorUpdate(vNode: VNode, error: any): Update { } return update; } -function isPromise(error: any): error is PromiseType { +export function isPromise(error: any): error is PromiseType { return error !== null && typeof error === 'object' && typeof error.then === 'function'; } // 处理capture和bubble阶段抛出的错误 diff --git a/libs/horizon/src/renderer/RootStack.ts b/libs/horizon/src/renderer/RootStack.ts index f5b88d1f..b5eadc6c 100644 --- a/libs/horizon/src/renderer/RootStack.ts +++ b/libs/horizon/src/renderer/RootStack.ts @@ -14,15 +14,21 @@ */ import { VNode } from './vnode/VNode'; -const currentRootStack: VNode[] = []; + +const currentRootStack: (VNode | undefined)[] = []; +let index = -1; export function getCurrentRoot() { - return currentRootStack[currentRootStack.length - 1]; + return currentRootStack[index]; } export function pushCurrentRoot(root: VNode) { - return currentRootStack.push(root); + index++; + currentRootStack[index] = root; } export function popCurrentRoot() { - return currentRootStack.pop(); + const target = currentRootStack[index]; + currentRootStack[index] = undefined; + index--; + return target; } diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts index 2e28c75f..1c3f4202 100644 --- a/libs/horizon/src/renderer/TreeBuilder.ts +++ b/libs/horizon/src/renderer/TreeBuilder.ts @@ -139,7 +139,7 @@ function bubbleVNode(vNode: VNode): void { node = parent; // 更新processing,抛出异常时可以使用 processing = node; - } while (node !== null); + } while (node); // 修改结果 if (getBuildResult() === BuildInComplete) { @@ -179,6 +179,11 @@ function isEqualByIndex(idx: number, pathArrays: string[][]) { function getChildByIndex(vNode: VNode, idx: number) { let node = vNode.child; for (let i = 0; i < idx; i++) { + // 场景:当组件被销毁,业务若异步(定时器)调用setState修改状态,可能出现路径错误,此处进行保护。 + if (node === null || node === undefined) { + return null; + } + node = node.next; } return node; @@ -220,7 +225,7 @@ export function calcStartUpdateVNode(treeRoot: VNode) { const pathIndex = Number(startNodePath[i]); node = getChildByIndex(node, pathIndex)!; // 路径错误时,回退到从根更新 - if (node == null) { + if (node === null) { return treeRoot; } } @@ -228,6 +233,40 @@ export function calcStartUpdateVNode(treeRoot: VNode) { return node; } +// 在局部更新时,从上到下恢复父节点的context和PortalStack +function recoverTreeContext(vNode: VNode) { + const contextProviders: VNode[] = []; + let parent = vNode.parent; + while (parent !== null) { + if (parent.tag === ContextProvider) { + contextProviders.unshift(parent); + } + if (parent.tag === DomPortal) { + pushCurrentRoot(parent); + } + parent = parent.parent; + } + + contextProviders.forEach(node => { + setContext(node, node.props.value); + }); +} + +// 在局部更新时,从下到上重置父节点的context +function resetTreeContext(vNode: VNode) { + let parent = vNode.parent; + + while (parent !== null) { + if (parent.tag === ContextProvider) { + resetContext(parent); + } + if (parent.tag === DomPortal) { + popCurrentRoot(); + } + parent = parent.parent; + } +} + // ============================== 深度遍历 ============================== function buildVNodeTree(treeRoot: VNode) { const preMode = copyExecuteMode(); @@ -299,43 +338,11 @@ function buildVNodeTree(treeRoot: VNode) { setExecuteMode(preMode); } -// 在局部更新时,从上到下恢复父节点的context和PortalStack -function recoverTreeContext(vNode: VNode) { - const contextProviders: VNode[] = []; - let parent = vNode.parent; - while (parent !== null) { - if (parent.tag === ContextProvider) { - contextProviders.unshift(parent); - } - if (parent.tag === DomPortal) { - pushCurrentRoot(parent); - } - parent = parent.parent; - } - contextProviders.forEach(node => { - setContext(node, node.props.value); - }); -} - -// 在局部更新时,从下到上重置父节点的context -function resetTreeContext(vNode: VNode) { - let parent = vNode.parent; - - while (parent !== null) { - if (parent.tag === ContextProvider) { - resetContext(parent); - } - if (parent.tag === DomPortal) { - popCurrentRoot(); - } - parent = parent.parent; - } -} - // 总体任务入口 function renderFromRoot(treeRoot) { runAsyncEffects(); pushCurrentRoot(treeRoot); + // 1. 构建vNode树 buildVNodeTree(treeRoot); @@ -346,6 +353,7 @@ function renderFromRoot(treeRoot) { // 2. 提交变更 submitToRender(treeRoot); + popCurrentRoot(); if (window.__HORIZON_DEV_HOOK__) { const hook = window.__HORIZON_DEV_HOOK__; diff --git a/libs/horizon/src/renderer/Types.ts b/libs/horizon/src/renderer/Types.ts index c514bf5c..870f129b 100644 --- a/libs/horizon/src/renderer/Types.ts +++ b/libs/horizon/src/renderer/Types.ts @@ -13,6 +13,8 @@ * See the Mulan PSL v2 for more details. */ +import { BELONG_CLASS_VNODE_KEY } from './vnode/VNode'; + export { VNode } from './vnode/VNode'; type Trigger = (A) => void; @@ -32,7 +34,7 @@ export type JSXElement = { key: any; ref: any; props: any; - belongClassVNode: any; + [BELONG_CLASS_VNODE_KEY]: any; }; export type ProviderType = { @@ -77,3 +79,5 @@ export type Source = { fileName: string; lineNumber: number; }; + +export type Callback = () => void; diff --git a/libs/horizon/src/renderer/UpdateHandler.ts b/libs/horizon/src/renderer/UpdateHandler.ts index 7f678065..7febd61e 100644 --- a/libs/horizon/src/renderer/UpdateHandler.ts +++ b/libs/horizon/src/renderer/UpdateHandler.ts @@ -13,7 +13,7 @@ * See the Mulan PSL v2 for more details. */ -import type { VNode } from './Types'; +import type { VNode, Callback } from './Types'; import { FlagUtils, ShouldCapture } from './vnode/VNodeFlags'; export type Update = { @@ -22,8 +22,6 @@ export type Update = { callback: Callback | null; }; -export type Callback = () => any; - export type Updates = Array | null; export enum UpdateState { diff --git a/libs/horizon/src/renderer/components/BaseClassComponent.ts b/libs/horizon/src/renderer/components/BaseClassComponent.ts index e9ad2fd3..34fa7dc9 100644 --- a/libs/horizon/src/renderer/components/BaseClassComponent.ts +++ b/libs/horizon/src/renderer/components/BaseClassComponent.ts @@ -13,6 +13,8 @@ * See the Mulan PSL v2 for more details. */ +import {Callback} from '../Types'; + /** * Component的api setState和forceUpdate在实例生成阶段实现 */ @@ -29,9 +31,9 @@ class Component { this.context = context; } - setState(state: S) { + setState(state: S, callback?: Callback) { if (isDev) { - console.error('Cant not call `this.setState` in the constructor of class component, it will do nothing'); + console.error('Can not call `this.setState` in the constructor of class component, it will do nothing'); } } } diff --git a/libs/horizon/src/renderer/components/ForwardRef.ts b/libs/horizon/src/renderer/components/ForwardRef.ts index 7acb1290..836735c2 100644 --- a/libs/horizon/src/renderer/components/ForwardRef.ts +++ b/libs/horizon/src/renderer/components/ForwardRef.ts @@ -13,11 +13,20 @@ * See the Mulan PSL v2 for more details. */ -import { TYPE_FORWARD_REF } from '../../external/JSXElementType'; +import {TYPE_FORWARD_REF, TYPE_MEMO} from '../../external/JSXElementType'; export function forwardRef(render: Function) { - return { + const forwardRefJSXElement = { vtype: TYPE_FORWARD_REF, + $$typeof: TYPE_FORWARD_REF, // 规避三方件hoist-non-react-statics中,通过$$typeof获取类型,但获取不到,导致render被覆盖 render, }; + + // 控制vtype不能修改,规避三方件hoist-non-react-statics修改vtype导致问题 + Object.defineProperty(forwardRefJSXElement, 'vtype', { + configurable: false, + writable: false, + }); + + return forwardRefJSXElement; } diff --git a/libs/horizon/src/renderer/components/Memo.ts b/libs/horizon/src/renderer/components/Memo.ts index 2ae1e094..6b27be77 100644 --- a/libs/horizon/src/renderer/components/Memo.ts +++ b/libs/horizon/src/renderer/components/Memo.ts @@ -16,9 +16,18 @@ import { TYPE_MEMO } from '../../external/JSXElementType'; export function memo(type, compare?: (oldProps: Props, newProps: Props) => boolean) { - return { + const memoJSXElement = { vtype: TYPE_MEMO, + $$typeof: TYPE_MEMO, // 规避三方件hoist-non-react-statics中,通过$$typeof获取类型,但获取不到,导致type被覆盖 type: type, compare: compare === undefined ? null : compare, }; + + // 控制vtype不能修改,规避三方件hoist-non-react-statics修改vtype导致问题 + Object.defineProperty(memoJSXElement, 'vtype', { + configurable: false, + writable: false, + }); + + return memoJSXElement; } diff --git a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts index 660e3ab9..e6a6f32a 100644 --- a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts +++ b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts @@ -27,6 +27,7 @@ import { import { isSameType, getIteratorFn, isTextType, isIteratorType, isObjectType } from './DiffTools'; import { travelChildren } from '../vnode/VNodeUtils'; import { markVNodePath } from '../utils/vNodePath'; +import { BELONG_CLASS_VNODE_KEY } from '../vnode/VNode'; enum DiffCategory { TEXT_NODE = 'TEXT_NODE', @@ -166,11 +167,11 @@ function getNewNode(parentNode: VNode, newChild: any, oldNode: VNode | null) { if (oldNode === null || !isSameType(oldNode, newChild)) { resultNode = createVNodeFromElement(newChild); resultNode.ref = newChild.ref; - resultNode.belongClassVNode = newChild.belongClassVNode; + resultNode[BELONG_CLASS_VNODE_KEY] = newChild[BELONG_CLASS_VNODE_KEY]; } else { resultNode = updateVNode(oldNode, newChild.props); resultNode.ref = newChild.ref; - resultNode.belongClassVNode = newChild.belongClassVNode; + resultNode[BELONG_CLASS_VNODE_KEY] = newChild[BELONG_CLASS_VNODE_KEY]; } break; } else if (newChild.vtype === TYPE_PORTAL) { @@ -181,6 +182,10 @@ function getNewNode(parentNode: VNode, newChild: any, oldNode: VNode | null) { } break; } + break; + } + default: { + break; } } @@ -231,6 +236,19 @@ function getOldNodeFromMap(nodeMap: Map, newIdx: number, return null; } +// 设置vNode中的cIndex属性,cIndex是节点在children中的位置 +function setVNodesCIndex(startChild: VNode | null, startIdx: number) { + let node: VNode | null = startChild; + let idx = startIdx; + + while (node !== null) { + node.cIndex = idx; + markVNodePath(node); + node = node.next; + idx++; + } +} + // diff数组类型的节点,核心算法 function diffArrayNodesHandler(parentNode: VNode, firstChild: VNode | null, newChildren: Array): VNode | null { let resultingFirstChild: VNode | null = null; @@ -360,7 +378,7 @@ function diffArrayNodesHandler(parentNode: VNode, firstChild: VNode | null, newC // 4. 新节点还有一部分,但是老节点已经没有了 if (oldNode === null) { let isDirectAdd = false; - // TODO: 是否可以扩大至非dom类型节点 + // 是否可以扩大至非dom类型节点待确认 // 如果dom节点在上次添加前没有节点,说明本次添加时,可以直接添加到最后,不需要通过 getSiblingDom 函数找到 before 节点 if ( parentNode.tag === DomComponent && @@ -478,19 +496,6 @@ function diffArrayNodesHandler(parentNode: VNode, firstChild: VNode | null, newC return resultingFirstChild; } -// 设置vNode中的cIndex属性,cIndex是节点在children中的位置 -function setVNodesCIndex(startChild: VNode | null, startIdx: number) { - let node: VNode | null = startChild; - let idx = startIdx; - - while (node !== null) { - node.cIndex = idx; - markVNodePath(node); - node = node.next; - idx++; - } -} - // 新节点是迭代器类型 function diffIteratorNodesHandler( parentNode: VNode, @@ -512,7 +517,7 @@ function diffIteratorNodesHandler( } // 新节点是字符串类型 -function diffStringNodeHandler(parentNode: VNode, newChild: any, firstChildVNode: VNode, isComparing: boolean) { +function diffStringNodeHandler(parentNode: VNode, newChild: any, firstChildVNode: VNode | null, isComparing: boolean) { let newTextNode: VNode | null = null; // 第一个vNode是Text,则复用 @@ -559,7 +564,7 @@ function diffObjectNodeHandler( } let resultNode: VNode | null = null; - let startDelVNode = firstChildVNode; + let startDelVNode: VNode | null = firstChildVNode; if (newChild.vtype === TYPE_COMMON_ELEMENT) { if (canReuseNode) { // 可以复用 @@ -570,7 +575,7 @@ function diffObjectNodeHandler( } else if (isSameType(canReuseNode, newChild)) { resultNode = updateVNode(canReuseNode, newChild.props); resultNode.ref = newChild.ref; - resultNode.belongClassVNode = newChild.belongClassVNode; + resultNode[BELONG_CLASS_VNODE_KEY] = newChild[BELONG_CLASS_VNODE_KEY]; startDelVNode = resultNode.next; resultNode.next = null; } @@ -583,7 +588,7 @@ function diffObjectNodeHandler( } else { resultNode = createVNodeFromElement(newChild); resultNode.ref = newChild.ref; - resultNode.belongClassVNode = newChild.belongClassVNode; + resultNode[BELONG_CLASS_VNODE_KEY] = newChild[BELONG_CLASS_VNODE_KEY]; } } } else if (newChild.vtype === TYPE_PORTAL) { diff --git a/libs/horizon/src/renderer/hooks/BaseHook.ts b/libs/horizon/src/renderer/hooks/BaseHook.ts index 5a5694c8..fe9389fa 100644 --- a/libs/horizon/src/renderer/hooks/BaseHook.ts +++ b/libs/horizon/src/renderer/hooks/BaseHook.ts @@ -34,7 +34,7 @@ export function setCurrentHook(hook: Hook | null) { currentHook = hook; } -export function throwNotInFuncError() { +export function throwNotInFuncError(): never { throw Error('Hooks should be used inside function component.'); } @@ -52,7 +52,7 @@ export function createHook(state: any = null): Hook { return currentHook; } -export function getNextHook(hook: Hook, hooks: Array>) { +export function getNextHook(hook: Hook, hooks: Array>): Hook | null { return hooks[hook.hIndex + 1] || null; } diff --git a/libs/horizon/src/renderer/hooks/HookExternal.ts b/libs/horizon/src/renderer/hooks/HookExternal.ts index 32be1591..41523b2a 100644 --- a/libs/horizon/src/renderer/hooks/HookExternal.ts +++ b/libs/horizon/src/renderer/hooks/HookExternal.ts @@ -27,22 +27,25 @@ import { getProcessingVNode } from '../GlobalVar'; import { Ref, Trigger } from './HookType'; type BasicStateAction = ((S) => S) | S; -type Dispatch = (A) => void; +type Dispatch = (value: A) => void; export function useContext(Context: ContextType): T { const processingVNode = getProcessingVNode(); return getNewContext(processingVNode!, Context, true); } - -export function useState(initialState: (() => S) | S): [S, Dispatch>] { +export function useState(): [S | undefined, Dispatch>] +export function useState(initialState: (() => S) | S): [S, Dispatch>] +export function useState(initialState?: (() => S) | S): [S, Dispatch>] { return useStateImpl(initialState); } -export function useReducer(reducer: (S, A) => S, initialArg: I, init?: (I) => S): [S, Trigger] | void { +export function useReducer(reducer: (S, A) => S, initialArg: I, init?: (I) => S): [S, Trigger] { return useReducerImpl(reducer, initialArg, init); } -export function useRef(initialValue: T): Ref { +export function useRef(): Ref +export function useRef(initialValue: T): Ref +export function useRef(initialValue?: T): Ref { return useRefImpl(initialValue); } diff --git a/libs/horizon/src/renderer/hooks/HookMain.ts b/libs/horizon/src/renderer/hooks/HookMain.ts index 1b63ed5a..27dec0a6 100644 --- a/libs/horizon/src/renderer/hooks/HookMain.ts +++ b/libs/horizon/src/renderer/hooks/HookMain.ts @@ -18,6 +18,12 @@ import type { VNode } from '../Types'; import { getLastTimeHook, setLastTimeHook, setCurrentHook, getNextHook } from './BaseHook'; import { HookStage, setHookStage } from './HookStage'; +function resetGlobalVariable() { + setHookStage(null); + setLastTimeHook(null); + setCurrentHook(null); +} + // hook对外入口 export function runFunctionWithHooks, Arg>( funcComp: (props: Props, arg: Arg) => any, @@ -57,9 +63,3 @@ export function runFunctionWithHooks, Arg>( return comp; } - -function resetGlobalVariable() { - setHookStage(null); - setLastTimeHook(null); - setCurrentHook(null); -} diff --git a/libs/horizon/src/renderer/hooks/HookType.ts b/libs/horizon/src/renderer/hooks/HookType.ts index 5c57454a..54b5158f 100644 --- a/libs/horizon/src/renderer/hooks/HookType.ts +++ b/libs/horizon/src/renderer/hooks/HookType.ts @@ -57,4 +57,4 @@ export type Ref = { current: V; }; -export type Trigger = (A) => void; +export type Trigger = (state: A) => void; diff --git a/libs/horizon/src/renderer/hooks/UseEffectHook.ts b/libs/horizon/src/renderer/hooks/UseEffectHook.ts index 18cdb5ba..67d68df4 100644 --- a/libs/horizon/src/renderer/hooks/UseEffectHook.ts +++ b/libs/horizon/src/renderer/hooks/UseEffectHook.ts @@ -21,27 +21,17 @@ import { getHookStage, HookStage } from './HookStage'; import { isArrayEqual } from '../utils/compare'; import { getProcessingVNode } from '../GlobalVar'; -export function useEffectImpl(effectFunc: () => (() => void) | void, deps?: Array | null): void { - // 异步触发的effect - useEffect(effectFunc, deps, EffectConstant.Effect); -} +function createEffect(effectFunc, removeFunc, deps, effectConstant): Effect { + const effect: Effect = { + effect: effectFunc, + removeEffect: removeFunc, + dependencies: deps, + effectConstant: effectConstant, + }; -export function useLayoutEffectImpl(effectFunc: () => (() => void) | void, deps?: Array | null): void { - // 同步触发的effect - useEffect(effectFunc, deps, EffectConstant.LayoutEffect); -} + getProcessingVNode().effectList.push(effect); -function useEffect(effectFunc: () => (() => void) | void, deps: Array | void | null, effectType: number): void { - const stage = getHookStage(); - if (stage === null) { - throwNotInFuncError(); - } - - if (stage === HookStage.Init) { - useEffectForInit(effectFunc, deps, effectType); - } else if (stage === HookStage.Update) { - useEffectForUpdate(effectFunc, deps, effectType); - } + return effect; } export function useEffectForInit(effectFunc, deps, effectType): void { @@ -76,15 +66,25 @@ export function useEffectForUpdate(effectFunc, deps, effectType): void { hook.state = createEffect(effectFunc, removeFunc, nextDeps, EffectConstant.DepsChange | effectType); } -function createEffect(effectFunc, removeFunc, deps, effectConstant): Effect { - const effect: Effect = { - effect: effectFunc, - removeEffect: removeFunc, - dependencies: deps, - effectConstant: effectConstant, - }; +function useEffect(effectFunc: () => (() => void) | void, deps: Array | void | null, effectType: number): void { + const stage = getHookStage(); + if (stage === null) { + throwNotInFuncError(); + } - getProcessingVNode().effectList.push(effect); - - return effect; + if (stage === HookStage.Init) { + useEffectForInit(effectFunc, deps, effectType); + } else if (stage === HookStage.Update) { + useEffectForUpdate(effectFunc, deps, effectType); + } +} + +export function useEffectImpl(effectFunc: () => (() => void) | void, deps?: Array | null): void { + // 异步触发的effect + useEffect(effectFunc, deps, EffectConstant.Effect); +} + +export function useLayoutEffectImpl(effectFunc: () => (() => void) | void, deps?: Array | null): void { + // 同步触发的effect + useEffect(effectFunc, deps, EffectConstant.LayoutEffect); } diff --git a/libs/horizon/src/renderer/hooks/UseImperativeHook.ts b/libs/horizon/src/renderer/hooks/UseImperativeHook.ts index 83f1a74d..1871af8f 100644 --- a/libs/horizon/src/renderer/hooks/UseImperativeHook.ts +++ b/libs/horizon/src/renderer/hooks/UseImperativeHook.ts @@ -17,26 +17,9 @@ import { useLayoutEffectImpl } from './UseEffectHook'; import { getHookStage } from './HookStage'; import { throwNotInFuncError } from './BaseHook'; import type { Ref } from './HookType'; +import { isNotNull } from '../../dom/utils/Common'; -export function useImperativeHandleImpl( - ref: { current: R | null } | ((any) => any) | null | void, - func: () => R, - dependencies?: Array | null -): void { - const stage = getHookStage(); - if (stage === null) { - throwNotInFuncError(); - } - - const params = isNotNull(dependencies) ? dependencies.concat([ref]) : null; - useLayoutEffectImpl(effectFunc.bind(null, func, ref), params); -} - -function isNotNull(object: any): boolean { - return object !== null && object !== undefined; -} - -function effectFunc(func: () => R, ref: Ref | ((any) => any) | null): (() => void) | void { +function effectFunc(func: () => R, ref: Ref | ((any) => any) | null): (() => void) | null { if (typeof ref === 'function') { const value = func(); ref(value); @@ -51,4 +34,19 @@ function effectFunc(func: () => R, ref: Ref | ((any) => any) | null): (() ref.current = null; }; } + return null; +} + +export function useImperativeHandleImpl( + ref: { current: R | null } | ((any) => any) | null | void, + func: () => R, + dependencies?: Array | null +): void { + const stage = getHookStage(); + if (stage === null) { + throwNotInFuncError(); + } + + const params = isNotNull(dependencies) ? dependencies.concat([ref]) : null; + useLayoutEffectImpl(effectFunc.bind(null, func, ref), params); } diff --git a/libs/horizon/src/renderer/hooks/UseReducerHook.ts b/libs/horizon/src/renderer/hooks/UseReducerHook.ts index edc4673e..ec348396 100644 --- a/libs/horizon/src/renderer/hooks/UseReducerHook.ts +++ b/libs/horizon/src/renderer/hooks/UseReducerHook.ts @@ -22,29 +22,6 @@ import { getHookStage, HookStage } from './HookStage'; import type { VNode } from '../Types'; import { getProcessingVNode } from '../GlobalVar'; -export function useReducerImpl( - reducer: (S, A) => S, - initArg: P, - init?: (P) => S, - isUseState?: boolean -): [S, Trigger] | void { - const stage = getHookStage(); - if (stage === null) { - throwNotInFuncError(); - } - - if (stage === HookStage.Init) { - return useReducerForInit(reducer, initArg, init, isUseState); - } else if (stage === HookStage.Update) { - // 获取当前的hook - const currentHook = getCurrentHook(); - // 获取currentHook的更新数组 - const currentHookUpdates = (currentHook.state as Reducer).updates; - - return updateReducerHookState(currentHookUpdates, currentHook, reducer); - } -} - // 构造新的Update数组 function insertUpdate(action: A, hook: Hook): Update { const newUpdate: Update = { @@ -116,6 +93,25 @@ export function useReducerForInit(reducer, initArg, init, isUseState?: boo return [hook.state.stateValue, trigger]; } +// 计算stateValue值 +function calculateNewState(currentHookUpdates: Array>, currentHook, reducer: (S, A) => S) { + const reducerObj = currentHook.state; + let state = reducerObj.stateValue; + + // 循环遍历更新数组,计算新的状态值 + currentHookUpdates.forEach(update => { + // 1. didCalculated = true 说明state已经计算过; 2. 如果来自 isUseState + if (update.didCalculated && reducerObj.isUseState) { + state = update.state; + } else { + const action = update.action; + state = reducer(state, action); + } + }); + + return state; +} + // 更新hook.state function updateReducerHookState(currentHookUpdates, currentHook, reducer): [S, Trigger] { if (currentHookUpdates !== null) { @@ -135,21 +131,25 @@ function updateReducerHookState(currentHookUpdates, currentHook, reducer): return [currentHook.state.stateValue, currentHook.state.trigger]; } -// 计算stateValue值 -function calculateNewState(currentHookUpdates: Array>, currentHook, reducer: (S, A) => S) { - const reducerObj = currentHook.state; - let state = reducerObj.stateValue; +export function useReducerImpl( + reducer: (S, A) => S, + initArg: P, + init?: (P) => S, + isUseState?: boolean +): [S, Trigger] | void { + const stage = getHookStage(); + if (stage === null) { + throwNotInFuncError(); + } - // 循环遍历更新数组,计算新的状态值 - currentHookUpdates.forEach(update => { - // 1. didCalculated = true 说明state已经计算过; 2. 如果来自 isUseState - if (update.didCalculated && reducerObj.isUseState) { - state = update.state; - } else { - const action = update.action; - state = reducer(state, action); - } - }); + if (stage === HookStage.Init) { + return useReducerForInit(reducer, initArg, init, isUseState); + } else if (stage === HookStage.Update) { + // 获取当前的hook + const currentHook = getCurrentHook(); + // 获取currentHook的更新数组 + const currentHookUpdates = (currentHook.state as Reducer).updates; - return state; + return updateReducerHookState(currentHookUpdates, currentHook, reducer); + } } diff --git a/libs/horizon/src/renderer/hooks/UseRefHook.ts b/libs/horizon/src/renderer/hooks/UseRefHook.ts index 3f5b0c37..6b154750 100644 --- a/libs/horizon/src/renderer/hooks/UseRefHook.ts +++ b/libs/horizon/src/renderer/hooks/UseRefHook.ts @@ -17,7 +17,7 @@ import { createHook, getCurrentHook, throwNotInFuncError } from './BaseHook'; import { getHookStage, HookStage } from './HookStage'; import type { Ref } from './HookType'; -export function useRefImpl(value: V): Ref { +export function useRefImpl(value?: V): Ref { const stage = getHookStage(); if (stage === null) { throwNotInFuncError(); diff --git a/libs/horizon/src/renderer/hooks/UseStateHook.ts b/libs/horizon/src/renderer/hooks/UseStateHook.ts index 84ef4ca2..b20e9f6a 100644 --- a/libs/horizon/src/renderer/hooks/UseStateHook.ts +++ b/libs/horizon/src/renderer/hooks/UseStateHook.ts @@ -21,6 +21,6 @@ function defaultReducer(state: S, action: ((S) => S) | S): S { return typeof action === 'function' ? action(state) : action; } -export function useStateImpl(initArg: (() => S) | S): [S, Trigger<((S) => S) | S>] { +export function useStateImpl(initArg?: (() => S) | S): [S, Trigger<((S) => S) | S>] { return useReducerImpl(defaultReducer, initArg, undefined, true); } diff --git a/libs/horizon/src/renderer/render/ContextProvider.ts b/libs/horizon/src/renderer/render/ContextProvider.ts index 39c91ba5..9734eb45 100644 --- a/libs/horizon/src/renderer/render/ContextProvider.ts +++ b/libs/horizon/src/renderer/render/ContextProvider.ts @@ -64,7 +64,7 @@ function handleContextChange(processing: VNode, context: ContextType): void node => { const depContexts = node.depContexts; if (depContexts && depContexts.length) { - isMatch = matchDependencies(depContexts, context, node) ?? isMatch; + isMatch = matchDependencies(depContexts, context, node) || isMatch; } }, node => diff --git a/libs/horizon/src/renderer/render/DomComponent.ts b/libs/horizon/src/renderer/render/DomComponent.ts index f7517fb8..c35305cf 100644 --- a/libs/horizon/src/renderer/render/DomComponent.ts +++ b/libs/horizon/src/renderer/render/DomComponent.ts @@ -54,7 +54,7 @@ export function bubbleRender(processing: VNode) { const type = processing.type; const newProps = processing.props; - if (!processing.isCreated && processing.realNode != null) { + if (!processing.isCreated && processing.realNode !== null) { // 更新dom属性 updateDom(processing, type, newProps); diff --git a/libs/horizon/src/renderer/render/DomPortal.ts b/libs/horizon/src/renderer/render/DomPortal.ts index ceeb23c4..242e9f8f 100644 --- a/libs/horizon/src/renderer/render/DomPortal.ts +++ b/libs/horizon/src/renderer/render/DomPortal.ts @@ -17,9 +17,11 @@ import type { VNode } from '../Types'; import { resetNamespaceCtx, setNamespaceCtx } from '../ContextSaver'; import { createChildrenByDiff } from '../diff/nodeDiffComparator'; import { popCurrentRoot, pushCurrentRoot } from '../RootStack'; +import { listenSimulatedDelegatedEvents } from '../../event/EventBinding'; export function bubbleRender(processing: VNode) { resetNamespaceCtx(processing); + listenSimulatedDelegatedEvents(processing); popCurrentRoot(); } diff --git a/libs/horizon/src/renderer/render/DomText.ts b/libs/horizon/src/renderer/render/DomText.ts index 1acd5003..213f9f8b 100644 --- a/libs/horizon/src/renderer/render/DomText.ts +++ b/libs/horizon/src/renderer/render/DomText.ts @@ -18,6 +18,7 @@ import type { VNode } from '../Types'; import { throwIfTrue } from '../utils/throwIfTrue'; import { newTextDom } from '../../dom/DOMOperator'; import { FlagUtils } from '../vnode/VNodeFlags'; +import { isNull } from '../../dom/utils/Common'; export function captureRender(): VNode | null { return null; @@ -26,7 +27,7 @@ export function captureRender(): VNode | null { export function bubbleRender(processing: VNode) { const newText = processing.props; - if (!processing.isCreated && processing.realNode != null) { + if (!processing.isCreated && processing.realNode !== null) { // 更新 const oldText = processing.oldProps; // 如果文本不同,将其标记为更新 diff --git a/libs/horizon/src/renderer/render/MemoComponent.ts b/libs/horizon/src/renderer/render/MemoComponent.ts index 1703a7d9..95ebb64c 100644 --- a/libs/horizon/src/renderer/render/MemoComponent.ts +++ b/libs/horizon/src/renderer/render/MemoComponent.ts @@ -31,7 +31,9 @@ export function bubbleRender() {} export function captureMemoComponent(processing: VNode, shouldUpdate: boolean): VNode | null { const Component = processing.type; // 合并 函数组件或类组件 的defaultProps - const newProps = mergeDefaultProps(Component, processing.props); + let newProps = mergeDefaultProps(Component, processing.props); + // 解决Horizon.memo(Horizon.forwardRef(()=>{}))两层包装的场景 + newProps = mergeDefaultProps(Component.type, newProps); if (processing.isCreated) { let newChild: VNode | null = null; diff --git a/libs/horizon/src/renderer/render/SuspenseComponent.ts b/libs/horizon/src/renderer/render/SuspenseComponent.ts index 38da6211..65708e49 100644 --- a/libs/horizon/src/renderer/render/SuspenseComponent.ts +++ b/libs/horizon/src/renderer/render/SuspenseComponent.ts @@ -165,7 +165,7 @@ function canCapturePromise(vNode: VNode | null): boolean { // 处理Suspense子组件抛出的promise export function handleSuspenseChildThrowError(parent: VNode, processing: VNode, promise: PromiseType): boolean { - let vNode = parent; + let vNode: VNode | null = parent; // 向上找到最近的不在fallback状态的Suspense,并触发重新渲染 do { diff --git a/libs/horizon/src/renderer/render/class/ClassLifeCycleProcessor.ts b/libs/horizon/src/renderer/render/class/ClassLifeCycleProcessor.ts index efc1444a..553e1be1 100644 --- a/libs/horizon/src/renderer/render/class/ClassLifeCycleProcessor.ts +++ b/libs/horizon/src/renderer/render/class/ClassLifeCycleProcessor.ts @@ -35,7 +35,7 @@ export function callDerivedStateFromProps( const newState = getDerivedStateFromProps(nextProps, oldState); // 组件未返回state,需要返回旧的preState - processing.state = newState === null || newState === undefined ? oldState : { ...oldState, ...newState }; + processing.state = newState ? { ...oldState, ...newState } : oldState; } } diff --git a/libs/horizon/src/renderer/submit/HookEffectHandler.ts b/libs/horizon/src/renderer/submit/HookEffectHandler.ts index daa40c44..604ec043 100644 --- a/libs/horizon/src/renderer/submit/HookEffectHandler.ts +++ b/libs/horizon/src/renderer/submit/HookEffectHandler.ts @@ -25,6 +25,11 @@ import { EffectConstant } from '../hooks/EffectConstant'; let hookEffects: Array = []; let hookRemoveEffects: Array = []; + +export function hasAsyncEffects() { + return hookEffects.length > 0 || hookRemoveEffects.length > 0; +} + // 是否正在异步调度effects let isScheduling = false; @@ -35,28 +40,6 @@ export function isSchedulingEffects() { return isScheduling; } -export function callUseEffects(vNode: VNode) { - const effectList: EffectList = vNode.effectList; - if (effectList !== null) { - effectList.forEach(effect => { - const { effectConstant } = effect; - if ( - (effectConstant & EffectConstant.Effect) !== EffectConstant.NoEffect && - (effectConstant & EffectConstant.DepsChange) !== EffectConstant.NoEffect - ) { - hookEffects.push(effect); - hookRemoveEffects.push(effect); - - // 异步调用 - if (!isScheduling) { - isScheduling = true; - runAsync(runAsyncEffects); - } - } - }); - } -} - export function runAsyncEffects() { const preMode = copyExecuteMode(); changeMode(InRender, true); @@ -93,6 +76,28 @@ export function runAsyncEffects() { setExecuteMode(preMode); } +export function callUseEffects(vNode: VNode) { + const effectList: EffectList = vNode.effectList; + if (effectList !== null) { + effectList.forEach(effect => { + const { effectConstant } = effect; + if ( + (effectConstant & EffectConstant.Effect) !== EffectConstant.NoEffect && + (effectConstant & EffectConstant.DepsChange) !== EffectConstant.NoEffect + ) { + hookEffects.push(effect); + hookRemoveEffects.push(effect); + + // 异步调用 + if (!isScheduling) { + isScheduling = true; + runAsync(runAsyncEffects); + } + } + }); + } +} + // 在销毁vNode的时候调用remove export function callEffectRemove(vNode: VNode) { const effectList: EffectList = vNode.effectList; diff --git a/libs/horizon/src/renderer/submit/LifeCycleHandler.ts b/libs/horizon/src/renderer/submit/LifeCycleHandler.ts index d936bd15..d405b73e 100644 --- a/libs/horizon/src/renderer/submit/LifeCycleHandler.ts +++ b/libs/horizon/src/renderer/submit/LifeCycleHandler.ts @@ -52,6 +52,7 @@ import { import { handleSubmitError } from '../ErrorHandler'; import { travelVNodeTree, clearVNode, isDomVNode, getSiblingDom } from '../vnode/VNodeUtils'; import { shouldAutoFocus } from '../../dom/utils/Common'; +import { BELONG_CLASS_VNODE_KEY } from '../vnode/VNode'; function callComponentWillUnmount(vNode: VNode, instance: any) { try { @@ -154,6 +155,22 @@ function hideOrUnhideAllChildren(vNode, isHidden) { ); } +function handleRef(vNode: VNode, ref, val) { + if (ref !== null && ref !== undefined) { + const refType = typeof ref; + + if (refType === 'function') { + ref(val); + } else if (refType === 'object') { + (ref).current = val; + } else { + if (vNode[BELONG_CLASS_VNODE_KEY] && vNode[BELONG_CLASS_VNODE_KEY].realNode) { + vNode[BELONG_CLASS_VNODE_KEY].realNode.refs[String(ref)] = val; + } + } + } +} + function attachRef(vNode: VNode) { const ref = vNode.ref; @@ -166,60 +183,6 @@ function detachRef(vNode: VNode, isOldRef?: boolean) { handleRef(vNode, ref, null); } -function handleRef(vNode: VNode, ref, val) { - if (ref !== null && ref !== undefined) { - const refType = typeof ref; - - if (refType === 'function') { - ref(val); - } else if (refType === 'object') { - (ref).current = val; - } else { - if (vNode.belongClassVNode && vNode.belongClassVNode.realNode) { - vNode.belongClassVNode.realNode.refs[String(ref)] = val; - } - } - } -} - -// 卸载一个vNode,不会递归 -function unmountVNode(vNode: VNode): void { - switch (vNode.tag) { - case FunctionComponent: - case ForwardRef: - case MemoComponent: { - callEffectRemove(vNode); - break; - } - case ClassComponent: { - detachRef(vNode); - - const instance = vNode.realNode; - // 当constructor中抛出异常时,instance会是null,这里判断一下instance是否为空 - // suspense打断时不需要触发WillUnmount - if (instance && typeof instance.componentWillUnmount === 'function' && !vNode.isSuspended) { - callComponentWillUnmount(vNode, instance); - } - - // HorizonX会在classComponentWillUnmount中清除对VNode的引入用 - if (vNode.classComponentWillUnmount) { - vNode.classComponentWillUnmount(vNode); - vNode.classComponentWillUnmount = null; - } - break; - } - case DomComponent: { - detachRef(vNode); - break; - } - case DomPortal: { - // 这里会递归 - unmountDomComponents(vNode); - break; - } - } -} - // 卸载vNode,递归遍历子vNode function unmountNestedVNodes(vNode: VNode): void { travelVNodeTree( @@ -235,59 +198,6 @@ function unmountNestedVNodes(vNode: VNode): void { ); } -function submitAddition(vNode: VNode): void { - let parent = vNode.parent; - let parentDom; - let tag; - while (parent !== null) { - tag = parent.tag; - if (tag === DomComponent || tag === TreeRoot || tag === DomPortal) { - parentDom = parent.realNode; - break; - } - parent = parent.parent; - } - - if ((parent.flags & ResetText) === ResetText) { - // 在insert之前先reset - clearText(parentDom); - FlagUtils.removeFlag(parent, ResetText); - } - - if ((vNode.flags & DirectAddition) === DirectAddition) { - insertOrAppendPlacementNode(vNode, null, parentDom); - FlagUtils.removeFlag(vNode, DirectAddition); - return; - } - const before = getSiblingDom(vNode); - insertOrAppendPlacementNode(vNode, before, parentDom); -} - -function insertOrAppendPlacementNode(node: VNode, beforeDom: Element | null, parent: Element | Container): void { - const { tag, realNode } = node; - - if (isDomVNode(node)) { - insertDom(parent, realNode, beforeDom); - } else if (tag === DomPortal) { - // 这里不做处理,直接在portal中处理 - } else { - // 插入子节点们 - let child = node.child; - while (child !== null) { - insertOrAppendPlacementNode(child, beforeDom, parent); - child = child.next; - } - } -} - -function insertDom(parent, realNode, beforeDom) { - if (beforeDom) { - insertDomBefore(parent, realNode, beforeDom); - } else { - appendChildElement(parent, realNode); - } -} - // 遍历所有子节点:删除dom节点,detach ref 和 调用componentWillUnmount() function unmountDomComponents(vNode: VNode): void { let currentParentIsValid = false; @@ -339,6 +249,100 @@ function unmountDomComponents(vNode: VNode): void { ); } +// 卸载一个vNode,不会递归 +function unmountVNode(vNode: VNode): void { + switch (vNode.tag) { + case FunctionComponent: + case ForwardRef: + case MemoComponent: { + callEffectRemove(vNode); + break; + } + case ClassComponent: { + detachRef(vNode); + + const instance = vNode.realNode; + // 当constructor中抛出异常时,instance会是null,这里判断一下instance是否为空 + // suspense打断时不需要触发WillUnmount + if (instance && typeof instance.componentWillUnmount === 'function' && !vNode.isSuspended) { + callComponentWillUnmount(vNode, instance); + } + + // HorizonX会在classComponentWillUnmount中清除对VNode的引入用 + if (vNode.classComponentWillUnmount) { + vNode.classComponentWillUnmount(vNode); + vNode.classComponentWillUnmount = null; + } + break; + } + case DomComponent: { + detachRef(vNode); + break; + } + case DomPortal: { + // 这里会递归 + unmountDomComponents(vNode); + break; + } + default: { + break; + } + } +} + +function insertDom(parent, realNode, beforeDom) { + if (beforeDom) { + insertDomBefore(parent, realNode, beforeDom); + } else { + appendChildElement(parent, realNode); + } +} + +function insertOrAppendPlacementNode(node: VNode, beforeDom: Element | null, parent: Element | Container): void { + const { tag, realNode } = node; + + if (isDomVNode(node)) { + insertDom(parent, realNode, beforeDom); + } else if (tag === DomPortal) { + // 这里不做处理,直接在portal中处理 + } else { + // 插入子节点们 + let child = node.child; + while (child !== null) { + insertOrAppendPlacementNode(child, beforeDom, parent); + child = child.next; + } + } +} + +function submitAddition(vNode: VNode): void { + let parent = vNode.parent; + let parentDom; + let tag; + while (parent !== null) { + tag = parent.tag; + if (tag === DomComponent || tag === TreeRoot || tag === DomPortal) { + parentDom = parent.realNode; + break; + } + parent = parent.parent; + } + + if ((parent.flags & ResetText) === ResetText) { + // 在insert之前先reset + clearText(parentDom); + FlagUtils.removeFlag(parent, ResetText); + } + + if ((vNode.flags & DirectAddition) === DirectAddition) { + insertOrAppendPlacementNode(vNode, null, parentDom); + FlagUtils.removeFlag(vNode, DirectAddition); + return; + } + const before = getSiblingDom(vNode); + insertOrAppendPlacementNode(vNode, before, parentDom); +} + function submitClear(vNode: VNode): void { const realNode = vNode.realNode; const cloneDom = realNode.cloneNode(false); // 复制节点后horizon添加给dom的属性未能复制 @@ -394,6 +398,13 @@ function submitDeletion(vNode: VNode): void { clearVNode(vNode); } +function submitSuspenseComponent(vNode: VNode) { + const { childStatus } = vNode.suspenseState; + if (childStatus !== SuspenseChildStatus.Init) { + hideOrUnhideAllChildren(vNode.child, childStatus === SuspenseChildStatus.ShowFallback); + } +} + function submitUpdate(vNode: VNode): void { switch (vNode.tag) { case FunctionComponent: @@ -413,13 +424,9 @@ function submitUpdate(vNode: VNode): void { listenToPromise(vNode); break; } - } -} - -function submitSuspenseComponent(vNode: VNode) { - const { childStatus } = vNode.suspenseState; - if (childStatus !== SuspenseChildStatus.Init) { - hideOrUnhideAllChildren(vNode.child, childStatus === SuspenseChildStatus.ShowFallback); + default: { + break; + } } } diff --git a/libs/horizon/src/renderer/submit/Submit.ts b/libs/horizon/src/renderer/submit/Submit.ts index ab3f4494..784e3aa9 100644 --- a/libs/horizon/src/renderer/submit/Submit.ts +++ b/libs/horizon/src/renderer/submit/Submit.ts @@ -41,63 +41,6 @@ const LOOPING_UPDATE_LIMIT = 50; let loopingUpdateCount = 0; let lastRoot: VNode | null = null; -export function submitToRender(treeRoot) { - treeRoot.shouldUpdate = treeRoot.childShouldUpdate; - // 置空task,让才能加入新的render任务 - treeRoot.task = null; - - const startVNode = getStartVNode(); - - if (FlagUtils.hasAnyFlag(startVNode)) { - // 把自己加上 - if (startVNode.dirtyNodes === null) { - startVNode.dirtyNodes = [startVNode]; - } else { - startVNode.dirtyNodes.push(startVNode); - } - } - - const dirtyNodes = startVNode.dirtyNodes; - if (dirtyNodes !== null && dirtyNodes.length) { - const preMode = copyExecuteMode(); - changeMode(InRender, true); - - prepareForSubmit(); - // before submit阶段 - beforeSubmit(dirtyNodes); - - // submit阶段 - submit(dirtyNodes); - - resetAfterSubmit(); - - // after submit阶段 - afterSubmit(dirtyNodes); - - setExecuteMode(preMode); - dirtyNodes.length = 0; - startVNode.dirtyNodes = null; - } - - if (isSchedulingEffects()) { - setSchedulingEffects(false); - } - - // 统计root同步重渲染的次数,如果太多可能是无线循环 - countLoopingUpdate(treeRoot); - - // 在退出`submit` 之前始终调用此函数,以确保任何已计划在此根上执行的update被执行。 - tryRenderFromRoot(treeRoot); - - if (rootThrowError) { - const error = rootThrowError; - rootThrowError = null; - throw error; - } - - return null; -} - function beforeSubmit(dirtyNodes: Array) { let node; const nodesLength = dirtyNodes.length; @@ -214,3 +157,60 @@ export function checkLoopingUpdateLimit() { ); } } + +export function submitToRender(treeRoot) { + treeRoot.shouldUpdate = treeRoot.childShouldUpdate; + // 置空task,让才能加入新的render任务 + treeRoot.task = null; + + const startVNode = getStartVNode(); + + if (FlagUtils.hasAnyFlag(startVNode)) { + // 把自己加上 + if (startVNode.dirtyNodes === null) { + startVNode.dirtyNodes = [startVNode]; + } else { + startVNode.dirtyNodes.push(startVNode); + } + } + + const dirtyNodes = startVNode.dirtyNodes; + if (dirtyNodes !== null && dirtyNodes.length) { + const preMode = copyExecuteMode(); + changeMode(InRender, true); + + prepareForSubmit(); + // before submit阶段 + beforeSubmit(dirtyNodes); + + // submit阶段 + submit(dirtyNodes); + + resetAfterSubmit(); + + // after submit阶段 + afterSubmit(dirtyNodes); + + setExecuteMode(preMode); + dirtyNodes.length = 0; + startVNode.dirtyNodes = null; + } + + if (isSchedulingEffects()) { + setSchedulingEffects(false); + } + + // 统计root同步重渲染的次数,如果太多可能是无线循环 + countLoopingUpdate(treeRoot); + + // 在退出`submit` 之前始终调用此函数,以确保任何已计划在此根上执行的update被执行。 + tryRenderFromRoot(treeRoot); + + if (rootThrowError) { + const error = rootThrowError; + rootThrowError = null; + throw error; + } + + return null; +} diff --git a/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts b/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts index fb192127..3764e5fc 100644 --- a/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts +++ b/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts @@ -19,12 +19,22 @@ let isMessageLoopRunning = false; let browserCallback = null; -const { port1, port2 } = new MessageChannel(); +let port1 = null; +let port2 = null; +let isTestRuntime = false; export function isOverTime() { return false; } +function asyncCall() { + if (isTestRuntime) { + setTimeout(callRenderTasks, 0); + } else { + port2.postMessage(null); + } +} + // 1、设置deadline;2、回调TaskExecutor传过来的browserCallback const callRenderTasks = () => { if (browserCallback === null) { @@ -41,21 +51,30 @@ const callRenderTasks = () => { browserCallback = null; } else { // 还有task,继续调用 - port2.postMessage(null); + asyncCall(); } } catch (error) { - port2.postMessage(null); + asyncCall(); throw error; } }; -port1.onmessage = callRenderTasks; +if (typeof MessageChannel === 'function') { + const mc = new MessageChannel(); + port1 = mc.port1; + port1.onmessage = callRenderTasks; + port2 = mc.port2; +} else { + // 测试环境没有 MessageChannel + isTestRuntime = true; +} export function requestBrowserCallback(callback) { browserCallback = callback; if (!isMessageLoopRunning) { isMessageLoopRunning = true; - port2.postMessage(null); + asyncCall(); } } + diff --git a/libs/horizon/src/renderer/taskExecutor/RenderQueue.ts b/libs/horizon/src/renderer/taskExecutor/RenderQueue.ts index b76f03b7..4f03cbd1 100644 --- a/libs/horizon/src/renderer/taskExecutor/RenderQueue.ts +++ b/libs/horizon/src/renderer/taskExecutor/RenderQueue.ts @@ -27,16 +27,6 @@ let callingQueueTask: any | null = null; // 防止重入 let isCallingRenderQueue = false; -export function callRenderQueueImmediate() { - if (callingQueueTask !== null) { - // 取消异步调度 - cancelTask(callingQueueTask); - callingQueueTask = null; - } - - callRenderQueue(); -} - // 执行render回调 function callRenderQueue() { if (!isCallingRenderQueue && renderQueue !== null) { @@ -45,7 +35,7 @@ function callRenderQueue() { try { let callback; - while ((callback = renderQueue.shift())) { + while (callback = renderQueue.shift()) { callback(); } @@ -58,6 +48,16 @@ function callRenderQueue() { } } +export function callRenderQueueImmediate() { + if (callingQueueTask !== null) { + // 取消异步调度 + cancelTask(callingQueueTask); + callingQueueTask = null; + } + + callRenderQueue(); +} + export function pushRenderCallback(callback: RenderCallback) { if (renderQueue === null) { renderQueue = [callback]; diff --git a/libs/horizon/src/renderer/taskExecutor/TaskQueue.ts b/libs/horizon/src/renderer/taskExecutor/TaskQueue.ts index 4705657a..1d564405 100644 --- a/libs/horizon/src/renderer/taskExecutor/TaskQueue.ts +++ b/libs/horizon/src/renderer/taskExecutor/TaskQueue.ts @@ -63,12 +63,12 @@ export function add(node: Node): void { export function first(): Node | null { const val: Node | null | undefined = taskQueue[0]; - return val !== undefined ? val : null; + return val ?? null; } export function shift(): Node | null { const val = taskQueue.shift(); - return val !== undefined ? val : null; + return val ?? null; } export function remove(node: Node) { diff --git a/libs/horizon/src/renderer/vnode/VNode.ts b/libs/horizon/src/renderer/vnode/VNode.ts index 8076e8f6..b383e62a 100644 --- a/libs/horizon/src/renderer/vnode/VNode.ts +++ b/libs/horizon/src/renderer/vnode/VNode.ts @@ -38,6 +38,8 @@ import type { Hook } from '../hooks/HookType'; import { InitFlag } from './VNodeFlags'; import { Observer } from '../../horizonx/proxy/Observer'; +export const BELONG_CLASS_VNODE_KEY = typeof Symbol === 'function' ? Symbol('belongClassVNode') : 'belongClassVNode'; + export class VNode { tag: VNodeTag; key: string | null; // 唯一标识符 @@ -89,7 +91,7 @@ export class VNode { oldChild: VNode | null = null; promiseResolve: boolean; // suspense的promise是否resolve devProps: any; // 用于dev插件临时保存更新props值 - suspenseState: SuspenseState; + suspenseState: SuspenseState | null; path = ''; // 保存从根到本节点的路径 @@ -97,7 +99,7 @@ export class VNode { toUpdateNodes: Set | null; // 保存要更新的节点 delegatedEvents: Set; - belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用 + [BELONG_CLASS_VNODE_KEY]: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用 // 状态管理器HorizonX使用 isStoreChange: boolean; @@ -200,6 +202,8 @@ export class VNode { break; case Profiler: break; + default: + break; } } } diff --git a/libs/horizon/src/renderer/vnode/VNodeCreator.ts b/libs/horizon/src/renderer/vnode/VNodeCreator.ts index 62a4e472..5b3db8ae 100644 --- a/libs/horizon/src/renderer/vnode/VNodeCreator.ts +++ b/libs/horizon/src/renderer/vnode/VNodeCreator.ts @@ -136,7 +136,7 @@ export function createUndeterminedVNode(type, key, props, source: Source | null) vNodeTag = typeMap[type.vtype]; isLazy = type.vtype === TYPE_LAZY; } else { - throw Error(`Component type is invalid, got: ${type == null ? type : componentType}`); + throw Error(`Component type is invalid, got: ${type === null || type === undefined ? type : componentType}`); } const vNode = newVirtualNode(vNodeTag, key, props); @@ -183,7 +183,7 @@ export function createTreeRootVNode(container) { return vNode; } -// TODO: 暂时保留给测试用例使用,后续修改测试用例 +// 暂时保留给测试用例使用,后续修改测试用例 export function createVNode(tag: VNodeTag | string, ...secondArg) { let vNode = null; switch (tag) { @@ -194,6 +194,8 @@ export function createVNode(tag: VNodeTag | string, ...secondArg) { vNode.updates = []; break; + default: + break; } return vNode; diff --git a/libs/horizon/src/renderer/vnode/VNodeFlags.ts b/libs/horizon/src/renderer/vnode/VNodeFlags.ts index faf567cf..0380b48c 100644 --- a/libs/horizon/src/renderer/vnode/VNodeFlags.ts +++ b/libs/horizon/src/renderer/vnode/VNodeFlags.ts @@ -36,64 +36,71 @@ export const ForceUpdate = /** */ 1 << 12; // For suspense export const Clear = /** */ 1 << 13; const LifecycleEffectArr = Update | Callback | Ref | Snapshot; -export class FlagUtils { - static removeFlag(node: VNode, flag: number) { +export const FlagUtils = { + removeFlag(node: VNode, flag: number) { node.flags &= ~flag; - } - static removeLifecycleEffectFlags(node) { + }, + + removeLifecycleEffectFlags(node) { node.flags &= ~LifecycleEffectArr; - } - static hasAnyFlag(node: VNode) { + }, + + hasAnyFlag(node: VNode) { // 有标志位 return node.flags !== InitFlag; - } + }, - static setNoFlags(node: VNode) { + hasFlag(node: VNode, flag) { + return (node.flags & flag) !== 0; + }, + + setNoFlags(node: VNode) { node.flags = InitFlag; - } + }, - static markAddition(node: VNode) { + markAddition(node: VNode) { node.flags |= Addition; - } - static setAddition(node: VNode) { + }, + + setAddition(node: VNode) { node.flags = Addition; - } + }, - static markDirectAddition(node: VNode) { + markDirectAddition(node: VNode) { node.flags |= DirectAddition; - } - static markUpdate(node: VNode) { + }, + markUpdate(node: VNode) { node.flags |= Update; - } - static setDeletion(node: VNode) { + }, + setDeletion(node: VNode) { node.flags = Deletion; - } - static markContentReset(node: VNode) { + }, + markContentReset(node: VNode) { node.flags |= ResetText; - } - static markCallback(node: VNode) { + }, + markCallback(node: VNode) { node.flags |= Callback; - } - static markDidCapture(node: VNode) { + }, + markDidCapture(node: VNode) { node.flags |= DidCapture; - } - static markShouldCapture(node: VNode) { + }, + markShouldCapture(node: VNode) { node.flags |= ShouldCapture; - } - static markRef(node: VNode) { + }, + markRef(node: VNode) { node.flags |= Ref; - } - static markSnapshot(node: VNode) { + }, + markSnapshot(node: VNode) { node.flags |= Snapshot; - } - static markInterrupted(node: VNode) { + }, + markInterrupted(node: VNode) { node.flags |= Interrupted; - } - static markForceUpdate(node: VNode) { + }, + markForceUpdate(node: VNode) { node.flags |= ForceUpdate; - } + }, - static markClear(node: VNode) { + markClear(node: VNode) { node.flags |= Clear; } } diff --git a/libs/horizon/src/renderer/vnode/VNodeUtils.ts b/libs/horizon/src/renderer/vnode/VNodeUtils.ts index 2f8445a0..952b9541 100644 --- a/libs/horizon/src/renderer/vnode/VNodeUtils.ts +++ b/libs/horizon/src/renderer/vnode/VNodeUtils.ts @@ -17,15 +17,19 @@ * 提供:vNode的“遍历”,“查找”,“判断”的相关工具方法 */ -import type { VNode } from '../Types'; +import type {VNode} from '../Types'; -import { DomComponent, DomPortal, DomText, TreeRoot } from './VNodeTags'; -import { isComment } from '../../dom/utils/Common'; -import { getNearestVNode } from '../../dom/DOMInternalKeys'; -import { Addition, InitFlag } from './VNodeFlags'; +import {DomComponent, DomPortal, DomText, TreeRoot} from './VNodeTags'; +import {getNearestVNode} from '../../dom/DOMInternalKeys'; +import {Addition, InitFlag} from './VNodeFlags'; +import { BELONG_CLASS_VNODE_KEY } from './VNode'; -export function travelChildren(beginVNode: VNode, handleVNode: Function, isFinish?: Function) { - let node: VNode | null = beginVNode; +export function travelChildren( + beginVNode: VNode | null, + handleVNode: (node: VNode) => void, + isFinish?: (node: VNode) => boolean +) { + let node = beginVNode; while (node !== null) { if (isFinish && isFinish(node)) { @@ -41,15 +45,16 @@ export function travelChildren(beginVNode: VNode, handleVNode: Function, isFinis // 从beginVNode开始深度遍历vNode树,对每个vNode调用handleVNode方法 export function travelVNodeTree( beginVNode: VNode, - handleVNode: Function, + handleVNode: (node: VNode) => VNode | boolean | null | void, childFilter: ((node: VNode) => boolean) | null, // 返回true不处理child finishVNode: VNode, // 结束遍历节点,有时候和beginVNode不相同 - handleWhenToParent: Function | null -): VNode | null { + handleWhenToParent: ((node: VNode) => void) | null +): VNode | boolean | null | void { let node = beginVNode; while (true) { const ret = handleVNode(node); + // 如果处理一个vNode时有返回值,则中断遍历 if (ret) { return ret; @@ -68,6 +73,8 @@ export function travelVNodeTree( return null; } + const isFun = typeof handleWhenToParent === 'function'; + // 找兄弟,没有就往上再找兄弟 while (node.next === null) { if (node.parent === null || node.parent === finishVNode) { @@ -75,8 +82,8 @@ export function travelVNodeTree( } node = node.parent; - if (typeof handleWhenToParent === 'function') { - handleWhenToParent(node); + if (isFun) { + handleWhenToParent!(node); } } // 找到兄弟 @@ -89,14 +96,20 @@ export function travelVNodeTree( // 置空vNode export function clearVNode(vNode: VNode) { vNode.isCleared = true; + + // 孩子节点的parent也置空 + travelChildren(vNode.child, (node) => { + node.parent = null; + }); vNode.child = null; + + vNode.parent = null; vNode.next = null; vNode.depContexts = null; vNode.dirtyNodes = null; vNode.state = null; vNode.hooks = null; vNode.props = null; - vNode.parent = null; vNode.suspenseState = null; vNode.changeList = null; vNode.effectList = null; @@ -111,7 +124,7 @@ export function clearVNode(vNode: VNode) { vNode.toUpdateNodes = null; - vNode.belongClassVNode = null; + vNode[BELONG_CLASS_VNODE_KEY] = null; if (window.__HORIZON_DEV_HOOK__) { const hook = window.__HORIZON_DEV_HOOK__; hook.deleteVNode(vNode); @@ -129,7 +142,7 @@ function isDomContainer(vNode: VNode): boolean { } export function findDomVNode(vNode: VNode): VNode | null { - return travelVNodeTree( + const ret = travelVNodeTree( vNode, node => { if (node.tag === DomComponent || node.tag === DomText) { @@ -141,6 +154,8 @@ export function findDomVNode(vNode: VNode): VNode | null { vNode, null ); + + return ret as VNode | null; } export function findDOMByClassInst(inst) { @@ -154,13 +169,6 @@ export function findDOMByClassInst(inst) { return domVNode !== null ? domVNode.realNode : null; } -// 判断dom树是否已经挂载 -export function isMounted(vNode: VNode) { - const rootNode = getTreeRootVNode(vNode); - // 如果根节点是 Dom 类型节点,表示已经挂载 - return rootNode.tag === TreeRoot; -} - function getTreeRootVNode(vNode) { let node = vNode; while (node.parent) { @@ -169,6 +177,13 @@ function getTreeRootVNode(vNode) { return node; } +// 判断dom树是否已经挂载 +export function isMounted(vNode: VNode) { + const rootNode = getTreeRootVNode(vNode); + // 如果根节点是 Dom 类型节点,表示已经挂载 + return rootNode.tag === TreeRoot; +} + // 找到相邻的DOM export function getSiblingDom(vNode: VNode): Element | null { let node: VNode = vNode; diff --git a/package.json b/package.json index f2d728f2..22d01c6d 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,7 @@ { + "name": "horizon-core", + "description": "Horizon is a JavaScript framework library.", + "version": "0.0.52", "private": true, "workspaces": [ "libs/*" @@ -8,8 +11,6 @@ "prettier": "prettier -w libs/**/*.ts", "build": "rollup --config ./scripts/rollup/rollup.config.js", "build:watch": "rollup --watch --config ./scripts/rollup/rollup.config.js", - "build:3rdLib": "node ./scripts/gen3rdLib.js build:3rdLib", - "build:3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js build:3rdLib-dev", "build:horizon3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js build:horizon3rdLib-dev", "build-types": "tsc -p ./libs/horizon/index.ts --emitDeclarationOnly --declaration --declarationDir ./build/horizon/@types --skipLibCheck || echo \"WARNING: TSC exited with status $?\"", "debug-test": "yarn test --debug", @@ -35,11 +36,11 @@ "@babel/plugin-transform-object-super": "7.16.7", "@babel/plugin-transform-parameters": "7.16.7", "@babel/plugin-transform-react-jsx": "7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", "@babel/plugin-transform-runtime": "7.16.7", "@babel/plugin-transform-shorthand-properties": "7.16.7", "@babel/plugin-transform-spread": "7.16.7", "@babel/plugin-transform-template-literals": "7.16.7", - "@babel/plugin-transform-react-jsx-source": "^7.16.7", "@babel/preset-env": "7.16.7", "@babel/preset-typescript": "7.16.7", "@rollup/plugin-babel": "^5.3.1", @@ -50,6 +51,7 @@ "@typescript-eslint/eslint-plugin": "4.8.0", "@typescript-eslint/parser": "4.8.0", "babel-jest": "^27.5.1", + "ejs": "^3.1.8", "eslint": "7.13.0", "eslint-config-prettier": "^6.9.0", "eslint-plugin-jest": "^22.15.0", @@ -67,5 +69,6 @@ "engines": { "node": ">=10.x", "npm": ">=7.x" - } + }, + "dependencies": {} } diff --git a/scripts/__tests__/ActTest/act.test.js b/scripts/__tests__/ActTest/act.test.js new file mode 100644 index 00000000..00f719ca --- /dev/null +++ b/scripts/__tests__/ActTest/act.test.js @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Horizon, { render, useState, act, useEffect } from '@cloudsop/horizon/index.ts'; + +describe('Horizon.act function Test', () => { + it('The act can wait for the useEffect update to complete.', function () { + const Parent = props => { + const [buttonOptions, setBtn] = useState([]); + const [checkedRows, setCheckedRows] = useState([]); + + useEffect(() => { + setBtn([1, 2, 3]); + }, [checkedRows.length]); + + return ( +
+ +
+ ); + }; + + const Child = props => { + const { buttonOptions } = props; + const [btnList, setBtnList] = useState(0); + + useEffect(() => { + setBtnList(buttonOptions.length); + }, [buttonOptions]); + + return
{btnList}
; + }; + + act(() => { + render(, container); + }); + + // act能够等待useEffect触发的update完成 + expect(container.querySelector('#childDiv').innerHTML).toEqual('3'); + }); +}); diff --git a/scripts/__tests__/ComponentTest/ClassRefs.test.js b/scripts/__tests__/ComponentTest/ClassRefs.test.js new file mode 100644 index 00000000..d5e91d29 --- /dev/null +++ b/scripts/__tests__/ComponentTest/ClassRefs.test.js @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Horizon from '@cloudsop/horizon/index.ts'; + +describe('Class refs Test', () => { + it('Parent can get Child instance by refs', function () { + let pInst; + + class Parent extends Horizon.Component { + componentDidMount() { + pInst = this; + } + + render() { + return ( +
+ +
childDiv
+
+
+ ); + } + } + + class Child extends Horizon.Component { + state = { y: 0 }; + + render() { + return
{this.props.children}
; + } + } + + Horizon.render(, container); + + expect(pInst.refs['child'].state.y).toEqual(0); + expect(pInst.refs['childDiv'].innerHTML).toEqual('childDiv'); + }); +}); diff --git a/scripts/__tests__/ComponentTest/FunctionComponent.test.js b/scripts/__tests__/ComponentTest/FunctionComponent.test.js index 7793cda6..bd02b9d5 100644 --- a/scripts/__tests__/ComponentTest/FunctionComponent.test.js +++ b/scripts/__tests__/ComponentTest/FunctionComponent.test.js @@ -60,4 +60,34 @@ describe('FunctionComponent Test', () => { expect(realNode).toBe(null); }); + it('测试函数组件的defaultProps:Horizon.memo(Horizon.forwardRef(()=>{}))两层包装的场景后,defaultProps依然正常', () => { + const App = () => { + return ; + }; + + const DefaultPropsComp = Horizon.forwardRef(props => { + return
{props.name}
; + }); + DefaultPropsComp.defaultProps = { + name: 'Hello!', + }; + const DefaultPropsCompMemo = Horizon.memo(DefaultPropsComp); + + Horizon.render(, container); + expect(container.querySelector('div').innerHTML).toBe('Hello!'); + }); + + it('测试', () => { + const App = () => { + return ; + }; + + const StyleComp = props => { + return
{props.name}
; + }; + + Horizon.render(, container); + expect(container.querySelector('div').style['_values']['--max-segment-num']).toBe(10); + }); + }); diff --git a/scripts/__tests__/ComponentTest/JsxElement.test.js b/scripts/__tests__/ComponentTest/JsxElement.test.js new file mode 100644 index 00000000..8794fbbf --- /dev/null +++ b/scripts/__tests__/ComponentTest/JsxElement.test.js @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Horizon from '@cloudsop/horizon/index.ts'; + +describe('JSX Element test', () => { + it('symbol attribute prevent cloneDeep unlimited loop', function () { + + function cloneDeep(obj) { + const result = {}; + Object.keys(obj).forEach(key => { + if (obj[key] && typeof obj[key] === 'object') { + result[key] = cloneDeep(obj[key]); + } else { + result[key] = obj[key]; + } + }) + return result; + } + class Demo extends Horizon.Component { + render() { + return ( +
+ hello +
+ ); + } + } + + const ele = Horizon.createElement(Demo); + const copy = cloneDeep(ele); + expect(copy.vtype).toEqual(ele.vtype); + expect(Object.getOwnPropertySymbols(copy).length).toEqual(0); + }); +}); + diff --git a/scripts/__tests__/DomTest/Attribute.test.js b/scripts/__tests__/DomTest/Attribute.test.js index e8aa843b..06706ede 100755 --- a/scripts/__tests__/DomTest/Attribute.test.js +++ b/scripts/__tests__/DomTest/Attribute.test.js @@ -83,6 +83,12 @@ describe('Dom Attribute', () => { expect(window.getComputedStyle(div).getPropertyValue('height')).toBe('20px'); }); + it('WebkitLineClamp和lineClamp样式不会把数字转换成字符串或者追加"px"', () => { + Horizon.render(
, container); + // 浏览器可以将WebkitLineClamp识别为-webkit-line-clamp,测试框架不可以 + expect(container.querySelector('div').style.WebkitLineClamp).toBe(2); + }); + it('空字符串做属性名', () => { const emptyStringProps = { '': '' }; expect(() => { diff --git a/scripts/__tests__/EventTest/MouseEnterEvent.test.js b/scripts/__tests__/EventTest/MouseEnterEvent.test.js new file mode 100644 index 00000000..6256062f --- /dev/null +++ b/scripts/__tests__/EventTest/MouseEnterEvent.test.js @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Horizon from '@cloudsop/horizon/index.ts'; + +describe('mouseenter和mouseleave事件测试', () => { + let container; + + beforeEach(() => { + jest.resetModules(); + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + container = null; + }); + + it('在iframe中mouseleave事件的relateTarget属性', () => { + const iframe = document.createElement('iframe'); + container.appendChild(iframe); + const iframeDocument = iframe.contentDocument; + iframeDocument.write( + '
', + ); + iframeDocument.close(); + + const leaveEvents = []; + const node = Horizon.render( +
{ + e.persist(); + leaveEvents.push(e); + }} + />, + iframeDocument.body.getElementsByTagName('div')[0], + ); + + node.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: iframe.contentWindow, + }), + ); + + expect(leaveEvents.length).toBe(1); + expect(leaveEvents[0].target).toBe(node); + expect(leaveEvents[0].relatedTarget).toBe(iframe.contentWindow); + }); + + it('在iframe中mouseenter事件的relateTarget属性', () => { + const iframe = document.createElement('iframe'); + container.appendChild(iframe); + const iframeDocument = iframe.contentDocument; + iframeDocument.write( + '
', + ); + iframeDocument.close(); + + const enterEvents = []; + const node = Horizon.render( +
{ + e.persist(); + enterEvents.push(e); + }} + />, + iframeDocument.body.getElementsByTagName('div')[0], + ); + + node.dispatchEvent( + new MouseEvent('mouseover', { + bubbles: true, + cancelable: true, + relatedTarget: null, + }), + ); + + expect(enterEvents.length).toBe(1); + expect(enterEvents[0].target).toBe(node); + expect(enterEvents[0].relatedTarget).toBe(iframe.contentWindow); + }); + + it('从新渲染的子组件触发mouseout事件,子组件响应mouseenter事件,父节点不响应', () => { + let parentEnterCalls = 0; + let childEnterCalls = 0; + let parent = null; + + class Parent extends Horizon.Component { + render() { + return ( +
parentEnterCalls++} + ref={node => (parent = node)}> + {this.props.showChild && ( +
childEnterCalls++}/> + )} +
+ ); + } + } + + Horizon.render(, container); + Horizon.render(, container); + + parent.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: parent.firstChild, + }), + ); + expect(childEnterCalls).toBe(1); + expect(parentEnterCalls).toBe(0); + }); + + it('render一个新组件,兄弟节点触发mouseout事件,mouseenter事件响应一次', done => { + const mockFn1 = jest.fn(); + const mockFn2 = jest.fn(); + const mockFn3 = jest.fn(); + + class Parent extends Horizon.Component { + constructor(props) { + super(props); + this.parentEl = Horizon.createRef(); + } + + componentDidMount() { + Horizon.render(, this.parentEl.current); + } + + render() { + return
; + } + } + + class MouseEnterDetect extends Horizon.Component { + constructor(props) { + super(props); + this.firstEl = Horizon.createRef(); + this.siblingEl = Horizon.createRef(); + } + + componentDidMount() { + this.siblingEl.current.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: this.firstEl.current, + }), + ); + expect(mockFn1.mock.calls.length).toBe(1); + expect(mockFn2.mock.calls.length).toBe(1); + expect(mockFn3.mock.calls.length).toBe(0); + done(); + } + + render() { + return ( + +
+
+ + ); + } + } + + Horizon.render(, container); + }); + + it('未被horizon管理的节点触发mouseout事件,mouseenter事件也能正常触发', done => { + const mockFn = jest.fn(); + + class Parent extends Horizon.Component { + constructor(props) { + super(props); + this.parentEl = Horizon.createRef(); + } + + componentDidMount() { + Horizon.render(, this.parentEl.current); + } + + render() { + return
; + } + } + + class MouseEnterDetect extends Horizon.Component { + constructor(props) { + super(props); + this.divRef = Horizon.createRef(); + this.siblingEl = Horizon.createRef(); + } + + componentDidMount() { + const attachedNode = document.createElement('div'); + this.divRef.current.appendChild(attachedNode); + attachedNode.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: this.siblingEl.current, + }), + ); + expect(mockFn.mock.calls.length).toBe(1); + done(); + } + + render() { + return ( +
+
+
+ ); + } + } + + Horizon.render(, container); + }); + + it('外部portal节点触发的mouseout事件,根节点的mouseleave事件也能响应', () => { + const divRef = Horizon.createRef(); + const onMouseLeave = jest.fn(); + + function Component() { + return ( +
+ {Horizon.createPortal(
, document.body)} +
+ ); + } + + Horizon.render(, container); + + divRef.current.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: document.body, + }), + ); + + expect(onMouseLeave).toHaveBeenCalledTimes(1); + }); + + it('外部portal节点触发的mouseout事件,根节点的mouseEnter事件也能响应', () => { + const divRef = Horizon.createRef(); + const otherDivRef = Horizon.createRef(); + const onMouseEnter = jest.fn(); + + function Component() { + return ( +
+ {Horizon.createPortal( +
, + document.body, + )} +
+ ); + } + + Horizon.render(, container); + + divRef.current.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: otherDivRef.current, + }), + ); + + expect(onMouseEnter).toHaveBeenCalledTimes(1); + }); +}); + + diff --git a/scripts/__tests__/HorizonXText/StateManager/StateArray.test.tsx b/scripts/__tests__/HorizonXTest/StateManager/StateArray.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StateManager/StateArray.test.tsx rename to scripts/__tests__/HorizonXTest/StateManager/StateArray.test.tsx diff --git a/scripts/__tests__/HorizonXText/StateManager/StateMap.test.tsx b/scripts/__tests__/HorizonXTest/StateManager/StateMap.test.tsx similarity index 98% rename from scripts/__tests__/HorizonXText/StateManager/StateMap.test.tsx rename to scripts/__tests__/HorizonXTest/StateManager/StateMap.test.tsx index f327b09c..e91e0e13 100644 --- a/scripts/__tests__/HorizonXText/StateManager/StateMap.test.tsx +++ b/scripts/__tests__/HorizonXTest/StateManager/StateMap.test.tsx @@ -73,13 +73,13 @@ describe('测试store中的Map', () => { function Parent(props) { const userStore = useUserStore(); - const addOnePerson = function() { + const addOnePerson = function () { userStore.addOnePerson(newPerson); }; - const delOnePerson = function() { + const delOnePerson = function () { userStore.delOnePerson(newPerson); }; - const clearPersons = function() { + const clearPersons = function () { userStore.clearPersons(); }; diff --git a/scripts/__tests__/HorizonXText/StateManager/StateMixType.test.tsx b/scripts/__tests__/HorizonXTest/StateManager/StateMixType.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StateManager/StateMixType.test.tsx rename to scripts/__tests__/HorizonXTest/StateManager/StateMixType.test.tsx diff --git a/scripts/__tests__/HorizonXText/StateManager/StateSet.test.tsx b/scripts/__tests__/HorizonXTest/StateManager/StateSet.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StateManager/StateSet.test.tsx rename to scripts/__tests__/HorizonXTest/StateManager/StateSet.test.tsx diff --git a/scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.tsx b/scripts/__tests__/HorizonXTest/StateManager/StateWeakMap.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StateManager/StateWeakMap.test.tsx rename to scripts/__tests__/HorizonXTest/StateManager/StateWeakMap.test.tsx diff --git a/scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.tsx b/scripts/__tests__/HorizonXTest/StateManager/StateWeakSet.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StateManager/StateWeakSet.test.tsx rename to scripts/__tests__/HorizonXTest/StateManager/StateWeakSet.test.tsx diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx b/scripts/__tests__/HorizonXTest/StoreFunctionality/async.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx rename to scripts/__tests__/HorizonXTest/StoreFunctionality/async.test.tsx diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx b/scripts/__tests__/HorizonXTest/StoreFunctionality/basicAccess.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/basicAccess.test.tsx rename to scripts/__tests__/HorizonXTest/StoreFunctionality/basicAccess.test.tsx diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/cloneDeep.test.js b/scripts/__tests__/HorizonXTest/StoreFunctionality/cloneDeep.test.js similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/cloneDeep.test.js rename to scripts/__tests__/HorizonXTest/StoreFunctionality/cloneDeep.test.js diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.tsx b/scripts/__tests__/HorizonXTest/StoreFunctionality/dollarAccess.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/dollarAccess.test.tsx rename to scripts/__tests__/HorizonXTest/StoreFunctionality/dollarAccess.test.tsx diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.tsx b/scripts/__tests__/HorizonXTest/StoreFunctionality/otherCases.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/otherCases.test.tsx rename to scripts/__tests__/HorizonXTest/StoreFunctionality/otherCases.test.tsx diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/reset.js b/scripts/__tests__/HorizonXTest/StoreFunctionality/reset.js similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/reset.js rename to scripts/__tests__/HorizonXTest/StoreFunctionality/reset.js diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/store.ts b/scripts/__tests__/HorizonXTest/StoreFunctionality/store.ts similarity index 100% rename from scripts/__tests__/HorizonXText/StoreFunctionality/store.ts rename to scripts/__tests__/HorizonXTest/StoreFunctionality/store.ts diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js b/scripts/__tests__/HorizonXTest/StoreFunctionality/utils.test.js similarity index 87% rename from scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js rename to scripts/__tests__/HorizonXTest/StoreFunctionality/utils.test.js index 7d1f786f..be07b4c4 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js +++ b/scripts/__tests__/HorizonXTest/StoreFunctionality/utils.test.js @@ -64,7 +64,6 @@ describe('Mutation resolve', () => { it('should resolve mutation same type types, same object', () => { const mutation = resolveMutation({ a: 1, b: 2 }, { a: 1, b: 2 }); - console.log(mutation); expect(mutation.mutation).toBe(false); }); @@ -78,3 +77,17 @@ describe('Mutation resolve', () => { expect(mutation.attributes.c.to).toBe(2); }); }); + +describe('Mutation collections', () => { + it('should resolve mutation of two sets', () => { + const values = [{ a: 1 }, { b: 2 }, { c: 3 }]; + + const source = new Set([values[0], values[1], values[2]]); + + const target = new Set([values[0], values[1]]); + + const mutation = resolveMutation(source, target); + + expect(mutation.mutation).toBe(true); + }); +}); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx b/scripts/__tests__/HorizonXTest/StoreFunctionality/watch.test.tsx similarity index 98% rename from scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx rename to scripts/__tests__/HorizonXTest/StoreFunctionality/watch.test.tsx index 1aa765c3..b6e44453 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx +++ b/scripts/__tests__/HorizonXTest/StoreFunctionality/watch.test.tsx @@ -17,7 +17,7 @@ import { createStore } from '@cloudsop/horizon/src/horizonx/store/StoreHandler'; import { watch } from '@cloudsop/horizon/src/horizonx/proxy/watch'; describe('watch', () => { - it('shouhld watch promitive state variable', async () => { + it('shouhld watch primitive state variable', async () => { const useStore = createStore({ state: { variable: 'x', diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.tsx b/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/adapters/ReduxAdapter.test.tsx rename to scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.tsx b/scripts/__tests__/HorizonXTest/adapters/ReduxAdapterThunk.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/adapters/ReduxAdapterThunk.test.tsx rename to scripts/__tests__/HorizonXTest/adapters/ReduxAdapterThunk.test.tsx diff --git a/scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx b/scripts/__tests__/HorizonXTest/adapters/ReduxReactAdapter.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/adapters/ReduxReactAdapter.test.tsx rename to scripts/__tests__/HorizonXTest/adapters/ReduxReactAdapter.test.tsx diff --git a/scripts/__tests__/HorizonXText/adapters/connectTest.tsx b/scripts/__tests__/HorizonXTest/adapters/connectTest.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/adapters/connectTest.tsx rename to scripts/__tests__/HorizonXTest/adapters/connectTest.tsx diff --git a/scripts/__tests__/HorizonXText/class/ClassException.test.tsx b/scripts/__tests__/HorizonXTest/class/ClassException.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/class/ClassException.test.tsx rename to scripts/__tests__/HorizonXTest/class/ClassException.test.tsx diff --git a/scripts/__tests__/HorizonXText/class/ClassStateArray.test.tsx b/scripts/__tests__/HorizonXTest/class/ClassStateArray.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/class/ClassStateArray.test.tsx rename to scripts/__tests__/HorizonXTest/class/ClassStateArray.test.tsx diff --git a/scripts/__tests__/HorizonXText/class/ClassStateMap.test.tsx b/scripts/__tests__/HorizonXTest/class/ClassStateMap.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/class/ClassStateMap.test.tsx rename to scripts/__tests__/HorizonXTest/class/ClassStateMap.test.tsx diff --git a/scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.tsx b/scripts/__tests__/HorizonXTest/clear/ClassVNodeClear.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/clear/ClassVNodeClear.test.tsx rename to scripts/__tests__/HorizonXTest/clear/ClassVNodeClear.test.tsx diff --git a/scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.tsx b/scripts/__tests__/HorizonXTest/clear/FunctionVNodeClear.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/clear/FunctionVNodeClear.test.tsx rename to scripts/__tests__/HorizonXTest/clear/FunctionVNodeClear.test.tsx diff --git a/scripts/__tests__/HorizonXTest/edgeCases/deepVariableObserver.test.tsx b/scripts/__tests__/HorizonXTest/edgeCases/deepVariableObserver.test.tsx new file mode 100644 index 00000000..6fff3531 --- /dev/null +++ b/scripts/__tests__/HorizonXTest/edgeCases/deepVariableObserver.test.tsx @@ -0,0 +1,155 @@ +import { createStore, useStore } from '@cloudsop/horizon/src/horizonx/store/StoreHandler'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +describe('Using deep variables', () => { + it('should listen to object variable change', () => { + let counter = 0; + const useTestStore = createStore({ + state: { a: { b: { c: 1 } } }, + }); + const testStore = useTestStore(); + testStore.$subscribe(() => { + counter++; + }); + + testStore.a.b.c = 0; + + expect(counter).toBe(1); + }); + + it('should listen to deep variable change', () => { + let counter = 0; + const useTestStore = createStore({ + state: { color: [{ a: 1 }, 255, 255] }, + }); + const testStore = useTestStore(); + testStore.$subscribe(() => { + counter++; + }); + + for (let i = 0; i < 5; i++) { + testStore.color[0].a = i; + } + testStore.color = 'x'; + + expect(counter).toBe(6); + }); + + it('should use set', () => { + const useTestStore = createStore({ + state: { data: new Set() }, + }); + const testStore = useTestStore(); + + const a = { a: true }; + + testStore.data.add(a); + + expect(testStore.data.has(a)).toBe(true); + + testStore.data.add(a); + testStore.data.add(a); + testStore.data.delete(a); + + expect(testStore.data.has(a)).toBe(false); + + testStore.data.add(a); + + const values = Array.from(testStore.data.values()); + expect(values.length).toBe(1); + + let counter = 0; + testStore.$subscribe(mutation => { + counter++; + }); + + values.forEach(val => { + val.a = !val.a; + }); + + expect(testStore.data.has(a)).toBe(true); + + expect(counter).toBe(1); + }); + + it('should use map', () => { + const useTestStore = createStore({ + state: { data: new Map() }, + }); + const testStore = useTestStore(); + + const data = { key: { a: 1 }, value: { b: 2 } }; + + testStore.data.set(data.key, data.value); + + const key = Array.from(testStore.data.keys())[0]; + + expect(testStore.data.has(key)).toBe(true); + + testStore.data.set(data.key, data.value); + testStore.data.set(data.key, data.value); + testStore.data.delete(key); + + expect(testStore.data.get(key)).toBe(); + + testStore.data.set(data.key, data.value); + + const entries = Array.from(testStore.data.entries()); + expect(entries.length).toBe(1); + + let counter = 0; + testStore.$subscribe(mutation => { + counter++; + }); + + entries.forEach(([key, value]) => { + key.a++; + value.b++; + }); + + expect(counter).toBe(2); + }); + + it('should use weakSet', () => { + const useTestStore = createStore({ + state: { data: new WeakSet() }, + }); + const testStore = useTestStore(); + + const a = { a: true }; + + testStore.data.add(a); + + expect(testStore.data.has(a)).toBe(true); + + testStore.data.add(a); + testStore.data.add(a); + testStore.data.delete(a); + + expect(testStore.data.has(a)).toBe(false); + + testStore.data.add(a); + + expect(testStore.data.has(a)).toBe(true); + }); + + it('should use weakMap', () => { + const useTestStore = createStore({ + state: { data: new WeakMap() }, + }); + const testStore = useTestStore(); + + const data = { key: { a: 1 }, value: { b: 2 } }; + + testStore.data.set(data.key, data.value); + + let counter = 0; + testStore.$subscribe(mutation => { + counter++; + }); + + testStore.data.get(data.key).b++; + + expect(counter).toBe(1); + }); +}); diff --git a/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx b/scripts/__tests__/HorizonXTest/edgeCases/multipleStores.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx rename to scripts/__tests__/HorizonXTest/edgeCases/multipleStores.test.tsx diff --git a/scripts/__tests__/HorizonXText/edgeCases/proxy.test.tsx b/scripts/__tests__/HorizonXTest/edgeCases/proxy.test.tsx similarity index 100% rename from scripts/__tests__/HorizonXText/edgeCases/proxy.test.tsx rename to scripts/__tests__/HorizonXTest/edgeCases/proxy.test.tsx diff --git a/scripts/rollup/rollup.config.js b/scripts/rollup/rollup.config.js index cf98694f..33ca0c34 100644 --- a/scripts/rollup/rollup.config.js +++ b/scripts/rollup/rollup.config.js @@ -20,8 +20,8 @@ import fs from 'fs'; import replace from '@rollup/plugin-replace'; import copy from './copy-plugin'; import execute from 'rollup-plugin-execute'; -import { terser } from 'rollup-plugin-terser'; -import { version as horizonVersion } from '@cloudsop/horizon/package.json'; +import {terser} from 'rollup-plugin-terser'; +import {version as horizonVersion} from '@cloudsop/horizon/package.json'; const extensions = ['.js', '.ts']; @@ -38,13 +38,41 @@ if (!fs.existsSync(outDir)) { const outputResolve = (...p) => path.resolve(outDir, ...p); +const isDev = (mode) => { + return mode === 'development'; +} + +const getBasicPlugins = (mode) => { + return [ + nodeResolve({ + extensions, + modulesOnly: true, + }), + babel({ + exclude: 'node_modules/**', + configFile: path.join(__dirname, '../../babel.config.js'), + babelHelpers: 'runtime', + extensions, + }), + replace({ + values: { + 'process.env.NODE_ENV': `"${mode}"`, + isDev: isDev(mode).toString(), + isTest: false, + __VERSION__: `"${horizonVersion}"`, + }, + preventAssignment: true, + }), + ]; +} + + function getOutputName(mode) { return mode === 'production' ? `horizon.${mode}.min.js` : `horizon.${mode}.js`; } function genConfig(mode) { - const isDev = mode === 'development'; - const sourcemap = isDev ? 'inline' : false; + const sourcemap = isDev(mode) ? 'inline' : false; return { input: path.resolve(libDir, 'index.ts'), output: [ @@ -61,25 +89,7 @@ function genConfig(mode) { }, ], plugins: [ - nodeResolve({ - extensions, - modulesOnly: true, - }), - babel({ - exclude: 'node_modules/**', - configFile: path.join(__dirname, '../../babel.config.js'), - babelHelpers: 'runtime', - extensions, - }), - replace({ - values: { - 'process.env.NODE_ENV': `"${mode}"`, - isDev: isDev.toString(), - isTest: false, - __VERSION__: `"${horizonVersion}"`, - }, - preventAssignment: true, - }), + ...getBasicPlugins(mode), execute('npm run build-types'), mode === 'production' && terser(), copy([ @@ -96,4 +106,30 @@ function genConfig(mode) { }; } -export default [genConfig('development'), genConfig('production')]; +function genJSXRuntimeConfig(mode) { + return { + input: path.resolve(libDir, 'jsx-runtime.ts'), + output: { + file: outputResolve('jsx-runtime.js'), + format: 'cjs', + }, + plugins: [ + ...getBasicPlugins(mode) + ] + }; +} + +function genJSXDEVRuntimeConfig(mode) { + return { + input: path.resolve(libDir, 'jsx-dev-runtime.ts'), + output: { + file: outputResolve('jsx-dev-runtime.js'), + format: 'cjs', + }, + plugins: [ + ...getBasicPlugins(mode) + ] + }; +} + +export default [genConfig('development'), genConfig('production'), genJSXRuntimeConfig(''), genJSXDEVRuntimeConfig('')];