diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..fb1f788c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +## 0.0.8 (2022-07-08) +### Features +- 增加HorizonX提供状态管理能力 +### Bug Fixes +- **core**: 修复局部更新场景下context计算错误 +### Code Refactoring +- 重构事件机制,取消全量挂载事件,改为按需懒挂载 diff --git a/fixtures/antd/.babelrc b/fixtures/antd/.babelrc new file mode 100644 index 00000000..bb6ba7f0 --- /dev/null +++ b/fixtures/antd/.babelrc @@ -0,0 +1,27 @@ +{ + "presets": [ + "@babel/react", + "@babel/typescript", + [ + "@babel/env", + { + "modules": false + } + ] + ], + "plugins": [ + [ + "@babel/plugin-proposal-class-properties", + { + "loose": true + } + ], + [ + "@babel/plugin-transform-react-jsx", + { + "pragma": "Horizon.createElement", + "pragmaFrag": "Horizon.Fragment" + } + ] + ] +} diff --git a/fixtures/antd/README.md b/fixtures/antd/README.md new file mode 100644 index 00000000..6a5b5e81 --- /dev/null +++ b/fixtures/antd/README.md @@ -0,0 +1,3 @@ +Horizon X antd demo: +1. run `npm run build:watch` in root's `package.json` +2. run `npm start` to run Horizon X antd diff --git a/fixtures/antd/components/Menu.jsx b/fixtures/antd/components/Menu.jsx new file mode 100644 index 00000000..16f6b407 --- /dev/null +++ b/fixtures/antd/components/Menu.jsx @@ -0,0 +1,38 @@ +import Horizon from 'horizon'; +import { AppstoreOutlined } from '@ant-design/icons'; +import { Menu } from 'antd'; + +function getItem(label, key, icon, children, type) { + return { + key, + icon, + children, + label, + type, + }; +} + +const items = [ + getItem('sub2', 'sub2', , [getItem('sub3', 'sub3', null, [getItem('sub4', 'sub4')])]), +]; + +const App = () => { + const onClick = e => { + console.log('click ', e); + }; + + return ( + + ); +}; + +export default App; diff --git a/fixtures/antd/components/Menu2.jsx b/fixtures/antd/components/Menu2.jsx new file mode 100644 index 00000000..97c1679a --- /dev/null +++ b/fixtures/antd/components/Menu2.jsx @@ -0,0 +1,73 @@ +import Horizon, { useState } from 'horizon'; +import { + AppstoreOutlined, + ContainerOutlined, + MenuFoldOutlined, + PieChartOutlined, + DesktopOutlined, + MailOutlined, + MenuUnfoldOutlined, +} from '@ant-design/icons'; +import { Button, Menu } from 'antd'; + +function getItem(label, key, icon, children, type) { + return { + key, + icon, + children, + label, + type, + }; +} + +const items = [ + getItem('选项1', '1', ), + getItem('选项2', '2', ), + getItem('选项3', '3', ), + getItem('分组1', 'sub1', , [ + getItem('选项5', '5'), + getItem('选项6', '6'), + getItem('选项7', '7'), + getItem('选项8', '8'), + ]), + getItem('分组2', 'sub2', , [ + getItem('选项9', '9'), + getItem('选项10', '10'), + getItem('分组2-1', 'sub3', null, [getItem('选项11', '11'), getItem('选项12', '12')]), + ]), +]; + +const App = () => { + const [collapsed, setCollapsed] = useState(false); + + return ( +
+ + +
+ ); +}; + +export default App; diff --git a/fixtures/antd/components/Table.jsx b/fixtures/antd/components/Table.jsx new file mode 100644 index 00000000..5ca9b680 --- /dev/null +++ b/fixtures/antd/components/Table.jsx @@ -0,0 +1,97 @@ +import Horizon from 'horizon'; +import { Table } from 'antd'; +const columns = [ + { + title: 'Full Name', + width: 100, + dataIndex: 'name', + key: 'name', + fixed: 'left', + }, + { + title: 'Lang', + width: 100, + dataIndex: 'lang', + key: 'age', + fixed: 'left', + }, + { + title: 'COL1', + dataIndex: 'description', + key: '1', + width: 220, + }, + { + title: 'COL2', + dataIndex: 'description', + key: '2', + width: 220, + }, + { + title: 'COL3', + dataIndex: 'description', + key: '3', + width: 220, + }, + { + title: 'COL4', + dataIndex: 'description', + key: '4', + width: 220, + }, + { + title: 'COL5', + dataIndex: 'description', + key: '5', + width: 220, + }, + { + title: 'COL6', + dataIndex: 'description', + key: '6', + width: 220, + }, + { + title: 'COL7', + dataIndex: 'description', + key: '7', + width: 220, + }, + { + title: 'COL8', + dataIndex: 'description', + key: '8', + }, + { + title: 'Action', + key: 'operation', + fixed: 'right', + width: 100, + render: () => action, + }, +]; +const data = []; + +for (let i = 0; i < 100; i++) { + data.push({ + key: i, + name: `Horizon ${i}`, + lang: 'js', + description: `Javascript Framework no. ${i}`, + }); +} + +const App = () => ( +
+ + +); + +export default App; diff --git a/fixtures/antd/index.jsx b/fixtures/antd/index.jsx new file mode 100644 index 00000000..a603b89c --- /dev/null +++ b/fixtures/antd/index.jsx @@ -0,0 +1,29 @@ +import Horizon from 'horizon'; +import 'antd/dist/antd.css'; +import Table from './components/Table'; +import Menu from './components/Menu'; +import Menu2 from './components/Menu2'; +import { Tabs } from 'antd'; + +const { TabPane } = Tabs; + +const onChange = key => { + console.log(key); +}; +const App = () => ( +
+

Horizon ❌ antd

+ + +
+ + +
+ + +
+
+ + +); +Horizon.render(, document.getElementById('app')); diff --git a/fixtures/antd/package.json b/fixtures/antd/package.json new file mode 100644 index 00000000..fdde6acd --- /dev/null +++ b/fixtures/antd/package.json @@ -0,0 +1,34 @@ +{ + "name": "horizon-antd", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "webpack-dev-server --config webpack.dev.js --hot --mode development --open" + }, + "license": "MIT", + "dependencies": { + "@ant-design/icons": "^4.7.0", + "@babel/polyfill": "^7.10.4", + "antd": "^4.21.3", + "css-loader": "^5.2.2", + "style-loader": "^2.0.0" + }, + "devDependencies": { + "@babel/core": "^7.11.1", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-object-rest-spread": "^7.11.0", + "@babel/plugin-syntax-jsx": "^7.10.4", + "@babel/preset-env": "^7.11.0", + "@babel/preset-react": "^7.10.4", + "@babel/preset-typescript": "^7.10.4", + "@hot-loader/react-dom": "16.9.0", + "babel-loader": "^8.1.0", + "html-webpack-plugin": "^3.2.0", + "html-webpack-template": "^6.2.0", + "react-hot-loader": "^4.12.20", + "webpack": "4.42.0", + "webpack-cli": "3.3.11", + "webpack-dev-server": "^3.10.3", + "webpack-watch-files-plugin": "^1.2.1" + } +} diff --git a/fixtures/antd/webpack.dev.js b/fixtures/antd/webpack.dev.js new file mode 100644 index 00000000..f452ad2c --- /dev/null +++ b/fixtures/antd/webpack.dev.js @@ -0,0 +1,54 @@ +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const horizon = path.resolve(__dirname, '../../build/horizon'); +const config = () => { + return { + entry: ['./index.jsx'], + output: { + path: path.resolve(__dirname, 'temp'), + filename: '[name].[hash].js', + }, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.ts(x)?|js|jsx$/, + exclude: /node_modules/, + loader: 'babel-loader', + }, + { + test: /\.css$/, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 1, + }, + }, + ], + exclude: /\.module\.css$/, + }, + ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js', '.jsx', 'json'], + alias: { + horizon: horizon, + react: horizon, + 'react-dom': horizon, + }, + }, + plugins: [ + new HtmlWebpackPlugin({ + template: require('html-webpack-template'), + title: 'Horizon Antd', + inject: false, + appMountId: 'app', + filename: 'index.html', + }), + ], + }; +}; + +module.exports = config; diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts index 320ac08f..9e29e71d 100644 --- a/libs/horizon/index.ts +++ b/libs/horizon/index.ts @@ -27,10 +27,12 @@ import { useState, useDebugValue } from './src/renderer/hooks/HookExternal'; -import { launchUpdateFromVNode as _launchUpdateFromVNode, asyncUpdates } from './src/renderer/TreeBuilder'; +import { asyncUpdates } from './src/renderer/TreeBuilder'; import { callRenderQueueImmediate } from './src/renderer/taskExecutor/RenderQueue'; import { runAsyncEffects } from './src/renderer/submit/HookEffectHandler'; -import { getProcessingVNode as _getProcessingVNode } from './src/renderer/GlobalVar'; + +import { createStore, useStore, clearStore } from './src/horizonx/store/StoreHandler'; +import * as reduxAdapter from './src/horizonx/adapters/redux'; // act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。 const act = fun => { @@ -79,8 +81,10 @@ const Horizon = { findDOMNode, unmountComponentAtNode, act, - _launchUpdateFromVNode, - _getProcessingVNode, + createStore, + useStore, + clearStore, + reduxAdapter, }; export const version = __VERSION__; @@ -116,9 +120,11 @@ export { findDOMNode, unmountComponentAtNode, act, - // 暂时给HorizonX使用 - _launchUpdateFromVNode, - _getProcessingVNode, + // 状态管理器HorizonX接口 + createStore, + useStore, + clearStore, + reduxAdapter, }; export default Horizon; diff --git a/libs/horizon/index.js b/libs/horizon/npm/index.js similarity index 100% rename from libs/horizon/index.js rename to libs/horizon/npm/index.js diff --git a/libs/horizon/package.json b/libs/horizon/package.json index dedb9445..844a4ee5 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.7", + "version": "0.0.8", "homepage": "", "bugs": "", "main": "index.js", diff --git a/libs/horizon/src/dom/DOMExternal.ts b/libs/horizon/src/dom/DOMExternal.ts index 077d0801..5050e3f5 100644 --- a/libs/horizon/src/dom/DOMExternal.ts +++ b/libs/horizon/src/dom/DOMExternal.ts @@ -22,9 +22,6 @@ function createRoot(children: any, container: Container, callback?: Callback) { const treeRoot = createTreeRootVNode(container); container._treeRoot = treeRoot; - // 根节点挂接全量事件 - listenDelegatedEvents(container as Element); - // 执行回调 if (typeof callback === 'function') { const cb = callback; diff --git a/libs/horizon/src/dom/DOMInternalKeys.ts b/libs/horizon/src/dom/DOMInternalKeys.ts index 2f4ccb23..f389a0cf 100644 --- a/libs/horizon/src/dom/DOMInternalKeys.ts +++ b/libs/horizon/src/dom/DOMInternalKeys.ts @@ -49,23 +49,17 @@ export function getVNode(dom: Node|Container): VNode | null { // 用 DOM 对象,来寻找其对应或者说是最近父级的 vNode export function getNearestVNode(dom: Node): null | VNode { - let vNode = dom[INTERNAL_VNODE]; - if (vNode) { // 如果是已经被框架标记过的 DOM 节点,那么直接返回其 VNode 实例 - return vNode; + let domNode: Node | null = dom; + // 寻找当前节点及其所有祖先节点是否有标记VNODE + while (domNode) { + const vNode = domNode[INTERNAL_VNODE]; + if (vNode) { + return vNode; + } + domNode = domNode.parentNode; } - // 下面处理的是为被框架标记过的 DOM 节点,向上找其父节点是否被框架标记过 - let parentDom = dom.parentNode; - let nearVNode = null; - while (parentDom) { - vNode = parentDom[INTERNAL_VNODE]; - if (vNode) { - nearVNode = vNode; - break; - } - parentDom = parentDom.parentNode; - } - return nearVNode; + return null; } // 获取 vNode 上的属性相关信息 diff --git a/libs/horizon/src/dom/DOMOperator.ts b/libs/horizon/src/dom/DOMOperator.ts index 073456fc..07700dd4 100644 --- a/libs/horizon/src/dom/DOMOperator.ts +++ b/libs/horizon/src/dom/DOMOperator.ts @@ -26,7 +26,7 @@ import { watchValueChange } from './valueHandler/ValueChangeHandler'; import { DomComponent, DomText } from '../renderer/vnode/VNodeTags'; import { updateCommonProp } from './DOMPropertiesHandler/UpdateCommonProp'; -export type Props = { +export type Props = Record & { autoFocus?: boolean; children?: any; dangerouslySetInnerHTML?: any; diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts index c7f76f2f..7864426d 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts @@ -1,20 +1,12 @@ -import { - allDelegatedHorizonEvents, -} from '../../event/EventCollection'; +import { allDelegatedHorizonEvents } from '../../event/EventHub'; import { updateCommonProp } from './UpdateCommonProp'; import { setStyles } from './StyleHandler'; -import { - listenNonDelegatedEvent -} from '../../event/EventBinding'; +import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding'; import { isEventProp } from '../validators/ValidateProps'; +import { getCurrentRoot } from '../../renderer/TreeBuilder'; // 初始化DOM属性和更新 DOM 属性 -export function setDomProps( - dom: Element, - props: Object, - isNativeTag: boolean, - isInit: boolean, -): void { +export function setDomProps(dom: Element, props: Object, isNativeTag: boolean, isInit: boolean): void { const keysOfProps = Object.keys(props); let propName; let propVal; @@ -27,10 +19,14 @@ export function setDomProps( setStyles(dom, propVal); } else if (isEventProp(propName)) { // 事件监听属性处理 + const currentRoot = getCurrentRoot(); if (!allDelegatedHorizonEvents.has(propName)) { listenNonDelegatedEvent(propName, dom, propVal); + } else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) { + lazyDelegateOnRoot(currentRoot, propName); } - } else if (propName === 'children') { // 只处理纯文本子节点,其他children在VNode树中处理 + } else if (propName === 'children') { + // 只处理纯文本子节点,其他children在VNode树中处理 const type = typeof propVal; if (type === 'string' || type === 'number') { dom.textContent = propVal; @@ -44,10 +40,7 @@ export function setDomProps( } // 找出两个 DOM 属性的差别,生成需要更新的属性集合 -export function compareProps( - oldProps: Object, - newProps: Object, -): Object { +export function compareProps(oldProps: Object, newProps: Object): Object { let updatesForStyle = {}; const toUpdateProps = {}; const keysOfOldProps = Object.keys(oldProps); @@ -107,7 +100,8 @@ export function compareProps( } if (propName === 'style') { - if (oldPropValue) { // 之前 style 属性有设置非空值 + if (oldPropValue) { + // 之前 style 属性有设置非空值 // 原来有这个 style,但现在没这个 style 了 oldStyleProps = Object.keys(oldPropValue); for (let j = 0; j < oldStyleProps.length; j++) { @@ -125,7 +119,8 @@ export function compareProps( updatesForStyle[styleProp] = newPropValue[styleProp]; } } - } else { // 之前未设置 style 属性或者设置了空值 + } else { + // 之前未设置 style 属性或者设置了空值 if (Object.keys(updatesForStyle).length === 0) { toUpdateProps[propName] = null; } @@ -144,8 +139,11 @@ export function compareProps( toUpdateProps[propName] = String(newPropValue); } } else if (isEventProp(propName)) { + const currentRoot = getCurrentRoot(); if (!allDelegatedHorizonEvents.has(propName)) { toUpdateProps[propName] = newPropValue; + } else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) { + lazyDelegateOnRoot(currentRoot, propName); } } else { toUpdateProps[propName] = newPropValue; diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts index 49429fbe..b93e1581 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts @@ -1,12 +1,12 @@ -function isNeedUnitCSS(propName: string) { - return !(noUnitCSS.includes(propName) - || propName.startsWith('borderImage') - || propName.startsWith('flex') - || propName.startsWith('gridRow') - || propName.startsWith('gridColumn') - || propName.startsWith('stroke') - || propName.startsWith('box') - || propName.endsWith('Opacity')); +function isNeedUnitCSS(styleName: string) { + return !(noUnitCSS.includes(styleName) + || styleName.startsWith('borderImage') + || styleName.startsWith('flex') + || styleName.startsWith('gridRow') + || styleName.startsWith('gridColumn') + || styleName.startsWith('stroke') + || styleName.startsWith('box') + || styleName.endsWith('Opacity')); } /** @@ -38,9 +38,7 @@ export function setStyles(dom, styles) { Object.keys(styles).forEach((name) => { const styleVal = styles[name]; - const validStyleValue = adjustStyleValue(name, styleVal); - - style[name] = validStyleValue; + style[name] = adjustStyleValue(name, styleVal); }); } diff --git a/libs/horizon/src/dom/utils/Common.ts b/libs/horizon/src/dom/utils/Common.ts index dae06d2b..3ed6a462 100644 --- a/libs/horizon/src/dom/utils/Common.ts +++ b/libs/horizon/src/dom/utils/Common.ts @@ -56,6 +56,10 @@ export function getDomTag(dom) { return dom.nodeName.toLowerCase(); } +export function isInputElement(dom: Element): dom is HTMLInputElement { + return getDomTag(dom) === 'input'; +} + const types = ['button', 'input', 'select', 'textarea']; // button、input、select、textarea、如果有 autoFocus 属性需要focus diff --git a/libs/horizon/src/dom/validators/ValidateProps.ts b/libs/horizon/src/dom/validators/ValidateProps.ts index 5616f342..9dcf4a23 100644 --- a/libs/horizon/src/dom/validators/ValidateProps.ts +++ b/libs/horizon/src/dom/validators/ValidateProps.ts @@ -6,7 +6,7 @@ const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/; // 是内置元素 -export function isNativeElement(tagName: string, props: Object) { +export function isNativeElement(tagName: string, props: Record) { return !tagName.includes('-') && props.is === undefined; } diff --git a/libs/horizon/src/dom/valueHandler/InputValueHandler.ts b/libs/horizon/src/dom/valueHandler/InputValueHandler.ts index 368bf288..2ebe01a7 100644 --- a/libs/horizon/src/dom/valueHandler/InputValueHandler.ts +++ b/libs/horizon/src/dom/valueHandler/InputValueHandler.ts @@ -1,20 +1,21 @@ -import {updateCommonProp} from '../DOMPropertiesHandler/UpdateCommonProp'; -import {getVNodeProps} from '../DOMInternalKeys'; -import {IProperty} from '../utils/Interface'; -import {isInputValueChanged} from './ValueChangeHandler'; +import { updateCommonProp } from '../DOMPropertiesHandler/UpdateCommonProp'; +import { IProperty } from '../utils/Interface'; +import { isInputElement } from '../utils/Common'; +import { getVNodeProps } from '../DOMInternalKeys'; +import { updateInputValueIfChanged } from './ValueChangeHandler'; function getInitValue(dom: HTMLInputElement, properties: IProperty) { - const {value, defaultValue, checked, defaultChecked} = properties; + const { value, defaultValue, checked, defaultChecked } = properties; const defaultValueStr = defaultValue != null ? defaultValue : ''; const initValue = value != null ? value : defaultValueStr; const initChecked = checked != null ? checked : defaultChecked; - return {initValue, initChecked}; + return { initValue, initChecked }; } export function getInputPropsWithoutValue(dom: HTMLInputElement, properties: IProperty) { - // checked属于必填属性,无法置空 + // checked属于必填属性,无法置 let {checked} = properties; if (checked == null) { checked = getInitValue(dom, properties).initChecked; @@ -59,30 +60,26 @@ export function setInitInputValue(dom: HTMLInputElement, properties: IProperty) dom.defaultChecked = Boolean(initChecked); } -export function resetInputValue(dom: HTMLInputElement, properties: IProperty) { - const {name, type} = properties; - // 如果是 radio,先更新相同 name 的 radio - if (type === 'radio' && name != null) { - const radioList = document.querySelectorAll(`input[type="radio"][name="${name}"]`); +// 找出同一form内,name相同的Radio,更新它们Handler的Value +export function syncRadiosHandler(targetRadio: Element) { + if (isInputElement(targetRadio)) { + const props = getVNodeProps(targetRadio); + if (props) { + const { name, type } = props; + 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 === targetRadio) { + continue; + } + if (radio.form != null && targetRadio.form != null && radio.form !== targetRadio.form) { + continue; + } - for (let i = 0; i < radioList.length; i++) { - const radio = radioList[i]; - if (radio === dom) { - continue; + updateInputValueIfChanged(radio); + } } - // @ts-ignore - if (radio.form !== dom.form) { - continue; - } - - // @ts-ignore - const nonHorizonRadioProps = getVNodeProps(radio); - - isInputValueChanged(radio); - // @ts-ignore - updateInputValue(radio, nonHorizonRadioProps); } - } else { - updateInputValue(dom, properties); } } diff --git a/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts b/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts index 75812620..d7c5852f 100644 --- a/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts +++ b/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts @@ -43,7 +43,7 @@ export function getSelectPropsWithoutValue(dom: HorizonSelect, properties: Objec return { ...properties, value: undefined, - } + }; } export function updateSelectValue(dom: HorizonSelect, properties: IProperty, isInit: boolean = false) { diff --git a/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts b/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts index 34406ef8..207e8c2e 100644 --- a/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts +++ b/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts @@ -54,7 +54,7 @@ export function watchValueChange(dom) { } } -export function isInputValueChanged(dom) { +export function updateInputValueIfChanged(dom) { const handler = dom[HANDLER_KEY]; if (!handler) { return true; diff --git a/libs/horizon/src/dom/valueHandler/index.ts b/libs/horizon/src/dom/valueHandler/index.ts index 88a35e5c..5948a937 100644 --- a/libs/horizon/src/dom/valueHandler/index.ts +++ b/libs/horizon/src/dom/valueHandler/index.ts @@ -8,7 +8,6 @@ import { getInputPropsWithoutValue, setInitInputValue, updateInputValue, - resetInputValue, } from './InputValueHandler'; import { getOptionPropsWithoutValue, @@ -21,7 +20,6 @@ import { getTextareaPropsWithoutValue, updateTextareaValue, } from './TextareaValueHandler'; -import {getDomTag} from "../utils/Common"; // 获取元素除了被代理的值以外的属性 function getPropsWithoutValue(type: string, dom: HorizonDom, properties: IProperty) { @@ -73,26 +71,8 @@ function updateValue(type: string, dom: HorizonDom, properties: IProperty) { } } -function resetValue(dom: HorizonDom, properties: IProperty) { - const type = getDomTag(dom); - switch (type) { - case 'input': - resetInputValue(dom, properties); - break; - case 'select': - updateSelectValue(dom, properties); - break; - case 'textarea': - updateTextareaValue(dom, properties); - break; - default: - break; - } -} - export { getPropsWithoutValue, setInitValue, updateValue, - resetValue, }; diff --git a/libs/horizon/src/event/ControlledValueUpdater.ts b/libs/horizon/src/event/ControlledValueUpdater.ts deleted file mode 100644 index 5e565a1d..00000000 --- a/libs/horizon/src/event/ControlledValueUpdater.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {getVNodeProps} from '../dom/DOMInternalKeys'; -import {resetValue} from '../dom/valueHandler'; - -let updateList: Array | null = null; - -// 受控组件值重新赋值 -function updateValue(target: Element) { - const props = getVNodeProps(target); - if (props) { - resetValue(target, props); - } -} - -// 存储队列中缓存组件 -export function addValueUpdateList(target: EventTarget): void { - if (updateList) { - updateList.push(target); - } else { - updateList = [target]; - } -} - -// 判断是否需要重新赋值 -export function shouldUpdateValue(): boolean { - return updateList !== null && updateList.length > 0; -} - -// 从缓存队列中对受控组件进行赋值 -export function updateControlledValue() { - if (!updateList) { - return; - } - updateList.forEach(item => { - updateValue(item); - }); - updateList = null; -} diff --git a/libs/horizon/src/event/EventBinding.ts b/libs/horizon/src/event/EventBinding.ts index 54d3fdb4..5a38e45b 100644 --- a/libs/horizon/src/event/EventBinding.ts +++ b/libs/horizon/src/event/EventBinding.ts @@ -1,52 +1,41 @@ /** * 事件绑定实现,分为绑定委托事件和非委托事件 */ -import {allDelegatedNativeEvents} from './EventCollection'; -import {isDocument} from '../dom/utils/Common'; import { - getNearestVNode, - getNonDelegatedListenerMap, -} from '../dom/DOMInternalKeys'; -import {runDiscreteUpdates} from '../renderer/TreeBuilder'; -import {isMounted} from '../renderer/vnode/VNodeUtils'; -import {SuspenseComponent} from '../renderer/vnode/VNodeTags'; -import {handleEventMain} from './HorizonEventMain'; -import {decorateNativeEvent} from './customEvents/EventFactory'; + allDelegatedHorizonEvents, + allDelegatedNativeEvents, +} from './EventHub'; +import { isDocument } from '../dom/utils/Common'; +import { getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys'; +import { runDiscreteUpdates } from '../renderer/TreeBuilder'; +import { handleEventMain } from './HorizonEventMain'; +import { decorateNativeEvent } from './EventWrapper'; +import { VNode } from '../renderer/vnode/VNode'; -const listeningMarker = '_horizonListening' + Math.random().toString(36).slice(4); +const listeningMarker = + '_horizonListening' + + Math.random() + .toString(36) + .slice(4); // 触发委托事件 function triggerDelegatedEvent( nativeEvtName: string, isCapture: boolean, targetDom: EventTarget, - nativeEvent, // 事件对象event + nativeEvent // 事件对象event ) { // 执行之前的调度事件 runDiscreteUpdates(); const nativeEventTarget = nativeEvent.target || nativeEvent.srcElement; - let targetVNode = getNearestVNode(nativeEventTarget); + const targetVNode = getNearestVNode(nativeEventTarget); - if (targetVNode !== null) { - if (isMounted(targetVNode)) { - if (targetVNode.tag === SuspenseComponent) { - targetVNode = null; - } - } else { - // vNode已销毁 - targetVNode = null; - } - } handleEventMain(nativeEvtName, isCapture, nativeEvent, targetVNode, targetDom); } // 监听委托事件 -function listenToNativeEvent( - nativeEvtName: string, - delegatedElement: Element, - isCapture: boolean, -): void { +function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, isCapture: boolean): void { let dom: Element | Document = delegatedElement; // document层次可能触发selectionchange事件,为了捕获这类事件,selectionchange事件绑定在document节点上 if (nativeEvtName === 'selectionchange' && !isDocument(delegatedElement)) { @@ -73,6 +62,22 @@ export function listenDelegatedEvents(dom: Element) { }); } +// 事件懒委托,当用户定义事件后,再进行委托到根节点 +export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) { + currentRoot.delegatedEvents.add(eventName); + + const isCapture = isCaptureEvent(eventName); + const nativeEvents = allDelegatedHorizonEvents.get(eventName); + + nativeEvents.forEach(nativeEvent => { + const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent; + if (!currentRoot.delegatedNativeEvents.has(nativeFullName)) { + listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture); + currentRoot.delegatedNativeEvents.add(nativeFullName); + } + }); +} + // 通过horizon事件名获取到native事件名 function getNativeEvtName(horizonEventName, capture) { let nativeName; @@ -104,11 +109,7 @@ function getWrapperListener(horizonEventName, nativeEvtName, targetElement, list } // 非委托事件单独监听到各自dom节点 -export function listenNonDelegatedEvent( - horizonEventName: string, - domElement: Element, - listener, -): void { +export function listenNonDelegatedEvent(horizonEventName: string, domElement: Element, listener): void { const isCapture = isCaptureEvent(horizonEventName); const nativeEvtName = getNativeEvtName(horizonEventName, isCapture); diff --git a/libs/horizon/src/event/EventCollection.ts b/libs/horizon/src/event/EventCollection.ts deleted file mode 100644 index d70dcf10..00000000 --- a/libs/horizon/src/event/EventCollection.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {horizonEventToNativeMap} from './const'; - -// 需要委托的horizon事件和原生事件对应关系 -export const allDelegatedHorizonEvents = new Map(); -// 所有委托的原生事件集合 -export const allDelegatedNativeEvents = new Set(); - -horizonEventToNativeMap.forEach((dependencies, horizonEvent) => { - allDelegatedHorizonEvents.set(horizonEvent, dependencies); - allDelegatedHorizonEvents.set(horizonEvent + 'Capture', dependencies); - - dependencies.forEach(d => { - allDelegatedNativeEvents.add(d); - }); -}); diff --git a/libs/horizon/src/event/const.ts b/libs/horizon/src/event/EventHub.ts similarity index 72% rename from libs/horizon/src/event/const.ts rename to libs/horizon/src/event/EventHub.ts index 449663e9..944112f2 100644 --- a/libs/horizon/src/event/const.ts +++ b/libs/horizon/src/event/EventHub.ts @@ -1,3 +1,7 @@ +// 需要委托的horizon事件和原生事件对应关系 +export const allDelegatedHorizonEvents = new Map(); +// 所有委托的原生事件集合 +export const allDelegatedNativeEvents = new Set(); // Horizon事件和原生事件对应关系 export const horizonEventToNativeMap = new Map([ @@ -28,16 +32,14 @@ export const horizonEventToNativeMap = new Map([ ['onCompositionStart', ['compositionstart']], ['onCompositionUpdate', ['compositionupdate']], ['onChange', ['change', 'click', 'focusout', 'input']], - ['onSelect', ['focusout', 'contextmenu', 'dragend', 'focusin', - 'keydown', 'keyup', 'mousedown', 'mouseup', 'selectionchange']], + ['onSelect', ['select']], ['onAnimationEnd', ['animationend']], ['onAnimationIteration', ['animationiteration']], ['onAnimationStart', ['animationstart']], - ['onTransitionEnd', ['transitionend']] + ['onTransitionEnd', ['transitionend']], ]); - -export const CommonEventToHorizonMap = { +export const NativeEventToHorizonMap = { click: 'click', dblclick: 'doubleClick', contextmenu: 'contextMenu', @@ -45,6 +47,7 @@ export const CommonEventToHorizonMap = { focusin: 'focus', focusout: 'blur', input: 'input', + select: 'select', keydown: 'keyDown', keypress: 'keyPress', keyup: 'keyUp', @@ -69,11 +72,22 @@ export const CommonEventToHorizonMap = { compositionend: 'compositionEnd', compositionupdate: 'compositionUpdate', }; - export const CHAR_CODE_SPACE = 32; - - export const EVENT_TYPE_BUBBLE = 'Bubble'; export const EVENT_TYPE_CAPTURE = 'Capture'; export const EVENT_TYPE_ALL = 'All'; +horizonEventToNativeMap.forEach((dependencies, horizonEvent) => { + allDelegatedHorizonEvents.set(horizonEvent, dependencies); + allDelegatedHorizonEvents.set(horizonEvent + 'Capture', dependencies); + + dependencies.forEach(d => { + allDelegatedNativeEvents.add(d); + }); +}); + +export function transformToHorizonEvent(nativeEvtName: string) { + const name = NativeEventToHorizonMap[nativeEvtName]; + // 例:dragEnd -> onDragEnd + return !name ? '' : `on${name[0].toUpperCase()}${name.slice(1)}`; +} diff --git a/libs/horizon/src/event/customEvents/EventFactory.ts b/libs/horizon/src/event/EventWrapper.ts similarity index 100% rename from libs/horizon/src/event/customEvents/EventFactory.ts rename to libs/horizon/src/event/EventWrapper.ts diff --git a/libs/horizon/src/event/HorizonEventMain.ts b/libs/horizon/src/event/HorizonEventMain.ts index 08e9f5d0..ebaadeb8 100644 --- a/libs/horizon/src/event/HorizonEventMain.ts +++ b/libs/horizon/src/event/HorizonEventMain.ts @@ -1,23 +1,75 @@ -import type { AnyNativeEvent } from './Types'; +import { AnyNativeEvent, ListenerUnitList } from './Types'; import type { VNode } from '../renderer/Types'; - +import { isInputElement, setPropertyWritable } from './utils'; +import { decorateNativeEvent } from './EventWrapper'; +import { getListenersFromTree } from './ListenerGetter'; +import { asyncUpdates, runDiscreteUpdates } from '../renderer/Renderer'; +import { findRoot } from '../renderer/vnode/VNodeUtils'; +import { syncRadiosHandler } from '../dom/valueHandler/InputValueHandler'; import { - CommonEventToHorizonMap, - horizonEventToNativeMap, + EVENT_TYPE_ALL, EVENT_TYPE_BUBBLE, EVENT_TYPE_CAPTURE, -} from './const'; -import { getListeners as getChangeListeners } from './simulatedEvtHandler/ChangeEventHandler'; -import { getListeners as getSelectionListeners } from './simulatedEvtHandler/SelectionEventHandler'; -import { - setPropertyWritable, -} from './utils'; -import { decorateNativeEvent } from './customEvents/EventFactory'; -import { getListenersFromTree } from './ListenerGetter'; -import { shouldUpdateValue, updateControlledValue } from './ControlledValueUpdater'; -import { asyncUpdates, runDiscreteUpdates } from '../renderer/Renderer'; -import { getExactNode } from '../renderer/vnode/VNodeUtils'; -import {ListenerUnitList} from './Types'; + horizonEventToNativeMap, + transformToHorizonEvent, +} from './EventHub'; +import { getDomTag } from '../dom/utils/Common'; +import { updateInputValueIfChanged } from '../dom/valueHandler/ValueChangeHandler'; +import { getDom } from '../dom/DOMInternalKeys'; + +// web规范,鼠标右键key值 +const RIGHT_MOUSE_BUTTON = 2; + +// 返回是否需要触发change事件标记 +// | 元素 | 事件 | 需要值变更 | +// | --- | --- | --------------- | +// | | click | YES | +// | | input / change | YES | +function shouldTriggerChangeEvent(targetDom, evtName) { + const { type } = targetDom; + const domTag = getDomTag(targetDom); + + if (domTag === 'select' || (domTag === 'input' && type === 'file')) { + return evtName === 'change'; + } else if (domTag === 'input' && (type === 'checkbox' || type === 'radio')) { + if (evtName === 'click') { + return updateInputValueIfChanged(targetDom); + } + } else if (isInputElement(targetDom)) { + if (evtName === 'input' || evtName === 'change') { + return updateInputValueIfChanged(targetDom); + } + } + return false; +} + +/** + * + * 支持input/textarea/select的onChange事件 + */ +function getChangeListeners( + nativeEvtName: string, + nativeEvt: AnyNativeEvent, + vNode: null | VNode, +): ListenerUnitList { + if (!vNode) { + return []; + } + const targetDom = getDom(vNode); + + // 判断是否需要触发change事件 + if (shouldTriggerChangeEvent(targetDom, nativeEvtName)) { + const event = decorateNativeEvent( + 'onChange', + 'change', + nativeEvt, + ); + return getListenersFromTree(vNode, 'onChange', event, EVENT_TYPE_ALL); + } + + return []; +} // 获取事件触发的普通事件监听方法队列 function getCommonListeners( @@ -27,15 +79,14 @@ function getCommonListeners( target: null | EventTarget, isCapture: boolean, ): ListenerUnitList { - const name = CommonEventToHorizonMap[nativeEvtName]; - const horizonEvtName = !name ? '' : `on${name[0].toUpperCase()}${name.slice(1)}`; // 例:dragEnd -> onDragEnd - + const horizonEvtName = transformToHorizonEvent(nativeEvtName); + if (!horizonEvtName) { return []; } // 鼠标点击右键 - if (nativeEvent instanceof MouseEvent && nativeEvtName === 'click' && nativeEvent.button === 2) { + if (nativeEvent instanceof MouseEvent && nativeEvtName === 'click' && nativeEvent.button === RIGHT_MOUSE_BUTTON) { return []; } @@ -71,13 +122,16 @@ function processListeners(listenerList: ListenerUnitList): void { }); } -function getProcessListeners( +// 触发可以被执行的horizon事件监听 +function triggerHorizonEvents( nativeEvtName: string, - vNode: VNode | null, + isCapture: boolean, nativeEvent: AnyNativeEvent, - target, - isCapture: boolean -): ListenerUnitList { + vNode: VNode | null, +) { + const target = nativeEvent.target || nativeEvent.srcElement; + let hasTriggeredChangeEvent = false; + // 触发普通委托事件 let listenerList: ListenerUnitList = getCommonListeners( nativeEvtName, @@ -88,42 +142,22 @@ function getProcessListeners( ); // 触发特殊handler委托事件 - if (!isCapture) { - if (horizonEventToNativeMap.get('onChange').includes(nativeEvtName)) { - listenerList = listenerList.concat(getChangeListeners( - nativeEvtName, - nativeEvent, - vNode, - target, - )); - } - - if (horizonEventToNativeMap.get('onSelect').includes(nativeEvtName)) { - listenerList = listenerList.concat(getSelectionListeners( - nativeEvtName, - nativeEvent, - vNode, - target, - )); + if (!isCapture && horizonEventToNativeMap.get('onChange')!.includes(nativeEvtName)) { + const changeListeners = getChangeListeners( + nativeEvtName, + nativeEvent, + vNode, + ); + if (changeListeners.length) { + hasTriggeredChangeEvent = true; + listenerList = listenerList.concat(changeListeners); } } - return listenerList; -} - -// 触发可以被执行的horizon事件监听 -function triggerHorizonEvents( - nativeEvtName: string, - isCapture: boolean, - nativeEvent: AnyNativeEvent, - vNode: VNode | null, -): void { - const nativeEventTarget = nativeEvent.target || nativeEvent.srcElement; - - // 获取委托事件队列 - const listenerList = getProcessListeners(nativeEvtName, vNode, nativeEvent, nativeEventTarget, isCapture); // 处理触发的事件队列 processListeners(listenerList); + + return hasTriggeredChangeEvent; } @@ -136,11 +170,11 @@ export function handleEventMain( isCapture: boolean, nativeEvent: AnyNativeEvent, vNode: null | VNode, - targetContainer: EventTarget, + targetDom: EventTarget, ): void { let startVNode = vNode; if (startVNode !== null) { - startVNode = getExactNode(startVNode, targetContainer); + startVNode = findRoot(startVNode, targetDom); if (!startVNode) { return; } @@ -154,13 +188,15 @@ export function handleEventMain( // 没有事件在执行,经过调度再执行事件 isInEventsExecution = true; + let hasTriggeredChangeEvent = false; try { - asyncUpdates(() => triggerHorizonEvents(nativeEvtName, isCapture, nativeEvent, startVNode)); + hasTriggeredChangeEvent = asyncUpdates(() => triggerHorizonEvents(nativeEvtName, isCapture, nativeEvent, startVNode)); } finally { isInEventsExecution = false; - if (shouldUpdateValue()) { + if (hasTriggeredChangeEvent) { runDiscreteUpdates(); - updateControlledValue(); + // 若是Radio,同步同组其他Radio的Handler Value + syncRadiosHandler(nativeEvent.target as Element); } } } diff --git a/libs/horizon/src/event/ListenerGetter.ts b/libs/horizon/src/event/ListenerGetter.ts index d6860f35..731368ed 100644 --- a/libs/horizon/src/event/ListenerGetter.ts +++ b/libs/horizon/src/event/ListenerGetter.ts @@ -1,7 +1,7 @@ import { VNode } from '../renderer/Types'; import { DomComponent } from '../renderer/vnode/VNodeTags'; -import { EVENT_TYPE_ALL, EVENT_TYPE_CAPTURE, EVENT_TYPE_BUBBLE } from './const'; import { AnyNativeEvent, ListenerUnitList } from './Types'; +import { EVENT_TYPE_ALL, EVENT_TYPE_BUBBLE, EVENT_TYPE_CAPTURE } from './EventHub'; // 从vnode属性中获取事件listener function getListenerFromVNode(vNode: VNode, eventName: string): Function | null { diff --git a/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts b/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts deleted file mode 100644 index c0ad947d..00000000 --- a/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {decorateNativeEvent} from '../customEvents/EventFactory'; -import {getDom} from '../../dom/DOMInternalKeys'; -import {isInputValueChanged} from '../../dom/valueHandler/ValueChangeHandler'; -import {addValueUpdateList} from '../ControlledValueUpdater'; -import {isInputElement} from '../utils'; -import {EVENT_TYPE_ALL} from '../const'; -import {AnyNativeEvent, ListenerUnitList} from '../Types'; -import { - getListenersFromTree, -} from '../ListenerGetter'; -import {VNode} from '../../renderer/Types'; -import {getDomTag} from '../../dom/utils/Common'; - -// 返回是否需要触发change事件标记 -function shouldTriggerChangeEvent(targetDom, evtName) { - const { type } = targetDom; - const domTag = getDomTag(targetDom); - - if (domTag === 'select' || (domTag === 'input' && type === 'file')) { - return evtName === 'change'; - } else if (domTag === 'input' && (type === 'checkbox' || type === 'radio')) { - if (evtName === 'click') { - return isInputValueChanged(targetDom); - } - } else if (isInputElement(targetDom)) { - if (evtName === 'input' || evtName === 'change') { - return isInputValueChanged(targetDom); - } - } - return false; -} - -/** - * - * 支持input/textarea/select的onChange事件 - */ -export function getListeners( - nativeEvtName: string, - nativeEvt: AnyNativeEvent, - vNode: null | VNode, - target: null | EventTarget, -): ListenerUnitList { - if (!vNode) { - return []; - } - const targetDom = getDom(vNode); - - // 判断是否需要触发change事件 - if (shouldTriggerChangeEvent(targetDom, nativeEvtName)) { - addValueUpdateList(target); - const event = decorateNativeEvent( - 'onChange', - 'change', - nativeEvt, - ); - return getListenersFromTree(vNode, 'onChange', event, EVENT_TYPE_ALL); - } - - return []; -} diff --git a/libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts b/libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts deleted file mode 100644 index cd5a09ad..00000000 --- a/libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts +++ /dev/null @@ -1,112 +0,0 @@ -import {decorateNativeEvent} from '../customEvents/EventFactory'; -import {shallowCompare} from '../../renderer/utils/compare'; -import {getFocusedDom} from '../../dom/utils/Common'; -import {getDom} from '../../dom/DOMInternalKeys'; -import {isDocument} from '../../dom/utils/Common'; -import {isInputElement, setPropertyWritable} from '../utils'; -import type {AnyNativeEvent} from '../Types'; -import {getListenersFromTree} from '../ListenerGetter'; -import type {VNode} from '../../renderer/Types'; -import {EVENT_TYPE_ALL} from '../const'; -import {ListenerUnitList} from '../Types'; - -const horizonEventName = 'onSelect'; - -let currentElement = null; -let currentVNode = null; -let lastSelection: Selection | null = null; - -function initTargetCache(dom, vNode) { - if (isInputElement(dom) || dom.contentEditable === 'true') { - currentElement = dom; - currentVNode = vNode; - lastSelection = null; - } -} - -function clearTargetCache() { - currentElement = null; - currentVNode = null; - lastSelection = null; -} - -// 标记是否在鼠标事件过程中 -let isInMouseEvent = false; - -// 获取节点所在的document对象 -function getDocument(eventTarget) { - if (eventTarget.window === eventTarget) { - return eventTarget.document; - } - if (isDocument(eventTarget)) { - return eventTarget; - } - return eventTarget.ownerDocument; -} - -function getSelectEvent(nativeEvent, target) { - const doc = getDocument(target); - if (isInMouseEvent || currentElement == null || currentElement !== getFocusedDom(doc)) { - return []; - } - - const currentSelection = window.getSelection(); - if (!shallowCompare(lastSelection, currentSelection)) { - lastSelection = currentSelection; - - const event = decorateNativeEvent( - horizonEventName, - 'select', - nativeEvent, - ); - setPropertyWritable(nativeEvent, 'target'); - event.target = currentElement; - - return getListenersFromTree( - currentVNode, - horizonEventName, - event, - EVENT_TYPE_ALL - ); - } - return []; -} - - -/** - * 该插件创建一个onSelect事件 - * 支持元素: input、textarea、contentEditable元素 - * 触发场景:用户输入、折叠选择、文本选择 - */ -export function getListeners( - nativeEvtName: string, - nativeEvt: AnyNativeEvent, - vNode: null | VNode, - target: null | EventTarget, -): ListenerUnitList { - const targetNode = vNode ? getDom(vNode) : window; - let eventUnitList: ListenerUnitList = []; - switch (nativeEvtName) { - case 'focusin': - initTargetCache(targetNode, vNode); - break; - case 'focusout': - clearTargetCache(); - break; - case 'mousedown': - isInMouseEvent = true; - break; - case 'contextmenu': - case 'mouseup': - case 'dragend': - isInMouseEvent = false; - eventUnitList = getSelectEvent(nativeEvt, target); - break; - case 'selectionchange': - case 'keydown': - case 'keyup': - eventUnitList = getSelectEvent(nativeEvt, target); - } - - return eventUnitList; -} diff --git a/libs/horizon/src/event/utils.ts b/libs/horizon/src/event/utils.ts index 09d1b240..37ec6218 100644 --- a/libs/horizon/src/event/utils.ts +++ b/libs/horizon/src/event/utils.ts @@ -1,9 +1,7 @@ export function isInputElement(dom?: HTMLElement): boolean { - if (dom instanceof HTMLInputElement || dom instanceof HTMLTextAreaElement) { - return true; - } - return false; + return dom instanceof HTMLInputElement || dom instanceof HTMLTextAreaElement; + } export function setPropertyWritable(obj, propName) { diff --git a/libs/horizon/src/horizonx/CommonUtils.js b/libs/horizon/src/horizonx/CommonUtils.js new file mode 100644 index 00000000..85a328b4 --- /dev/null +++ b/libs/horizon/src/horizonx/CommonUtils.js @@ -0,0 +1,52 @@ +export function isObject(obj) { + const type = typeof obj; + return obj != null && (type === 'object' || type === 'function'); +} + +export function isSet(obj) { + return obj != null && (Object.prototype.toString.call(obj) === '[object Set]' || obj.constructor === Set); +} + +export function isWeakSet(obj) { + return obj != null && (Object.prototype.toString.call(obj) === '[object WeakSet]' || obj.constructor === WeakSet); +} + +export function isMap(obj) { + return obj != null && (Object.prototype.toString.call(obj) === '[object Map]' || obj.constructor === Map); +} + +export function isWeakMap(obj) { + return obj != null && (Object.prototype.toString.call(obj) === '[object WeakMap]' || obj.constructor === WeakMap); +} + +export function isArray(obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; +} + +export function isCollection(obj) { + return isSet(obj) || isWeakSet(obj) || isMap(obj) || isWeakMap(obj); +} + +export function isString(obj) { + return typeof obj === 'string'; +} + +export function isValidIntegerKey(key) { + return isString(key) && key !== 'NaN' && key[0] !== '-' && String(parseInt(key, 10)) === key; +} + +export const noop = () => {}; + +export function isSame(x, y) { + if (!(typeof Object.is === 'function')) { + if (x === y) { + // +0 != -0 + return x !== 0 || 1 / x === 1 / y; + } else { + // NaN == NaN + return x !== x && y !== y; + } + } else { + return Object.is(x, y); + } +} diff --git a/libs/horizon/src/horizonx/Constants.ts b/libs/horizon/src/horizonx/Constants.ts new file mode 100644 index 00000000..5967d853 --- /dev/null +++ b/libs/horizon/src/horizonx/Constants.ts @@ -0,0 +1,3 @@ +// The two constants must be the same as those in horizon. +export const FunctionComponent = 'FunctionComponent'; +export const ClassComponent = 'ClassComponent'; diff --git a/libs/horizon/src/horizonx/adapters/redux.ts b/libs/horizon/src/horizonx/adapters/redux.ts new file mode 100644 index 00000000..293700b6 --- /dev/null +++ b/libs/horizon/src/horizonx/adapters/redux.ts @@ -0,0 +1,124 @@ +import { createStore as createStoreX } from '../store/StoreHandler'; + +import { ReduxStoreHandler, ReduxAction, ReduxMiddleware } from '../types'; + +export { thunk } from './reduxThunk'; + +export { Provider, useSelector, useStore, useDispatch, connect, createSelectorHook, createDispatchHook } from './reduxReact'; + +type Reducer = (state: any, action: ReduxAction) => any; + +export function createStore(reducer: Reducer, preloadedState: any, enhancers): ReduxStoreHandler { + const store = createStoreX({ + id: 'defaultStore', + state: { stateWrapper: preloadedState }, + actions: { + dispatch: (state: { stateWrapper?: any }, action) => { + let result; + if (state.stateWrapper !== undefined && state.stateWrapper !== null) { + result = reducer(state.stateWrapper, action); + } else { + result = reducer(undefined, action); + } + + if (result === undefined) { + return; + } // NOTE: reducer should never return undefined, in this case, do not change state + state.stateWrapper = result; + }, + }, + options: { + suppressHooks: true, + }, + })(); + + const result = { + reducer, + getState: function() { + return store.$state.stateWrapper; + }, + subscribe: listener => { + store.$subscribe(listener); + + return () => { + store.$unsubscribe(listener); + }; + }, + replaceReducer: newReducer => { + reducer = newReducer; + }, + _horizonXstore: store, + dispatch: store.$actions.dispatch, + }; + + enhancers && enhancers(result); + + result.dispatch({ type: 'HorizonX' }); + + store.reduxHandler = result; + + return result; +} + +export function combineReducers(reducers: { [key: string]: Reducer }): Reducer { + return (state = {}, action) => { + const newState = {}; + Object.entries(reducers).forEach(([key, reducer]) => { + newState[key] = reducer(state[key], action); + }); + return newState; + }; +} + +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(); + let dispatch = store.dispatch; + middlewares.forEach(middleware => { + dispatch = middleware(store)(dispatch); + }); + store.dispatch = dispatch; +} + +type ActionCreator = (...params: any[]) => ReduxAction; +type ActionCreators = { [key: string]: ActionCreator }; +export type BoundActionCreator = (...params: any[]) => void; +type BoundActionCreators = { [key: string]: BoundActionCreator }; +type Dispatch = (action) => any; + +export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators { + const boundActionCreators = {}; + Object.entries(actionCreators).forEach(([key, value]) => { + boundActionCreators[key] = (...args) => { + dispatch(value(...args)); + }; + }); + + return boundActionCreators; +} + +export function compose(middlewares: ReduxMiddleware[]) { + return (store: ReduxStoreHandler, extraArgument: any) => { + let val; + middlewares.reverse().forEach((middleware: ReduxMiddleware, index) => { + if (!index) { + val = middleware(store, extraArgument); + return; + } + val = middleware(val); + }); + return val; + }; +} + + +// HorizonX batches updates by default, this function is only for backwards compatibility +export function batch(fn: () => void) { + fn(); +} diff --git a/libs/horizon/src/event/WrapperListener.ts b/libs/horizon/src/horizonx/adapters/reduxPromiseMiddleware.ts similarity index 100% rename from libs/horizon/src/event/WrapperListener.ts rename to libs/horizon/src/horizonx/adapters/reduxPromiseMiddleware.ts diff --git a/libs/horizon/src/horizonx/adapters/reduxReact.ts b/libs/horizon/src/horizonx/adapters/reduxReact.ts new file mode 100644 index 00000000..da99ab52 --- /dev/null +++ b/libs/horizon/src/horizonx/adapters/reduxReact.ts @@ -0,0 +1,166 @@ +// @ts-ignore +import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/HookExternal'; +import { createContext } from '../../renderer/components/context/CreateContext'; +import { createElement } from '../../external/JSXElement'; +import { BoundActionCreator } from './redux'; +import { ReduxAction, ReduxStoreHandler } from '../types'; + +const DefaultContext = createContext(); +type Context = typeof DefaultContext; + +export function Provider({ + store, + context = DefaultContext, + children, +}: { + store: ReduxStoreHandler; + context: Context; + children?: any[]; +}) { + const Context = context; // NOTE: bind redux API to horizon API requires this renaming; + return createElement(Context.Provider, { value: store }, children); +} + +export function createStoreHook(context: Context) { + return () => { + return useContext(context); + }; +} + +export function createSelectorHook(context: Context): (selector: (any) => any) => any { + const store = createStoreHook(context)(); + return function(selector = state => state) { + const [b, fr] = useState(false); + + const listener = () => { + fr(!b); + }; + + useEffect(() => { + const unsubscribe = store.subscribe(listener); + + return () => { + unsubscribe(listener); + }; + }); + + return selector(store.getState()); + }; +} + +export function createDispatchHook(context: Context): BoundActionCreator { + const store = createStoreHook(context)(); + return function() { + return action => { + this.dispatch(action); + }; + }.bind(store); +} + +export const useSelector = selector => { + return createSelectorHook(DefaultContext)(selector); +}; + +export const useDispatch = () => { + return createDispatchHook(DefaultContext)(); +}; + +export const useStore = () => { + return createStoreHook(DefaultContext)(); +}; + +// function shallowCompare(a,b){ +// return Object.keys(a).length === Object.keys(b).length && +// Object.keys(a).every(key => a[key] === b[key]); +// } + +//TODO: implement options +// context?: Object, +// areStatesEqual?: Function, :) +// areOwnPropsEqual?: Function, +// areStatePropsEqual?: Function, +// areMergedPropsEqual?: Function, +// forwardRef?: boolean, +// const defaultOptions = { +// areStatesEqual: shallowCompare, +// areOwnPropsEqual: shallowCompare, +// areStatePropsEqual: shallowCompare, +// areMergedPropsEqual: shallowCompare +// }; + +export function connect( + mapStateToProps?: (state: any, ownProps: { [key: string]: any }) => Object, + mapDispatchToProps?: + | { [key: string]: (...args: any[]) => ReduxAction } + | ((dispatch: (action: ReduxAction) => any, ownProps?: Object) => Object), + mergeProps?: (stateProps: Object, dispatchProps: Object, ownProps: Object) => Object, + options?: { + areStatesEqual?: (oldState: any, newState: any) => boolean; + context?: any; // TODO: type this + } +) { + if (!options) { + options = {}; + } + + return Component => { + const useStore = createStoreHook(options.context || DefaultContext); + + function Wrapper(props) { + const [f, forceReload] = useState(true); + + const store = useStore(); + + useEffect(() => { + const unsubscribe = store.subscribe(() => forceReload(!f)); + () => { + unsubscribe(() => forceReload(!f)); + }; + }); + + const previous = useRef({ + state: {}, + }); + + let mappedState; + if (options.areStatesEqual) { + if (options.areStatesEqual(previous.current.state, store.getState())) { + mappedState = previous.current.mappedState; + } else { + mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {}; + previous.current.mappedState = mappedState; + } + } else { + mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {}; + previous.current.mappedState = mappedState; + } + let mappedDispatch: { dispatch?: (action) => void } = {}; + if (mapDispatchToProps) { + if (typeof mapDispatchToProps === 'object') { + Object.entries(mapDispatchToProps).forEach(([key, value]) => { + mappedDispatch[key] = (...args) => { + store.dispatch(value(...args)); + }; + }); + } else { + mappedDispatch = mapDispatchToProps(store.dispatch, props); + } + } else { + mappedDispatch.dispatch = store.dispatch; + } + const mergedProps = ( + mergeProps || + ((state, dispatch, originalProps) => { + return { ...state, ...dispatch, ...originalProps }; + }) + )(mappedState, mappedDispatch, props); + + previous.current.state = store.getState(); + + const node = createElement(Component, mergedProps); + return node; + } + + return Wrapper; + }; +} diff --git a/libs/horizon/src/horizonx/adapters/reduxThunk.ts b/libs/horizon/src/horizonx/adapters/reduxThunk.ts new file mode 100644 index 00000000..850f1b1c --- /dev/null +++ b/libs/horizon/src/horizonx/adapters/reduxThunk.ts @@ -0,0 +1,22 @@ +import { ReduxStoreHandler, ReduxAction, ReduxMiddleware } from '../types'; + +function createThunkMiddleware(extraArgument?: any): ReduxMiddleware { + return (store: ReduxStoreHandler) => (next: (action: ReduxAction) => any) => ( + action: + | ReduxAction + | ((dispatch: (action: ReduxAction) => void, store: ReduxStoreHandler, extraArgument?: any) => any) + ) => { + // This gets called for every action you dispatch. + // If it's a function, call it. + if (typeof action === 'function') { + return action(store.dispatch, store.getState.bind(store), extraArgument); + } + + // Otherwise, just continue processing this action as usual + return next(action); + }; +} + +export const thunk = createThunkMiddleware(); +// @ts-ignore +thunk.withExtraArgument = createThunkMiddleware; diff --git a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts new file mode 100644 index 00000000..9ea8937f --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts @@ -0,0 +1,45 @@ +// TODO: implement vNode type + +import {IObserver} from '../types'; + +/** + * 一个对象(对象、数组、集合)对应一个Observer + * + */ +export class HooklessObserver implements IObserver { + + listeners:(() => void)[] = []; + + useProp(key: string): void { + } + + addListener(listener: () => void) { + this.listeners.push(listener); + } + + removeListener(listener: () => void) { + this.listeners = this.listeners.filter(item => item != listener); + } + + setProp(key: string): void { + this.triggerChangeListeners(); + } + + triggerChangeListeners(): void { + this.listeners.forEach(listener => { + if (!listener) { + return; + } + listener(); + }); + } + + triggerUpdate(vNode): void { + } + + allChange(): void { + } + + clearByVNode(vNode): void { + } +} diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts new file mode 100644 index 00000000..be24d1c2 --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -0,0 +1,102 @@ +/** + * 一个对象(对象、数组、集合)对应一个Observer + */ + +//@ts-ignore +import { launchUpdateFromVNode } from '../../renderer/TreeBuilder'; +import { getProcessingVNode } from '../../renderer/GlobalVar'; +import { VNode } from '../../renderer/vnode/VNode'; +import { IObserver } from '../types'; + +export class Observer implements IObserver { + + vNodeKeys = new WeakMap(); + + keyVNodes = new Map(); + + listeners:(()=>void)[] = []; + + useProp(key: string): void { + const processingVNode = getProcessingVNode(); + if (processingVNode === null || !processingVNode.observers) { + return; + } + + // vNode -> Observers + processingVNode.observers.add(this); + + // key -> vNodes + let vNodes = this.keyVNodes.get(key); + if (!vNodes) { + vNodes = new Set(); + this.keyVNodes.set(key, vNodes); + } + vNodes.add(processingVNode); + + // vNode -> keys + let keys = this.vNodeKeys.get(processingVNode); + if (!keys) { + keys = new Set(); + this.vNodeKeys.set(processingVNode, keys); + } + keys.add(key); + } + + addListener(listener: () => void): void { + this.listeners.push(listener); + } + + removeListener(listener: () => void): void { + this.listeners = this.listeners.filter(item => item != listener); + } + + setProp(key: string): void { + const vNodes = this.keyVNodes.get(key); + vNodes?.forEach((vNode: VNode) => { + if (vNode.isStoreChange) { + // update already triggered + return; + } + vNode.isStoreChange = true; + + // 触发vNode更新 + this.triggerUpdate(vNode); + }); + this.triggerChangeListeners(); + } + + triggerChangeListeners(): void { + this.listeners.forEach(listener => listener()); + } + + triggerUpdate(vNode: VNode): void { + if (!vNode) { + return; + } + launchUpdateFromVNode(vNode); + } + + allChange(): void { + let keyIt = this.keyVNodes.keys(); + let keyItem = keyIt.next(); + while (!keyItem.done) { + this.setProp(keyItem.value); + keyItem = keyIt.next(); + } + } + + clearByVNode(vNode: Vnode): void { + const keys = this.vNodeKeys.get(vNode); + if (keys) { + keys.forEach((key: any) => { + const vNodes = this.keyVNodes.get(key); + vNodes.delete(vNode); + if (vNodes.size === 0) { + this.keyVNodes.delete(key); + } + }); + } + + this.vNodeKeys.delete(vNode); + } +} diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts new file mode 100644 index 00000000..f2051b9d --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -0,0 +1,62 @@ +import {createObjectProxy} from './handlers/ObjectProxyHandler'; +import {Observer} from './Observer'; +import {HooklessObserver} from './HooklessObserver'; +import {isArray, isCollection, isObject} from '../CommonUtils'; +import {createArrayProxy} from './handlers/ArrayProxyHandler'; +import {createCollectionProxy} from './handlers/CollectionProxyHandler'; +import { IObserver } from '../types'; + +const OBSERVER_KEY = Symbol('_horizonObserver'); + +const proxyMap = new WeakMap(); + +export const hookObserverMap = new WeakMap(); + +export function createProxy(rawObj: any, hookObserver = true): any { + // 不是对象(是原始数据类型)不用代理 + if (!isObject(rawObj)) { + return rawObj; + } + + const existProxy = proxyMap.get(rawObj); + if (existProxy) { + return existProxy; + } + + // Observer不需要代理 + if (rawObj instanceof Observer) { + return rawObj; + } + + // 创建Observer + let observer:IObserver = getObserver(rawObj); + if (!observer) { + observer = hookObserver ? new Observer() : new HooklessObserver(); + rawObj[OBSERVER_KEY] = observer; + } + + hookObserverMap.set(rawObj, hookObserver); + + // 创建Proxy + let proxyObj; + if (isArray(rawObj)) { + // 数组 + proxyObj = createArrayProxy(rawObj as []); + } else if (isCollection(rawObj)) { + // 集合 + proxyObj = createCollectionProxy(rawObj); + } else { + // 原生对象 或 函数 + proxyObj = createObjectProxy(rawObj); + } + + proxyMap.set(rawObj, proxyObj); + proxyMap.set(proxyObj, proxyObj); + + return proxyObj; +} + +export function getObserver(rawObj: any): Observer { + return rawObj[OBSERVER_KEY]; +} + diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts new file mode 100644 index 00000000..879dc088 --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -0,0 +1,40 @@ +import { getObserver } from '../ProxyHandler'; +import { isSame, isValidIntegerKey } from '../../CommonUtils'; +import { get as objectGet } from './ObjectProxyHandler'; + +export function createArrayProxy(rawObj: any[]): any[] { + const handle = { + get, + set, + }; + + return new Proxy(rawObj, handle); +} + +function get(rawObj: any[], key: string, receiver: any) { + if (isValidIntegerKey(key) || key === 'length') { + return objectGet(rawObj, key, receiver); + } + return Reflect.get(rawObj, key, receiver); +} + +function set(rawObj: any[], key: string, value: any, receiver: any) { + const oldValue = rawObj[key]; + const oldLength = rawObj.length; + const newValue = value; + + const ret = Reflect.set(rawObj, key, newValue, receiver); + + const newLength = rawObj.length; + const tracker = getObserver(rawObj); + + if (!isSame(newValue, oldValue)) { + tracker.setProp(key); + } + + if (oldLength !== newLength) { + tracker.setProp('length'); + } + + return ret; +} diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts new file mode 100644 index 00000000..ec8214b3 --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -0,0 +1,188 @@ +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; +import { isMap, isWeakMap, isSame } from '../../CommonUtils'; + +const COLLECTION_CHANGE = '_collectionChange'; +const handler = { + get, + set, + add, + delete: deleteFun, + clear, + has, + entries, + forEach, + keys, + values, + [Symbol.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); + } + + return Reflect.get(rawObj, key, receiver); +} + +function getFun(rawObj: { get: (key: any) => any }, key: any) { + const tracker = getObserver(rawObj); + tracker.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 tracker = getObserver(rawObj); + + if (valChange || !rawObj.has(key)) { + tracker.setProp(COLLECTION_CHANGE); + } + + if (valChange) { + tracker.setProp(key); + } + + return rawObj; +} + +// Set的add方法 +function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object { + if (!rawObj.has(value)) { + rawObj.add(value); + + const tracker = getObserver(rawObj); + tracker.setProp(value); + tracker.setProp(COLLECTION_CHANGE); + } + + return rawObj; +} + +function has(rawObj: { has: (string) => boolean }, key: any): boolean { + const tracker = getObserver(rawObj); + tracker.useProp(key); + + return rawObj.has(key); +} + +function clear(rawObj: { size: number; clear: () => void }) { + const oldSize = rawObj.size; + rawObj.clear(); + + if (oldSize > 0) { + const tracker = getObserver(rawObj); + tracker.allChange(); + } +} + +function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) { + if (rawObj.has(key)) { + rawObj.delete(key); + + const tracker = getObserver(rawObj); + tracker.setProp(key); + tracker.setProp(COLLECTION_CHANGE); + + return true; + } + + return false; +} + +function size(rawObj: { size: number }) { + const tracker = getObserver(rawObj); + tracker.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 tracker = getObserver(rawObj); + tracker.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 tracker = getObserver(rawObj); + const hookObserver = hookObserverMap.get(rawObj); + tracker.useProp(COLLECTION_CHANGE); + + return { + next() { + const { value, done } = rawIt.next(); + if (done) { + return { value: createProxy(value, hookObserver), done }; + } + + tracker.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.iterator]() { + return this; + }, + }; +} diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts new file mode 100644 index 00000000..709c68a9 --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -0,0 +1,50 @@ +import { isSame } from '../../CommonUtils'; +import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; + +export function createObjectProxy(rawObj: T): ProxyHandler { + const proxy = new Proxy(rawObj, { + get, + set, + }); + + return proxy; +} + +export function get(rawObj: object, key: string, receiver: any): any { + const observer = getObserver(rawObj); + + 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); + + // 对于value也需要进一步代理 + const valProxy = createProxy(value, hookObserverMap.get(rawObj)); + + return valProxy; +} + +export function set(rawObj: object, key: string, value: any, receiver: any): boolean { + 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); + + if (!isSame(newValue, oldValue)) { + observer.setProp(key); + } + + return ret; +} diff --git a/libs/horizon/src/horizonx/proxy/readonlyProxy.ts b/libs/horizon/src/horizonx/proxy/readonlyProxy.ts new file mode 100644 index 00000000..65361c6a --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/readonlyProxy.ts @@ -0,0 +1,25 @@ +import { isObject } from '../CommonUtils'; + +export function readonlyProxy(target: T): ProxyHandler { + return new Proxy(target, { + get(target, property, receiver) { + const result = Reflect.get(target, property, receiver); + try { + if (isObject(result)) { + return readonlyProxy(result); + } + } catch {} + return result; + }, + + set() { + throw Error('Trying to change readonly variable'); + }, + + deleteProperty() { + throw Error('Trying to change readonly variable'); + }, + }); +} + +export default readonlyProxy; diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts new file mode 100644 index 00000000..9b746c96 --- /dev/null +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -0,0 +1,219 @@ +//@ts-ignore +import { useEffect, useRef } from '../../renderer/hooks/HookExternal'; +import { getProcessingVNode } from '../../renderer/GlobalVar'; +import { createProxy } from '../proxy/ProxyHandler'; +import readonlyProxy from '../proxy/readonlyProxy'; +import { StoreHandler, StoreConfig, UserActions, UserComputedValues, StoreActions, ComputedValues, ActionFunction, Action, QueuedStoreActions } from '../types'; +import { Observer } from '../proxy/Observer'; +import { FunctionComponent, ClassComponent } from '../Constants'; + +const storeMap = new Map>(); + +function isPromise(obj: any): boolean { + return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; +} + +type PlannedAction>={ + action:string, + payload: any[], + resolve: ReturnType +} + +export function createStore,C extends UserComputedValues>(config: StoreConfig): () => StoreHandler { + //create a local shalow copy to ensure consistency (if user would change the config object after store creation) + config = { + id:config.id, + options: config.options, + state: config.state, + actions: config.actions ? {...config.actions}:undefined, + computed: config.computed ? {...config.computed}:undefined + } + + // 校验 + if (Object.prototype.toString.call(config) !== '[object Object]') { + throw new Error('store obj must be pure object'); + } + + const proxyObj = createProxy(config.state, !config.options?.suppressHooks); + + proxyObj.$pending = false; + + const $subscribe = (listener) => { + proxyObj.addListener(listener); + }; + + const $unsubscribe = (listener) => { + proxyObj.removeListener(listener); + }; + + const plannedActions:PlannedAction>[] = []; + const $actions:Partial>={} + const $queue:Partial> = {}; + const $computed:Partial>={} + const handler = { + $subscribe, + $unsubscribe, + $actions:$actions as StoreActions, + $state:proxyObj, + $computed: $computed as ComputedValues, + $config:config, + $queue: $queue as QueuedStoreActions, + } as StoreHandler; + + function tryNextAction() { + if (!plannedActions.length) { + proxyObj.$pending = false; + return; + } + + const nextAction = plannedActions.shift()!; + const result = config.actions ? config.actions[nextAction.action].bind(self, proxyObj)(...nextAction.payload) : undefined; + + if (isPromise(result)) { + result.then(value => { + nextAction.resolve(value); + tryNextAction(); + }); + } else { + nextAction.resolve(result); + tryNextAction(); + } + } + + // 包装actions + if(config.actions){ + Object.keys(config.actions).forEach(action => { + ($queue as any)[action] = (...payload) => { + return new Promise((resolve) => { + if (!proxyObj.$pending) { + proxyObj.$pending = true; + const result = config.actions![action].bind(self, proxyObj)(...payload); + + if (isPromise(result)) { + result.then((value) => { + resolve(value); + tryNextAction(); + }); + } else { + resolve(result); + tryNextAction(); + } + } else { + plannedActions.push({ + action, + payload, + resolve + }); + } + }); + }; + + ($actions as any)[action] = function Wrapped(...payload) { + return config.actions![action].bind(self, proxyObj)(...payload); + }; + + // direct store access + Object.defineProperty(handler, action, { + writable: false, + value: $actions[action] + }); + }); + } + + if (config.computed) { + Object.keys(config.computed).forEach((key) => { + ($computed as any)[key] = config.computed![key].bind(handler, readonlyProxy(proxyObj)); + + // direct store access + Object.defineProperty(handler, key, { + get: $computed[key] as ()=>any + }); + }); + } + + // direct state access + if(config.state){ + Object.keys(config.state).forEach(key => { + Object.defineProperty(handler, key, { + get: () => proxyObj[key] + }); + }); + } + + if (config.id) { + storeMap.set(config.id, handler); + } + + return createStoreHook(handler); +} + +function clearVNodeObservers(vNode) { + vNode.observers.forEach(observer => { + observer.clearByVNode(vNode); + }); + + vNode.observers.clear(); +} + +function hookStore() { + const processingVNode = getProcessingVNode(); + + // did not execute in a component + if (!processingVNode) { + return; + } + + if (processingVNode.observers) { + // 清除上一次缓存的Observer依赖 + clearVNodeObservers(processingVNode); + } else { + processingVNode.observers = new Set(); + } + + if (processingVNode.tag === FunctionComponent) { + // from FunctionComponent + const vNodeRef = useRef(null); + vNodeRef.current = processingVNode; + + useEffect(() => { + return () => { + clearVNodeObservers(vNodeRef.current); + vNodeRef.current.observers = null; + }; + }, []); + } else if (processingVNode.tag === ClassComponent) { + // from ClassComponent + if (!processingVNode.classComponentWillUnmount) { + processingVNode.classComponentWillUnmount = function(vNode) { + clearVNodeObservers(vNode); + vNode.observers = null; + }; + } + } +} + +function createStoreHook, C extends UserComputedValues>( + storeHandler: StoreHandler +): () => StoreHandler { + return () => { + if (!storeHandler.$config.options?.suppressHooks) { + hookStore(); + } + + return storeHandler; + }; +} + +export function useStore, C extends UserComputedValues>( + id: string +): StoreHandler { + const storeObj = storeMap.get(id); + + if (storeObj && !storeObj.$config.options?.suppressHooks) hookStore(); + + return storeObj as StoreHandler; +} + +export function clearStore(id:string):void { + storeMap.delete(id); +} \ No newline at end of file diff --git a/libs/horizon/src/horizonx/types.d.ts b/libs/horizon/src/horizonx/types.d.ts new file mode 100644 index 00000000..3fed3afe --- /dev/null +++ b/libs/horizon/src/horizonx/types.d.ts @@ -0,0 +1,81 @@ +export interface IObserver { + + useProp: (key: string) => void; + + addListener: (listener: () => void) => void; + + removeListener: (listener: () => void) => void; + + setProp: (key: string) => void; + + triggerChangeListeners: () => void; + + triggerUpdate: (vNode: any) => void; + + allChange: () => void; + + clearByVNode: (vNode: any) => void; +} + +type RemoveFirstFromTuple = + T['length'] extends 0 ? [] : + (((...b: T) => void) extends (a, ...b: infer I) => void ? I : []) + + +type UserActions = { [K:string]: ActionFunction }; +type UserComputedValues = { [K:string]: ComputedFunction }; + +type ActionFunction = (state: S, ...args: any[]) => any; +type ComputedFunction = (state: S) => any; +type Action> = (...args:RemoveFirstFromTuple>)=>ReturnType +type AsyncAction> = (...args:RemoveFirstFromTuple>)=>Promise> + +type StoreActions> = { [K in keyof A]: Action }; +type QueuedStoreActions> = { [K in keyof A]: AsyncAction }; +type ComputedValues> = { [K in keyof C]: ReturnType }; +type PostponedAction = (state: object, ...args: any[]) => Promise; +type PostponedActions = { [key:string]: PostponedAction } + +export type StoreHandler,C extends UserComputedValues> = + {$subscribe: ((listener: () => void) => void), + $unsubscribe: ((listener: () => void) => void), + $state: S, + $config: StoreConfig, + $queue: QueuedStoreActions, + $actions: StoreActions, + $computed: ComputedValues, + reduxHandler?:ReduxStoreHandler} + & + {[K in keyof S]: S[K]} + & + {[K in keyof A]: Action} + & + {[K in keyof C]: ReturnType} + +export type StoreConfig,C extends UserComputedValues> = { + state?: S, + options?:{suppressHooks?: boolean}, + actions?: A, + id?: string, + computed?: C +} + +type ReduxStoreHandler = { + reducer:(state:any,action:{type:string})=>any, + dispatch:(action:{type:string})=>void, + getState:()=>any, + subscribe:(listener:()=>void)=>((listener:()=>void)=>void) + replaceReducer: (reducer: (state:any,action:{type:string})=>any)=>void + _horizonXstore: StoreHandler +} + +type ReduxAction = { + type:string +} + +type ReduxMiddleware = (store:ReduxStoreHandler, extraArgument?:any) => + (next:((action:ReduxAction)=>any)) => + (action:( + ReduxAction| + ((dispatch:(action:ReduxAction)=>void,store:ReduxStoreHandler,extraArgument?:any)=>any) + )) => ReduxStoreHandler \ No newline at end of file diff --git a/libs/horizon/src/renderer/ContextSaver.ts b/libs/horizon/src/renderer/ContextSaver.ts index cd21da0b..2e72ce3c 100644 --- a/libs/horizon/src/renderer/ContextSaver.ts +++ b/libs/horizon/src/renderer/ContextSaver.ts @@ -45,13 +45,28 @@ export function resetContext(providerVNode: VNode) { context.value = providerVNode.context; } -// 在局部更新时,恢复父节点的context +// 在局部更新时,从上到下恢复父节点的context export function recoverParentContext(vNode: VNode) { + const contextProviders: VNode[] = []; + let parent = vNode.parent; + while (parent !== null) { + if (parent.tag === ContextProvider) { + contextProviders.unshift(parent); + } + parent = parent.parent; + } + contextProviders.forEach(node => { + setContext(node, node.props.value); + }); +} + +// 在局部更新时,从下到上重置父节点的context +export function resetParentContext(vNode: VNode) { let parent = vNode.parent; while (parent !== null) { if (parent.tag === ContextProvider) { - setContext(parent, parent.props.value); + resetContext(parent); } parent = parent.parent; } diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts index 12626215..c8b81ae7 100644 --- a/libs/horizon/src/renderer/TreeBuilder.ts +++ b/libs/horizon/src/renderer/TreeBuilder.ts @@ -29,7 +29,7 @@ import { isExecuting, setExecuteMode } from './ExecuteMode'; -import { recoverParentContext, resetNamespaceCtx, setNamespaceCtx } from './ContextSaver'; +import { recoverParentContext, resetParentContext, resetNamespaceCtx, setNamespaceCtx } from './ContextSaver'; import { updateChildShouldUpdate, updateParentsChildShouldUpdate, @@ -43,6 +43,11 @@ let unrecoverableErrorDuringBuild: any = null; // 当前运行的vNode节点 let processing: VNode | null = null; +let currentRoot: VNode | null = null; +export function getCurrentRoot() { + return currentRoot; +} + export function setProcessing(vNode: VNode | null) { processing = vNode; } @@ -258,6 +263,10 @@ function buildVNodeTree(treeRoot: VNode) { handleError(treeRoot, thrownValue); } } + if (startVNode.tag !== TreeRoot) { // 不是根节点 + // 恢复父节点的context + resetParentContext(startVNode); + } setProcessingClassVNode(null); @@ -267,7 +276,7 @@ function buildVNodeTree(treeRoot: VNode) { // 总体任务入口 function renderFromRoot(treeRoot) { runAsyncEffects(); - + currentRoot = treeRoot; // 1. 构建vNode树 buildVNodeTree(treeRoot); @@ -278,6 +287,7 @@ function renderFromRoot(treeRoot) { // 2. 提交变更 submitToRender(treeRoot); + currentRoot = null; if (window.__HORIZON_DEV_HOOK__) { const hook = window.__HORIZON_DEV_HOOK__; diff --git a/libs/horizon/src/renderer/submit/Submit.ts b/libs/horizon/src/renderer/submit/Submit.ts index e80f911a..d19dfea2 100644 --- a/libs/horizon/src/renderer/submit/Submit.ts +++ b/libs/horizon/src/renderer/submit/Submit.ts @@ -114,21 +114,21 @@ function submit(dirtyNodes: Array) { if ((node.flags & ResetText) === ResetText) { submitResetTextContent(node); } - + if ((node.flags & Ref) === Ref) { if (!node.isCreated) { // 需要执行 detachRef(node, true); } } - + isAdd = (node.flags & Addition) === Addition; isUpdate = (node.flags & Update) === Update; if (isAdd && isUpdate) { // Addition submitAddition(node); FlagUtils.removeFlag(node, Addition); - + // Update submitUpdate(node); } else { @@ -161,7 +161,7 @@ function afterSubmit(dirtyNodes: Array) { if ((node.flags & Update) === Update || (node.flags & Callback) === Callback) { callAfterSubmitLifeCycles(node); } - + if ((node.flags & Ref) === Ref) { attachRef(node); } diff --git a/libs/horizon/src/renderer/vnode/VNode.ts b/libs/horizon/src/renderer/vnode/VNode.ts index d3e6c23a..7c360e47 100644 --- a/libs/horizon/src/renderer/vnode/VNode.ts +++ b/libs/horizon/src/renderer/vnode/VNode.ts @@ -74,7 +74,11 @@ export class VNode { suspenseState: SuspenseState; path = ''; // 保存从根到本节点的路径 + + // 根节点数据 toUpdateNodes: Set | null; // 保存要更新的节点 + delegatedEvents: Set + delegatedNativeEvents: Set belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用 @@ -94,6 +98,8 @@ export class VNode { this.realNode = realNode; this.task = null; this.toUpdateNodes = new Set(); + this.delegatedEvents = new Set(); + this.delegatedNativeEvents = new Set(); this.updates = null; this.stateCallbacks = null; this.state = null; diff --git a/libs/horizon/src/renderer/vnode/VNodeUtils.ts b/libs/horizon/src/renderer/vnode/VNodeUtils.ts index ef60b566..a586b922 100644 --- a/libs/horizon/src/renderer/vnode/VNodeUtils.ts +++ b/libs/horizon/src/renderer/vnode/VNodeUtils.ts @@ -31,7 +31,6 @@ export function travelVNodeTree( finishVNode: VNode, // 结束遍历节点,有时候和beginVNode不相同 handleWhenToParent: Function | null ): VNode | null { - const filter = childFilter === null; let node = beginVNode; while (true) { @@ -43,7 +42,7 @@ export function travelVNodeTree( // 找子节点 const childVNode = node.child; - if (childVNode !== null && (filter || !childFilter(node))) { + if (childVNode !== null && (childFilter === null || !childFilter(node))) { childVNode.parent = node; node = childVNode; continue; @@ -194,20 +193,6 @@ export function getSiblingDom(vNode: VNode): Element | null { } } -function isSameContainer( - container: Element, - targetContainer: EventTarget, -): boolean { - if (container === targetContainer) { - return true; - } - // 注释类型的节点 - if (isComment(container) && container.parentNode === targetContainer) { - return true; - } - return false; -} - function isPortalRoot(vNode, targetContainer) { if (vNode.tag === DomPortal) { let topVNode = vNode.parent; @@ -216,7 +201,7 @@ function isPortalRoot(vNode, targetContainer) { if (grandTag === TreeRoot || grandTag === DomPortal) { const topContainer = topVNode.realNode; // 如果topContainer是targetContainer,不需要在这里处理 - if (isSameContainer(topContainer, targetContainer)) { + if (topContainer === targetContainer) { return true; } } @@ -228,28 +213,28 @@ function isPortalRoot(vNode, targetContainer) { } // 获取根vNode节点 -export function getExactNode(targetVNode, targetContainer) { +export function findRoot(targetVNode, targetDom) { // 确认vNode节点是否准确,portal场景下可能祖先节点不准确 let vNode = targetVNode; while (vNode !== null) { if (vNode.tag === TreeRoot || vNode.tag === DomPortal) { - let container = vNode.realNode; - if (isSameContainer(container, targetContainer)) { + let dom = vNode.realNode; + if (dom === targetDom) { break; } - if (isPortalRoot(vNode, targetContainer)) { + if (isPortalRoot(vNode, targetDom)) { return null; } - while (container !== null) { - const parentNode = getNearestVNode(container); + while (dom !== null) { + const parentNode = getNearestVNode(dom); if (parentNode === null) { return null; } if (parentNode.tag === DomComponent || parentNode.tag === DomText) { - return getExactNode(parentNode, targetContainer); + return findRoot(parentNode, targetDom); } - container = container.parentNode; + dom = dom.parentNode; } } vNode = vNode.parent; diff --git a/package.json b/package.json index 58bf2f9f..c31b27cf 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "lint": "eslint . --ext .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-dev": "npm run build & node ./scripts/gen3rdLib.js --dev", "build-horizon3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev --type horizon", diff --git a/scripts/__tests__/ComponentTest/Context.test.js b/scripts/__tests__/ComponentTest/Context.test.js index d64934ef..14423b34 100644 --- a/scripts/__tests__/ComponentTest/Context.test.js +++ b/scripts/__tests__/ComponentTest/Context.test.js @@ -358,4 +358,79 @@ describe('Context Test', () => { Horizon.render(, container); expect(container.querySelector('p').innerHTML).toBe('Num: 8, Type: typeR'); }); + + // antd menu 级连context场景,menu路径使用级联context实现 + it('nested context', () => { + const NestedContext = Horizon.createContext([]); + let updateContext; + + function App() { + const [state, useState] = Horizon.useState([]); + updateContext = useState; + return ( + + + + + ); + } + + const div1Ref = Horizon.createRef(); + const div2Ref = Horizon.createRef(); + + let updateSub1; + function Sub1() { + const path = Horizon.useContext(NestedContext); + const [_, setState] = Horizon.useState({}); + updateSub1 = () => setState({}); + return ( + + + + ); + } + + function Sub2() { + const path = Horizon.useContext(NestedContext); + + return ( + + + + ); + } + + function Sub3() { + const path = Horizon.useContext(NestedContext); + + return ( + + + + ); + } + + function Son({ divRef }) { + const path = Horizon.useContext(NestedContext); + return ( + +
{path.join(',')}
+
+ ); + } + + Horizon.render(, container); + updateSub1(); + expect(div1Ref.current.innerHTML).toEqual('1'); + expect(div2Ref.current.innerHTML).toEqual('2,3'); + + updateContext([0]); + expect(div1Ref.current.innerHTML).toEqual('0,1'); + expect(div2Ref.current.innerHTML).toEqual('0,2,3'); + + // 局部更新Sub1 + updateSub1(); + expect(div1Ref.current.innerHTML).toEqual('0,1'); + expect(div2Ref.current.innerHTML).toEqual('0,2,3'); + }); }); diff --git a/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js b/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js index 129c2ec3..a88cb813 100644 --- a/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js +++ b/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js @@ -30,6 +30,7 @@ describe('useEffect Hook Test', () => { expect(document.getElementById('p').style.display).toBe('block'); // 点击按钮触发num加1 container.querySelector('button').click(); + expect(document.getElementById('p').style.display).toBe('none'); container.querySelector('button').click(); expect(container.querySelector('p').style.display).toBe('inline'); diff --git a/scripts/__tests__/DomTest/Attribute.test.js b/scripts/__tests__/DomTest/Attribute.test.js index 49030e2f..32fe44a5 100755 --- a/scripts/__tests__/DomTest/Attribute.test.js +++ b/scripts/__tests__/DomTest/Attribute.test.js @@ -61,4 +61,10 @@ describe('Dom Attribute', () => { container.querySelector('div').setAttribute('data-first-name', 'Tom'); expect(container.querySelector('div').dataset.firstName).toBe('Tom'); }); -}); \ No newline at end of file + + it('style 自动加px', () => { + const div = Horizon.render(
, container); + expect(window.getComputedStyle(div).getPropertyValue('width')).toBe('10px'); + expect(window.getComputedStyle(div).getPropertyValue('height')).toBe('20px'); + }); +}); diff --git a/scripts/__tests__/DomTest/DomInput.test.js b/scripts/__tests__/DomTest/DomInput.test.js index d5649ff3..2e8ccc23 100755 --- a/scripts/__tests__/DomTest/DomInput.test.js +++ b/scripts/__tests__/DomTest/DomInput.test.js @@ -22,16 +22,6 @@ describe('Dom Input', () => { ).not.toThrow(); }); - it('checked属性受控时无法更改', () => { - Horizon.render( { - LogUtils.log('checkbox click'); - }} />, container); - container.querySelector('input').click(); - // 点击复选框不会改变checked的值 - expect(LogUtils.getAndClear()).toEqual(['checkbox click']); - expect(container.querySelector('input').checked).toBe(true); - }); - it('复选框的value属性值可以改变', () => { Horizon.render( { @@ -96,30 +86,6 @@ describe('Dom Input', () => { ).not.toThrow(); }); - it('value属性受控时无法更改', () => { - const realNode = Horizon.render( { - LogUtils.log('text change'); - }} />, container); - - // 模拟改变text输入框的值 - // 先修改 - Object.getOwnPropertyDescriptor( - HTMLInputElement.prototype, - 'value', - ).set.call(realNode, 'abcd'); - // 再触发事件 - realNode.dispatchEvent( - new Event('input', { - bubbles: true, - cancelable: true, - }), - ); - // 确实发生了input事件 - expect(LogUtils.getAndClear()).toEqual(['text change']); - // value受控,不会改变 - expect(container.querySelector('input').value).toBe('text'); - }); - it('value值会转为字符串', () => { const realNode = Horizon.render(, container); expect(realNode.value).toBe('1'); @@ -172,6 +138,11 @@ describe('Dom Input', () => { expect(realNode.getAttribute('value')).toBe('default'); }); + it('value为0、defaultValue为1,input 的value应该为0', () => { + const input = Horizon.render(, container); + expect(input.getAttribute('value')).toBe('0'); + }); + it('name属性', () => { let realNode = Horizon.render(, container); expect(realNode.name).toBe('name'); @@ -244,30 +215,6 @@ describe('Dom Input', () => { expect(document.getElementById('d').checked).toBe(true); }); - it('受控radio的状态', () => { - Horizon.render( - <> - - - , container); - expect(container.querySelector('input').checked).toBe(true); - expect(document.getElementById('b').checked).toBe(false); - Object.getOwnPropertyDescriptor( - HTMLInputElement.prototype, - 'checked', - ).set.call(document.getElementById('b'), true); - // 再触发事件 - document.getElementById('b').dispatchEvent( - new Event('click', { - bubbles: true, - cancelable: true, - }), - ); - // 模拟点击单选框B,两个受控radio的状态不会改变 - expect(container.querySelector('input').checked).toBe(true); - expect(document.getElementById('b').checked).toBe(false); - }); - it('name改变不影响相同name的radio', () => { const inputRef = Horizon.createRef(); const App = () => { @@ -426,4 +373,4 @@ describe('Dom Input', () => { expect(container.querySelector('input').hasAttribute('value')).toBe(false); }); }); -}); \ No newline at end of file +}); diff --git a/scripts/__tests__/DomTest/DomSelect.test.js b/scripts/__tests__/DomTest/DomSelect.test.js index dfd1828e..48932033 100755 --- a/scripts/__tests__/DomTest/DomSelect.test.js +++ b/scripts/__tests__/DomTest/DomSelect.test.js @@ -53,37 +53,6 @@ describe('Dom Select', () => { expect(realNode.value).toBe('React'); }); - it('受控select', () => { - const selectNode = ( - - ); - const realNode = Horizon.render(selectNode, container); - expect(realNode.value).toBe('Vue'); - expect(realNode.options[1].selected).toBe(true); - // 先修改 - Object.getOwnPropertyDescriptor( - HTMLSelectElement.prototype, - 'value', - ).set.call(realNode, 'React'); - // 再触发事件 - container.querySelector('select').dispatchEvent( - new Event('change', { - bubbles: true, - cancelable: true, - }), - ); - // 鼠标改变受控select不生效 - Horizon.render(selectNode, container); - // 'React'项没有被选中 - expect(realNode.options[0].selected).toBe(false); - expect(realNode.options[1].selected).toBe(true); - expect(realNode.value).toBe('Vue'); - }); - it('受控select转为不受控会保存原来select', () => { const selectNode = (