Match-id-7f8d174af4898d07f955fd534ed03502794f40ed
This commit is contained in:
commit
f276a3ecad
25
.eslintrc.js
25
.eslintrc.js
|
@ -1,19 +1,13 @@
|
|||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier',
|
||||
],
|
||||
root: true,
|
||||
|
||||
plugins: [
|
||||
'jest',
|
||||
'no-for-of-loops',
|
||||
'no-function-declare-after-return',
|
||||
'react',
|
||||
'@typescript-eslint',
|
||||
],
|
||||
plugins: ['jest', 'no-for-of-loops', 'no-function-declare-after-return', 'react', '@typescript-eslint'],
|
||||
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
|
@ -34,7 +28,9 @@ module.exports = {
|
|||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'semi': ["error", "always"],
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
semi: ['warn', 'always'],
|
||||
quotes: ['warn', 'single'],
|
||||
'accessor-pairs': 'off',
|
||||
'brace-style': ['error', '1tbs'],
|
||||
'func-style': ['warn', 'declaration', { allowArrowFunctions: true }],
|
||||
|
@ -43,19 +39,18 @@ module.exports = {
|
|||
// 尾随逗号
|
||||
'comma-dangle': ['error', 'only-multiline'],
|
||||
|
||||
'no-constant-condition': 'off',
|
||||
'no-for-of-loops/no-for-of-loops': 'error',
|
||||
'no-function-declare-after-return/no-function-declare-after-return': 'error',
|
||||
},
|
||||
globals: {
|
||||
isDev: true
|
||||
isDev: true,
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'scripts/__tests__/**/*.js'
|
||||
],
|
||||
files: ['scripts/__tests__/**/*.js'],
|
||||
globals: {
|
||||
container: true
|
||||
container: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
## 文件清单说明:
|
||||
devtools_page: devtool主页面
|
||||
default_popup: 拓展图标点击时弹窗页面
|
||||
content_scripts: 内容脚本,在项目中负责在页面初始化时调用注入全局变量代码和消息传递
|
||||
web_accessible_resources: 注入全局变量代码
|
||||
|
||||
## 打开 panel 页面调试面板的方式
|
||||
|
||||
1. Open the developer tools.
|
||||
1. Undock the developer tools if not already done (via the button in the bottom-left corner).
|
||||
1. Press Ctrl + Shift + J to open the developer tools of the developer tools.
|
||||
Optional: Feel free to dock the developer tools again if you had undocked it at step 2.
|
||||
1. Switch from "<top frame>" to devtoolsBackground.html (or whatever name you have chosen for your devtools). (example)
|
||||
1. Now you can use the Console tab to play with the chrome.devtools API.
|
||||
|
||||
## 全局变量注入
|
||||
通过content_scripts在document初始化时给页面添加script脚本,在新添加的脚本中给window注入全局变量
|
||||
|
||||
## horizon页面判断
|
||||
在页面完成渲染后往全局变量中添加信息,并传递 tabId 给 background 告知这是 horizon 页面
|
||||
|
||||
## 通信方式:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant web_page
|
||||
participant script_content
|
||||
participant background
|
||||
participant panel
|
||||
|
||||
Note over web_page: window.postMessage
|
||||
web_page ->> script_content : {}
|
||||
Note over script_content: window.addEventListener
|
||||
Note over script_content: chrome.runtime.sendMessage
|
||||
script_content ->> background : {}
|
||||
Note over background: chrome.runtime.onMessage
|
||||
Note over background: port.postMessage
|
||||
background ->> panel : {}
|
||||
Note over panel: connection.onMessage.addListener
|
||||
Note over panel: connection.postMessage
|
||||
panel ->> background : {}
|
||||
Note over background: port.onMessage.addListener
|
||||
Note over background: chrome.tabs.sendMessage
|
||||
background ->> script_content : {}
|
||||
Note over script_content: chrome.runtime.onMessage
|
||||
Note over script_content: window.postMessage
|
||||
script_content ->> web_page : {}
|
||||
Note over web_page: window.addEventListener
|
||||
```
|
||||
|
||||
## 数据压缩
|
||||
渲染组件树需要知道组件名和层次信息,如果把整个vNode树传递过来,传递对象太大,最好将数据进行压缩然后传递。
|
||||
- 相同的组件名可以进行压缩
|
||||
- 每个vNode有唯一的 path 属性,可以作为标识使用
|
||||
- 通过解析 path 值可以分析出组件树的结构
|
||||
|
||||
## 滚动动态渲染 Tree
|
||||
考虑到组件树可能很大,所以并不适合一次性全部渲染出来,可以通过滚动渲染的方式减少页面 dom 的数量。我们可以把树看成有不同缩进长度的列表,动态渲染滚动列表的实现可以参考谷歌的这篇文章:https://developers.google.com/web/updates/2016/07/infinite-scroller 这样,我们需要的组件树数据可以由树结构转变为数组,可以减少动态渲染时对树结构进行解析时的计算工作。
|
|
@ -0,0 +1,126 @@
|
|||
import styles from './ComponentsInfo.less';
|
||||
import Eye from '../svgs/Eye';
|
||||
import Debug from '../svgs/Debug';
|
||||
import Copy from '../svgs/Copy';
|
||||
import Triangle from '../svgs/Triangle';
|
||||
import { useState } from 'horizon';
|
||||
import { IData } from './VTree';
|
||||
|
||||
type IComponentInfo = {
|
||||
name: string;
|
||||
attrs: {
|
||||
props?: IAttr[];
|
||||
context?: IAttr[];
|
||||
state?: IAttr[];
|
||||
hooks?: IAttr[];
|
||||
};
|
||||
parents: IData[];
|
||||
onClickParent: (item: IData) => void;
|
||||
};
|
||||
|
||||
type IAttr = {
|
||||
name: string;
|
||||
type: string;
|
||||
value: string | boolean;
|
||||
indentation: number;
|
||||
}
|
||||
|
||||
function collapseAllNodes(attrs: IAttr[]) {
|
||||
return attrs.filter((item, index) => {
|
||||
const nextItem = attrs[index + 1];
|
||||
return nextItem ? nextItem.indentation - item.indentation > 0 : false;
|
||||
});
|
||||
}
|
||||
|
||||
function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
|
||||
const [collapsedNode, setCollapsedNode] = useState(collapseAllNodes(attrs));
|
||||
const handleCollapse = (item: IAttr) => {
|
||||
const nodes = [...collapsedNode];
|
||||
const i = nodes.indexOf(item);
|
||||
if (i === -1) {
|
||||
nodes.push(item);
|
||||
} else {
|
||||
nodes.splice(i, 1);
|
||||
}
|
||||
setCollapsedNode(nodes);
|
||||
};
|
||||
|
||||
const showAttr = [];
|
||||
let currentIndentation = null;
|
||||
attrs.forEach((item, index) => {
|
||||
const indentation = item.indentation;
|
||||
if (currentIndentation !== null) {
|
||||
if (indentation > currentIndentation) {
|
||||
return;
|
||||
} else {
|
||||
currentIndentation = null;
|
||||
}
|
||||
}
|
||||
const nextItem = attrs[index + 1];
|
||||
const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false;
|
||||
const isCollapsed = collapsedNode.includes(item);
|
||||
showAttr.push(
|
||||
<div style={{ paddingLeft: item.indentation * 10 }} key={index} onClick={() => (handleCollapse(item))}>
|
||||
<span className={styles.attrArrow}>{hasChild && <Triangle director={isCollapsed ? 'right' : 'down'} />}</span>
|
||||
<span className={styles.attrName}>{`${item.name}`}</span>
|
||||
{' :'}
|
||||
<span className={styles.attrValue}>{item.value}</span>
|
||||
</div>
|
||||
);
|
||||
if (isCollapsed) {
|
||||
currentIndentation = indentation;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.attrContainer}>
|
||||
<div className={styles.attrHead}>
|
||||
<span className={styles.attrType}>{name}</span>
|
||||
<span className={styles.attrCopy}>
|
||||
<Copy />
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.attrDetail}>
|
||||
{showAttr}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ComponentInfo({ name, attrs, parents, onClickParent }: IComponentInfo) {
|
||||
const { state, props, context, hooks } = attrs;
|
||||
return (
|
||||
<div className={styles.infoContainer} >
|
||||
<div className={styles.componentInfoHead}>
|
||||
{name && <>
|
||||
<span className={styles.name}>
|
||||
{name}
|
||||
</span>
|
||||
<span className={styles.eye} >
|
||||
<Eye />
|
||||
</span>
|
||||
<span className={styles.debug}>
|
||||
<Debug />
|
||||
</span>
|
||||
</>}
|
||||
</div>
|
||||
<div className={styles.componentInfoMain}>
|
||||
{context && <ComponentAttr name={'context'} attrs={context} />}
|
||||
{props && <ComponentAttr name={'props'} attrs={props} />}
|
||||
{state && <ComponentAttr name={'state'} attrs={state} />}
|
||||
{hooks && <ComponentAttr name={'hook'} attrs={hooks} />}
|
||||
<div className={styles.parentsInfo}>
|
||||
{name && <div>
|
||||
parents: {
|
||||
parents.map(item => (<button
|
||||
className={styles.parent}
|
||||
onClick={() => (onClickParent(item))}>
|
||||
{item.name}
|
||||
</button>))
|
||||
}
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
@import 'assets.less';
|
||||
|
||||
.infoContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
|
||||
.componentInfoHead {
|
||||
flex: 0 0 @top-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: @divider-style;
|
||||
|
||||
.name {
|
||||
flex: 1 1 0;
|
||||
padding: 0 1rem 0 1rem;
|
||||
}
|
||||
|
||||
.eye {
|
||||
flex: 0 0 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.debug {
|
||||
flex: 0 0 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.componentInfoMain {
|
||||
overflow-y: auto;
|
||||
|
||||
>:last-child {
|
||||
border-bottom: unset;
|
||||
}
|
||||
|
||||
>:first-child {
|
||||
padding: unset;
|
||||
}
|
||||
|
||||
>div {
|
||||
border-bottom: @divider-style;
|
||||
padding: 0.5rem
|
||||
}
|
||||
|
||||
.attrContainer {
|
||||
flex: 0 0;
|
||||
|
||||
.attrHead {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.5rem 0 0.5rem;
|
||||
|
||||
.attrType {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
.attrCopy {
|
||||
flex: 0 0 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
.attrDetail {
|
||||
padding-bottom: 0.5rem;
|
||||
|
||||
.attrArrow {
|
||||
color: @arrow-color;
|
||||
width: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.attrName {
|
||||
color: @attr-name-color;
|
||||
}
|
||||
|
||||
.attrValue {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.parentsInfo {
|
||||
flex: 1 1 0;
|
||||
.parent {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
color: @component-name-color;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
background-color: @select-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
// 过滤树的抽象逻辑
|
||||
// 需要知道渲染了哪些数据,过滤的字符串/正则表达式
|
||||
// 控制Tree组件位置跳转,告知匹配结果
|
||||
// 清空搜索框,告知搜索框当前是第几个结果,跳转搜索结果
|
||||
//
|
||||
// 跳转搜索结果的交互逻辑:
|
||||
// 如果当前页面存在匹配项,页面不动
|
||||
// 如果当前页面不存在匹配项,页面跳转到第一个匹配项位置
|
||||
// 如果匹配项被折叠,需要展开其父节点。注意只展开当前匹配项的父节点,其他匹配项的父节点不展开
|
||||
// 跳转到上一个匹配项或下一个匹配项时,如果匹配项被折叠,需要展开其父节点
|
||||
//
|
||||
// 寻找父节点:
|
||||
// 找到该节点的缩进值,和index值,在data中向上遍历,通过缩进值判断父节点
|
||||
|
||||
import { useState, useRef } from 'horizon';
|
||||
import { createRegExp } from '../utils';
|
||||
|
||||
/**
|
||||
* 把节点的父节点从收起节点数组中删除,并返回新的收起节点数组
|
||||
*
|
||||
* @param item 需要展开父节点的节点
|
||||
* @param data 全部数据
|
||||
* @param collapsedNodes 收起节点数据
|
||||
* @returns 新的收起节点数组
|
||||
*/
|
||||
function expandItemParent(item: BaseType, data: BaseType[], collapsedNodes: BaseType[]): BaseType[] {
|
||||
const index = data.indexOf(item);
|
||||
let currentIndentation = item.indentation;
|
||||
// 不对原始数据进行修改
|
||||
const newCollapsedNodes = [...collapsedNodes];
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
const lastData = data[i];
|
||||
const lastIndentation = lastData.indentation;
|
||||
// 缩进更小,找到了父节点
|
||||
if (lastIndentation < currentIndentation) {
|
||||
// 更新缩进值,只找父节点的父节点,避免修改父节点的兄弟节点的展开状态
|
||||
currentIndentation = lastIndentation;
|
||||
const cIndex = newCollapsedNodes.indexOf(lastData);
|
||||
if (cIndex !== -1) {
|
||||
newCollapsedNodes.splice(cIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newCollapsedNodes;
|
||||
}
|
||||
|
||||
type BaseType = {
|
||||
id: string,
|
||||
name: string,
|
||||
indentation: number,
|
||||
}
|
||||
|
||||
export function FilterTree<T extends BaseType>(props: { data: T[] }) {
|
||||
const { data } = props;
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
const [currentItem, setCurrentItem] = useState(null); // 当前选中的匹配项
|
||||
const showItemsRef = useRef([]); // 页面展示的 items
|
||||
const matchItemsRef = useRef([]); // 匹配过滤条件的 items
|
||||
const collapsedNodesRef = useRef([]); // 折叠节点,如果匹配 item 被折叠了,需要展开
|
||||
|
||||
const matchItems = matchItemsRef.current;
|
||||
const collapsedNodes = collapsedNodesRef.current;
|
||||
|
||||
const updateCollapsedNodes = (item: BaseType) => {
|
||||
const newcollapsedNodes = expandItemParent(item, data, collapsedNodes);
|
||||
// 如果新旧收起节点数组长度不一样,说明存在收起节点
|
||||
if (newcollapsedNodes.length !== collapsedNodes.length) {
|
||||
// 更新引用,确保 VTree 拿到新的 collapsedNodes
|
||||
collapsedNodesRef.current = newcollapsedNodes;
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeSearchValue = (search: string) => {
|
||||
const reg = createRegExp(search);
|
||||
let newCurrentItem = null;
|
||||
let newMatchItems = [];
|
||||
if (search !== '') {
|
||||
const showItems: T[] = showItemsRef.current;
|
||||
newMatchItems = data.reduce((pre, current) => {
|
||||
const { name } = current;
|
||||
if (reg && name.match(reg)) {
|
||||
pre.push(current);
|
||||
// 如果当前页面显示的 item 存在匹配项,则把它设置为 currentItem
|
||||
if (newCurrentItem === null && showItems.includes(current)) {
|
||||
newCurrentItem = current;
|
||||
}
|
||||
}
|
||||
return pre;
|
||||
}, []);
|
||||
if (newMatchItems.length === 0) {
|
||||
setCurrentItem(null);
|
||||
} else {
|
||||
if (newCurrentItem === null) {
|
||||
const item = newMatchItems[0];
|
||||
// 不处于当前展示页面,需要展开父节点
|
||||
updateCollapsedNodes(item);
|
||||
setCurrentItem(item);
|
||||
} else {
|
||||
setCurrentItem(newCurrentItem);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setCurrentItem(null);
|
||||
}
|
||||
matchItemsRef.current = newMatchItems;
|
||||
setFilterValue(search);
|
||||
};
|
||||
const onSelectNext = () => {
|
||||
const index = matchItems.indexOf(currentItem);
|
||||
const nextIndex = index + 1;
|
||||
const item = nextIndex < matchItemsRef.current.length ? matchItems[nextIndex] : matchItems[0];
|
||||
// 可能不处于当前展示页面,需要展开父节点
|
||||
updateCollapsedNodes(item);
|
||||
setCurrentItem(item);
|
||||
};
|
||||
const onSelectLast = () => {
|
||||
const index = matchItems.indexOf(currentItem);
|
||||
const last = index - 1;
|
||||
const item = last >= 0 ? matchItems[last] : matchItems[matchItems.length - 1];
|
||||
// 可能不处于当前展示页面,需要展开父节点
|
||||
updateCollapsedNodes(item);
|
||||
setCurrentItem(item);
|
||||
};
|
||||
const setShowItems = (items) => {
|
||||
showItemsRef.current = [...items];
|
||||
};
|
||||
const onClear = () => {
|
||||
onChangeSearchValue('');
|
||||
};
|
||||
const setCollapsedNodes = (items) => {
|
||||
// 不更新引用,避免子组件的重复渲染
|
||||
collapsedNodesRef.current.length = 0;
|
||||
collapsedNodesRef.current.push(...items);
|
||||
};
|
||||
return {
|
||||
filterValue,
|
||||
onChangeSearchValue,
|
||||
onClear,
|
||||
currentItem,
|
||||
matchItems,
|
||||
onSelectNext,
|
||||
onSelectLast,
|
||||
setShowItems,
|
||||
collapsedNodes,
|
||||
setCollapsedNodes,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
*
|
||||
* 由于 ResizeObserver 对 IE 和低版本主流浏览器不兼容,需要我们自己解决这个问题。
|
||||
* 这是一个不依赖任何框架的监听 dom 元素尺寸变化的解决方案。
|
||||
* 浏览器出于性能的考虑,只有 window 的 resize 事件会触发。我们通过 object 标签可以得到
|
||||
* 一个 window 对象,让 object dom 元素成为待观测 dom 的子元素,并且和待观测 dom 大小一致。
|
||||
* 这样一旦待观测 dom 的大小发生变化, window 的大小也会发生变化,我们就可以通过监听 window
|
||||
* 大小变化的方式监听待观测 dom 的大小变化。
|
||||
*
|
||||
* <div id='test'>
|
||||
* <object> --> 和父 div 保持大小一致
|
||||
* <html></html> --> 添加 resize 事件监听
|
||||
* </object>
|
||||
* </div>
|
||||
*
|
||||
*/
|
||||
|
||||
function timeout(fn) {
|
||||
return setTimeout(fn, 20);
|
||||
}
|
||||
|
||||
function requestFrame(fn) {
|
||||
const raf = requestAnimationFrame || timeout;
|
||||
return raf(fn);
|
||||
}
|
||||
|
||||
function cancelFrame(id) {
|
||||
const cancel = cancelAnimationFrame || clearTimeout;
|
||||
cancel(id);
|
||||
}
|
||||
|
||||
// 在闲置帧触发回调事件,如果在本次触发前存在未处理回调事件,
|
||||
// 需要取消未处理的回调事件
|
||||
function resizeListener(event) {
|
||||
const win = event.target;
|
||||
if (win.__resizeRAF__) {
|
||||
cancelFrame(win.__resizeRAF__);
|
||||
}
|
||||
win.__resizeRAF__ = requestFrame(function () {
|
||||
const observeElement = win.__observeElement__;
|
||||
observeElement.__resizeCallbacks__.forEach(function (fn) {
|
||||
fn.call(observeElement, observeElement, event);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadObserver() {
|
||||
// 将待观测元素传递给 object 标签的 window 对象,这样在触发 resize 事件时可以拿到待观测元素
|
||||
this.contentDocument.defaultView.__observeElement__ = this.__observeElement__;
|
||||
// 给 html 的 window 对象添加 resize 事件
|
||||
this.contentDocument.defaultView.addEventListener('resize', resizeListener);
|
||||
}
|
||||
|
||||
export function addResizeListener(element: any, fn: any) {
|
||||
if (!element.__resizeCallbacks__) {
|
||||
element.__resizeCallbacks__ = [fn];
|
||||
element.style.position = 'relative';
|
||||
const observer = document.createElement('object');
|
||||
observer.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
|
||||
observer.data = 'about:blank';
|
||||
observer.onload = loadObserver;
|
||||
observer.type = 'text/html';
|
||||
observer.__observeElement__ = element;
|
||||
element.__observer__ = observer;
|
||||
element.appendChild(observer);
|
||||
} else {
|
||||
element.__resizeCallbacks__.push(fn);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeResizeListener(element, fn) {
|
||||
element.__resizeCallbacks__.splice(element.__resizeCallbacks__.indexOf(fn), 1);
|
||||
if (!element.__resizeCallbacks__.length) {
|
||||
element.__observer__.contentDocument.defaultView.removeEventListener('resize', resizeListener);
|
||||
element.removeChild(element.__observer__);
|
||||
element.__observer__ = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.search {
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import styles from './Search.less';
|
||||
|
||||
interface SearchProps {
|
||||
onChange: (event: any) => void,
|
||||
value: string,
|
||||
}
|
||||
|
||||
export default function Search(props: SearchProps) {
|
||||
const { onChange, value } = props;
|
||||
const handleChange = (event) => {
|
||||
onChange(event.target.value);
|
||||
};
|
||||
return (
|
||||
<input
|
||||
onChange={handleChange}
|
||||
className={styles.search}
|
||||
value={value}
|
||||
placeholder={'Search (text or /regex/)'}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { useEffect, useState, useRef } from 'horizon';
|
||||
import { addResizeListener, removeResizeListener } from './ResizeEvent';
|
||||
|
||||
|
||||
export function SizeObserver(props) {
|
||||
const { children, ...rest } = props;
|
||||
const containerRef = useRef();
|
||||
const [size, setSize] = useState();
|
||||
const notifyChild = (element) => {
|
||||
setSize({
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
const element = containerRef.current;
|
||||
setSize({
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
});
|
||||
addResizeListener(element, notifyChild);
|
||||
return () => {
|
||||
removeResizeListener(element, notifyChild);
|
||||
};
|
||||
}, []);
|
||||
const myChild = size ? children(size.width, size.height) : null;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} {...rest}>
|
||||
{myChild}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
.container {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
|
||||
import { useState, useRef, useEffect } from 'horizon';
|
||||
import styles from './VList.less';
|
||||
|
||||
interface IProps<T extends { id: string }> {
|
||||
data: T[],
|
||||
width: number, // 暂时未用到,当需要支持横向滚动时使用
|
||||
height: number, // VList 的高度
|
||||
children: any, // horizon 组件,组件类型是 T
|
||||
itemHeight: number,
|
||||
scrollToItem?: T, // 滚动到指定项位置,如果该项在可见区域内,不滚动,如果不在,则滚动到中间位置
|
||||
onRendered: (renderInfo: renderInfoType<T>) => void;
|
||||
filter?(data: T): boolean, // false 表示该行不显示
|
||||
}
|
||||
|
||||
export type renderInfoType<T> = {
|
||||
visibleItems: T[],
|
||||
skipItemCountBeforeScrollItem: number,
|
||||
};
|
||||
|
||||
export function VList<T extends { id: string }>(props: IProps<T>) {
|
||||
const {
|
||||
data,
|
||||
height,
|
||||
children,
|
||||
itemHeight,
|
||||
scrollToItem,
|
||||
filter,
|
||||
onRendered,
|
||||
} = props;
|
||||
const [scrollTop, setScrollTop] = useState(data.indexOf(scrollToItem) * itemHeight);
|
||||
const renderInfoRef: { current: renderInfoType<T> } = useRef({ visibleItems: [], skipItemCountBeforeScrollItem: 0 });
|
||||
const containerRef = useRef();
|
||||
useEffect(() => {
|
||||
onRendered(renderInfoRef.current);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollToItem) {
|
||||
const renderInfo = renderInfoRef.current;
|
||||
// 在滚动区域,不滚动
|
||||
if (!renderInfo.visibleItems.includes(scrollToItem)) {
|
||||
const index = data.indexOf(scrollToItem);
|
||||
// top值计算需要减掉filter条件判定不显示项
|
||||
const totalCount = index - renderInfoRef.current.skipItemCountBeforeScrollItem;
|
||||
// 显示在页面中间
|
||||
const top = totalCount * itemHeight - height / 2;
|
||||
containerRef.current.scrollTo({ top: top });
|
||||
}
|
||||
}
|
||||
}, [scrollToItem]);
|
||||
|
||||
const handleScroll = (event: any) => {
|
||||
const scrollTop = event.target.scrollTop;
|
||||
setScrollTop(scrollTop);
|
||||
};
|
||||
const showList: any[] = [];
|
||||
let totalHeight = 0;
|
||||
// 顶部冗余
|
||||
const startShowTopValue = Math.max(scrollTop - itemHeight * 4, 0);
|
||||
// 底部冗余
|
||||
const showNum = Math.floor(height / itemHeight) + 4;
|
||||
// 如果最后一个显示不全,不统计在显示 ids 内
|
||||
const maxTop = scrollTop + height - itemHeight;
|
||||
// 清空记录的上次渲染的数据
|
||||
renderInfoRef.current.visibleItems.length = 0;
|
||||
const scrollItemIndex = data.indexOf(scrollToItem);
|
||||
renderInfoRef.current.skipItemCountBeforeScrollItem = 0;
|
||||
data.forEach((item, i) => {
|
||||
if (filter && !filter(item)) {
|
||||
if (scrollItemIndex > i) {
|
||||
renderInfoRef.current.skipItemCountBeforeScrollItem++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (totalHeight >= startShowTopValue && showList.length <= showNum) {
|
||||
showList.push(
|
||||
<div
|
||||
key={String(item.id)}
|
||||
className={styles.item}
|
||||
style={{ transform: `translateY(${totalHeight}px)` }} >
|
||||
{children(i, item)}
|
||||
</div>
|
||||
);
|
||||
if (totalHeight >= scrollTop && totalHeight < maxTop) {
|
||||
renderInfoRef.current.visibleItems.push(item);
|
||||
}
|
||||
}
|
||||
totalHeight += itemHeight;
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={styles.container} onScroll={handleScroll}>
|
||||
{showList}
|
||||
<div style={{ marginTop: totalHeight }} />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
@import 'assets.less';
|
||||
|
||||
.treeContainer {
|
||||
height: 100%;
|
||||
|
||||
.treeItem {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
line-height: 18px;
|
||||
|
||||
&:hover {
|
||||
background-color: @select-color;
|
||||
}
|
||||
|
||||
.treeIcon {
|
||||
color: @arrow-color;
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.componentName {
|
||||
color: @component-name-color;
|
||||
}
|
||||
|
||||
.componentKeyName {
|
||||
color: @component-key-color;
|
||||
}
|
||||
|
||||
.componentKeyValue {
|
||||
color: @componentKeyValue-color;
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
background-color: rgb(141 199 248 / 60%);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
import { useState, useEffect } from 'horizon';
|
||||
import styles from './VTree.less';
|
||||
import Triangle from '../svgs/Triangle';
|
||||
import { createRegExp } from './../utils';
|
||||
import { SizeObserver } from './SizeObserver';
|
||||
import { renderInfoType, VList } from './VList';
|
||||
|
||||
export interface IData {
|
||||
id: string;
|
||||
name: string;
|
||||
indentation: number;
|
||||
userKey: string;
|
||||
}
|
||||
|
||||
interface IItem {
|
||||
hasChild: boolean,
|
||||
onCollapse: (data: IData) => void,
|
||||
onClick: (id: IData) => void,
|
||||
isCollapsed: boolean,
|
||||
isSelect: boolean,
|
||||
highlightValue: string,
|
||||
data: IData,
|
||||
}
|
||||
|
||||
const indentationLength = 20;
|
||||
|
||||
function Item(props: IItem) {
|
||||
const {
|
||||
hasChild,
|
||||
onCollapse,
|
||||
isCollapsed,
|
||||
data,
|
||||
onClick,
|
||||
isSelect,
|
||||
highlightValue = '',
|
||||
} = props;
|
||||
|
||||
const {
|
||||
name,
|
||||
userKey,
|
||||
indentation,
|
||||
} = data;
|
||||
|
||||
const isShowKey = userKey !== '';
|
||||
const showIcon = hasChild ? <Triangle director={isCollapsed ? 'right' : 'down'} /> : '';
|
||||
const handleClickCollapse = () => {
|
||||
onCollapse(data);
|
||||
};
|
||||
const handleClick = () => {
|
||||
onClick(data);
|
||||
};
|
||||
const itemAttr: any = { className: styles.treeItem, onClick: handleClick };
|
||||
if (isSelect) {
|
||||
itemAttr.tabIndex = 0;
|
||||
itemAttr.className = styles.treeItem + ' ' + styles.select;
|
||||
}
|
||||
const reg = createRegExp(highlightValue);
|
||||
const heightCharacters = name.match(reg);
|
||||
let showName;
|
||||
if (heightCharacters) {
|
||||
let cutName = name;
|
||||
showName = [];
|
||||
// 高亮第一次匹配即可
|
||||
const char = heightCharacters[0];
|
||||
const index = name.search(char);
|
||||
const notHighlightStr = cutName.slice(0, index);
|
||||
showName.push(notHighlightStr);
|
||||
showName.push(<mark>{char}</mark>);
|
||||
cutName = cutName.slice(index + char.length);
|
||||
showName.push(cutName);
|
||||
} else {
|
||||
showName = name;
|
||||
}
|
||||
return (
|
||||
<div {...itemAttr}>
|
||||
<div style={{ marginLeft: indentation * indentationLength }} className={styles.treeIcon} onClick={handleClickCollapse} >
|
||||
{showIcon}
|
||||
</div>
|
||||
<span className={styles.componentName} >
|
||||
{showName}
|
||||
</span>
|
||||
{isShowKey && (
|
||||
<>
|
||||
<span className={styles.componentKeyName}>
|
||||
{' '}key
|
||||
</span>
|
||||
{'="'}
|
||||
<span className={styles.componentKeyValue}>
|
||||
{userKey}
|
||||
</span>
|
||||
{'"'}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VTree(props: {
|
||||
data: IData[],
|
||||
highlightValue: string,
|
||||
scrollToItem: IData,
|
||||
onRendered: (renderInfo: renderInfoType<IData>) => void,
|
||||
collapsedNodes?: IData[],
|
||||
onCollapseNode?: (item: IData[]) => void,
|
||||
selectItem: IData[],
|
||||
onSelectItem: (item: IData) => void,
|
||||
}) {
|
||||
const { data, highlightValue, scrollToItem, onRendered, onCollapseNode, onSelectItem } = props;
|
||||
const [collapseNode, setCollapseNode] = useState(props.collapsedNodes || []);
|
||||
const [selectItem, setSelectItem] = useState(props.selectItem);
|
||||
useEffect(() => {
|
||||
setSelectItem(scrollToItem);
|
||||
}, [scrollToItem]);
|
||||
useEffect(() => {
|
||||
if (props.selectItem !== selectItem) {
|
||||
setSelectItem(props.selectItem);
|
||||
}
|
||||
}, [props.selectItem]);
|
||||
useEffect(() => {
|
||||
setCollapseNode(props.collapsedNodes || []);
|
||||
}, [props.collapsedNodes]);
|
||||
|
||||
const changeCollapseNode = (item: IData) => {
|
||||
const nodes: IData[] = [...collapseNode];
|
||||
const index = nodes.indexOf(item);
|
||||
if (index === -1) {
|
||||
nodes.push(item);
|
||||
} else {
|
||||
nodes.splice(index, 1);
|
||||
}
|
||||
setCollapseNode(nodes);
|
||||
if (onCollapseNode) {
|
||||
onCollapseNode(nodes);
|
||||
}
|
||||
};
|
||||
const handleClickItem = (item: IData) => {
|
||||
setSelectItem(item);
|
||||
if (onSelectItem) {
|
||||
onSelectItem(item);
|
||||
}
|
||||
};
|
||||
|
||||
let currentCollapseIndentation: null | number = null;
|
||||
// 过滤掉折叠的 item,不展示在 VList 中
|
||||
const filter = (item: IData) => {
|
||||
if (currentCollapseIndentation !== null) {
|
||||
// 缩进更大,不显示
|
||||
if (item.indentation > currentCollapseIndentation) {
|
||||
return false;
|
||||
} else {
|
||||
// 缩进小,说明完成了该收起节点的子节点处理。
|
||||
currentCollapseIndentation = null;
|
||||
}
|
||||
}
|
||||
const isCollapsed = collapseNode.includes(item);
|
||||
if (isCollapsed) {
|
||||
// 该节点需要收起子节点
|
||||
currentCollapseIndentation = item.indentation;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<SizeObserver className={styles.treeContainer}>
|
||||
{(width: number, height: number) => {
|
||||
return (
|
||||
<VList
|
||||
data={data}
|
||||
width={width}
|
||||
height={height}
|
||||
itemHeight={18}
|
||||
scrollToItem={selectItem}
|
||||
filter={filter}
|
||||
onRendered={onRendered}
|
||||
>
|
||||
{(index: number, item: IData) => {
|
||||
// 如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标
|
||||
const nextItem = data[index + 1];
|
||||
const hasChild = nextItem && nextItem.indentation > item.indentation;
|
||||
return (
|
||||
<Item
|
||||
hasChild={hasChild}
|
||||
isCollapsed={collapseNode.includes(item)}
|
||||
isSelect={selectItem === item}
|
||||
onCollapse={changeCollapseNode}
|
||||
onClick={handleClickItem}
|
||||
highlightValue={highlightValue}
|
||||
data={item} />
|
||||
);
|
||||
}}
|
||||
</VList>
|
||||
);
|
||||
}}
|
||||
</SizeObserver>
|
||||
);
|
||||
}
|
||||
|
||||
export default VTree;
|
|
@ -0,0 +1,15 @@
|
|||
@arrow-color: rgb(95, 99, 104);
|
||||
@divider-color: rgb(202, 205, 209);
|
||||
@attr-name-color: rgb(200, 0, 0);
|
||||
@component-name-color: rgb(136, 18, 128);
|
||||
@component-key-color: rgb(153, 69, 0);
|
||||
@componentKeyValue-color: rgb(26, 26, 166);
|
||||
@component-attr-color: rgb(200, 0, 0);
|
||||
@select-color: rgb(141 199 248 / 60%);
|
||||
@hover-color: black;
|
||||
|
||||
@top-height: 2.625rem;
|
||||
@divider-width: 0.2px;
|
||||
@common-font-size: 12px;
|
||||
|
||||
@divider-style: @divider-color solid @divider-width;
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* 用一个纯数据类型的对象 tree 去表示树的结构是非常清晰的,但是它不能准确的模拟 VNode 中存在的引用
|
||||
* 关系,需要进行转换 getMockVNodeTree
|
||||
*/
|
||||
|
||||
import { parseAttr } from '../parser/parseAttr';
|
||||
import parseTreeRoot from '../parser/parseVNode';
|
||||
import { VNode } from './../../../horizon/src/renderer/vnode/VNode';
|
||||
import { FunctionComponent, ClassComponent } from './../../../horizon/src/renderer/vnode/VNodeTags';
|
||||
|
||||
const mockComponentNames = ['Apple', 'Pear', 'Banana', 'Orange', 'Jenny', 'Kiwi', 'Coconut'];
|
||||
|
||||
function MockVNode(tag: string, props = {}, key = null, realNode = {}) {
|
||||
const vNode = new VNode(tag, props, key, realNode);
|
||||
const name = mockComponentNames.shift() || 'MockComponent';
|
||||
vNode.type = { name };
|
||||
return vNode;
|
||||
}
|
||||
|
||||
interface IMockTree {
|
||||
tag: string,
|
||||
children?: IMockTree[],
|
||||
}
|
||||
|
||||
// 模拟树
|
||||
const tree: IMockTree = {
|
||||
tag: ClassComponent,
|
||||
children: [
|
||||
{ tag: FunctionComponent },
|
||||
{ tag: ClassComponent },
|
||||
{ tag: FunctionComponent },
|
||||
{
|
||||
tag: FunctionComponent,
|
||||
children: [
|
||||
{ tag: ClassComponent }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
function addOneThousandNode(node: IMockTree) {
|
||||
const nodes = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
nodes.push({ tag: FunctionComponent });
|
||||
}
|
||||
node?.children.push({ tag: ClassComponent, children: nodes });
|
||||
}
|
||||
|
||||
addOneThousandNode(tree);
|
||||
|
||||
/**
|
||||
* 将mock数据转变为 VNode 树
|
||||
*
|
||||
* @param node 树节点
|
||||
* @param vNode VNode节点
|
||||
*/
|
||||
function getMockVNodeTree(node: IMockTree, vNode: VNode) {
|
||||
const children = node.children;
|
||||
if (children && children.length !== 0) {
|
||||
const childNode = children[0];
|
||||
let childVNode = MockVNode(childNode.tag);
|
||||
childVNode.key = '0';
|
||||
getMockVNodeTree(childNode, childVNode);
|
||||
// 需要建立双链
|
||||
vNode.child = childVNode;
|
||||
childVNode.parent = vNode;
|
||||
for (let i = 1; i < children.length; i++) {
|
||||
const nextNode = children[i];
|
||||
const nextVNode = MockVNode(nextNode.tag);
|
||||
nextVNode.key = String(i);
|
||||
nextVNode.parent = vNode;
|
||||
getMockVNodeTree(nextNode, nextVNode);
|
||||
childVNode.next = nextVNode;
|
||||
childVNode = nextVNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
const rootVNode = MockVNode(tree.tag);
|
||||
getMockVNodeTree(tree, rootVNode);
|
||||
|
||||
export const mockParsedVNodeData = parseTreeRoot(rootVNode);
|
||||
|
||||
const mockState = {
|
||||
str: 'jenny',
|
||||
num: 3,
|
||||
boolean: true,
|
||||
und: undefined,
|
||||
fun: () => ({}),
|
||||
symbol: Symbol('sym'),
|
||||
map: new Map([['a', 'a']]),
|
||||
set: new Set(['a', 1, 2, Symbol('bambi')]),
|
||||
arr: [1, 2, 3, 4],
|
||||
obj: {
|
||||
niko: { jenny: 'jenny' }
|
||||
}
|
||||
};
|
||||
|
||||
export const parsedMockState = parseAttr(mockState);
|
|
@ -0,0 +1,74 @@
|
|||
@import '../components/assets.less';
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
font-size: @common-font-size;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.left_top {
|
||||
border-bottom: @divider-style;
|
||||
flex: 0 0 @top-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 0.4rem;
|
||||
|
||||
.select {
|
||||
padding: 0 0.25rem 0 0.25rem;
|
||||
flex: 0 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
flex: 0 0 1px;
|
||||
margin: 0 0.25rem 0 0.25rem;
|
||||
border-left: @divider-style;
|
||||
height: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
.search {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.searchResult{
|
||||
flex: 0 0 ;
|
||||
padding: 0 0.4rem;
|
||||
}
|
||||
|
||||
.searchAction {
|
||||
flex: 0 0 1rem;
|
||||
height: 1rem;
|
||||
color: @arrow-color;
|
||||
&:hover{
|
||||
color: @hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left_bottom {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 3;
|
||||
border-left: @divider-style;
|
||||
}
|
||||
|
||||
input {
|
||||
outline: none;
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
import { useState, useEffect } from 'horizon';
|
||||
import VTree, { IData } from '../components/VTree';
|
||||
import Search from '../components/Search';
|
||||
import ComponentInfo from '../components/ComponentInfo';
|
||||
import styles from './App.less';
|
||||
import Select from '../svgs/Select';
|
||||
import { mockParsedVNodeData, parsedMockState } from '../devtools/mock';
|
||||
import { FilterTree } from '../components/FilterTree';
|
||||
import Close from '../svgs/Close';
|
||||
import Arrow from './../svgs/Arrow';
|
||||
|
||||
const parseVNodeData = (rawData) => {
|
||||
const idIndentationMap: {
|
||||
[id: string]: number;
|
||||
} = {};
|
||||
const data: IData[] = [];
|
||||
let i = 0;
|
||||
while (i < rawData.length) {
|
||||
const id = rawData[i] as string;
|
||||
i++;
|
||||
const name = rawData[i] as string;
|
||||
i++;
|
||||
const parentId = rawData[i] as string;
|
||||
i++;
|
||||
const userKey = rawData[i] as string;
|
||||
i++;
|
||||
const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1;
|
||||
idIndentationMap[id] = indentation;
|
||||
const item = {
|
||||
id, name, indentation, userKey
|
||||
};
|
||||
data.push(item);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const getParents = (item: IData | null, parsedVNodeData: IData[]) => {
|
||||
const parents: IData[] = [];
|
||||
if (item) {
|
||||
const index = parsedVNodeData.indexOf(item);
|
||||
let indentation = item.indentation;
|
||||
for (let i = index; i >= 0; i--) {
|
||||
const last = parsedVNodeData[i];
|
||||
const lastIndentation = last.indentation;
|
||||
if (lastIndentation < indentation) {
|
||||
parents.push(last);
|
||||
indentation = lastIndentation;
|
||||
}
|
||||
}
|
||||
}
|
||||
return parents;
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [parsedVNodeData, setParsedVNodeData] = useState([]);
|
||||
const [componentAttrs, setComponentAttrs] = useState({});
|
||||
const [selectComp, setSelectComp] = useState(null);
|
||||
|
||||
const {
|
||||
filterValue,
|
||||
onChangeSearchValue: setFilterValue,
|
||||
onClear,
|
||||
currentItem,
|
||||
matchItems,
|
||||
onSelectNext,
|
||||
onSelectLast,
|
||||
setShowItems,
|
||||
collapsedNodes,
|
||||
setCollapsedNodes,
|
||||
} = FilterTree({ data: parsedVNodeData });
|
||||
|
||||
useEffect(() => {
|
||||
if (isDev) {
|
||||
const parsedData = parseVNodeData(mockParsedVNodeData);
|
||||
setParsedVNodeData(parsedData);
|
||||
setComponentAttrs({
|
||||
state: parsedMockState,
|
||||
props: parsedMockState,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSearchChange = (str: string) => {
|
||||
setFilterValue(str);
|
||||
};
|
||||
|
||||
const handleSelectComp = (item: IData) => {
|
||||
setComponentAttrs({
|
||||
state: parsedMockState,
|
||||
props: parsedMockState,
|
||||
});
|
||||
setSelectComp(item);
|
||||
};
|
||||
|
||||
const handleClickParent = (item: IData) => {
|
||||
setSelectComp(item);
|
||||
};
|
||||
|
||||
const onRendered = (info) => {
|
||||
setShowItems(info.visibleItems);
|
||||
};
|
||||
const parents = getParents(selectComp, parsedVNodeData);
|
||||
|
||||
return (
|
||||
<div className={styles.app}>
|
||||
<div className={styles.left}>
|
||||
<div className={styles.left_top} >
|
||||
<div className={styles.select} >
|
||||
<Select />
|
||||
</div>
|
||||
<div className={styles.divider} />
|
||||
<div className={styles.search}>
|
||||
<Search onChange={handleSearchChange} value={filterValue} />
|
||||
</div>
|
||||
{filterValue !== '' && <>
|
||||
<span className={styles.searchResult}>{`${matchItems.indexOf(currentItem) + 1}/${matchItems.length}`}</span>
|
||||
<div className={styles.divider} />
|
||||
<button className={styles.searchAction} onClick={onSelectLast}><Arrow direction={'up'} /></button>
|
||||
<button className={styles.searchAction} onClick={onSelectNext}><Arrow direction={'down'} /></button>
|
||||
<button className={styles.searchAction} onClick={onClear}><Close /></button>
|
||||
</>}
|
||||
</div>
|
||||
<div className={styles.left_bottom}>
|
||||
<VTree
|
||||
data={parsedVNodeData}
|
||||
highlightValue={filterValue}
|
||||
onRendered={onRendered}
|
||||
collapsedNodes={collapsedNodes}
|
||||
onCollapseNode={setCollapsedNodes}
|
||||
scrollToItem={currentItem}
|
||||
selectItem={selectComp}
|
||||
onSelectItem={handleSelectComp} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
<ComponentInfo
|
||||
name={selectComp ? selectComp.name: null}
|
||||
attrs={selectComp ? componentAttrs: {}}
|
||||
parents={parents}
|
||||
onClickParent={handleClickParent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,7 @@
|
|||
import { render } from 'horizon';
|
||||
import App from './App';
|
||||
|
||||
render(
|
||||
<App />,
|
||||
document.getElementById('root')
|
||||
);
|
|
@ -0,0 +1,34 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Horizon</title>
|
||||
<script src='horizon.production.js'></script>
|
||||
<style>
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,83 @@
|
|||
|
||||
// 将状态的值解析成固定格式
|
||||
export function parseAttr(rootAttr: any) {
|
||||
const result: {
|
||||
name: string,
|
||||
type: string,
|
||||
value: string,
|
||||
indentation: number
|
||||
}[] = [];
|
||||
const indentation = 0;
|
||||
const parseSubAttr = (attr: any, parentIndentation: number, attrName: string) => {
|
||||
const stateType = typeof attr;
|
||||
let value: any;
|
||||
let showType;
|
||||
let addSubState;
|
||||
if (stateType === 'boolean' ||
|
||||
stateType === 'number' ||
|
||||
stateType === 'string' ||
|
||||
stateType === 'undefined') {
|
||||
value = attr;
|
||||
showType = stateType;
|
||||
} else if (stateType === 'function') {
|
||||
const funName = attr.name;
|
||||
value = `f() ${funName}{}`;
|
||||
} else if (stateType === 'symbol') {
|
||||
value = attr.description;
|
||||
} else if (stateType === 'object') {
|
||||
if (attr === null) {
|
||||
showType = 'null';
|
||||
} else if (attr instanceof Map) {
|
||||
showType = 'map';
|
||||
const size = attr.size;
|
||||
value = `Map(${size})`;
|
||||
addSubState = () => {
|
||||
attr.forEach((value, key) => {
|
||||
parseSubAttr(value, parentIndentation + 2, key);
|
||||
});
|
||||
};
|
||||
} else if (attr instanceof Set) {
|
||||
showType = 'set';
|
||||
const size = attr.size;
|
||||
value = `Set(${size})`;
|
||||
addSubState = () => {
|
||||
let i = 0;
|
||||
attr.forEach((value) => {
|
||||
parseSubAttr(value, parentIndentation + 2, String(i));
|
||||
});
|
||||
i++;
|
||||
};
|
||||
} else if (Array.isArray(attr)) {
|
||||
showType = 'array';
|
||||
value = `Array(${attr.length})`;
|
||||
addSubState = () => {
|
||||
attr.forEach((value, index) => {
|
||||
parseSubAttr(value, parentIndentation + 2, String(index));
|
||||
});
|
||||
};
|
||||
} else {
|
||||
showType = stateType;
|
||||
value = '{...}';
|
||||
addSubState = () => {
|
||||
Object.keys(attr).forEach((key) => {
|
||||
parseSubAttr(attr[key], parentIndentation + 2, key);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
result.push({
|
||||
name: attrName,
|
||||
type: showType,
|
||||
value,
|
||||
indentation: parentIndentation + 1,
|
||||
});
|
||||
if (addSubState) {
|
||||
addSubState();
|
||||
}
|
||||
};
|
||||
Object.keys(rootAttr).forEach(key => {
|
||||
parseSubAttr(rootAttr[key], indentation, key);
|
||||
});
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import { travelVNodeTree } from '../../../../libs/horizon/src/renderer/vnode/VNodeUtils';
|
||||
import { VNode } from '../../../../libs/horizon/src/renderer/Types';
|
||||
import { ClassComponent, FunctionComponent } from '../../../../libs/horizon/src/renderer/vnode/VNodeTags';
|
||||
|
||||
// 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode
|
||||
const VNodeToIdMap = new Map<VNode, number>();
|
||||
const IdToVNodeMap = new Map<number, VNode>();
|
||||
|
||||
let uid = 0;
|
||||
function generateUid () {
|
||||
uid++;
|
||||
return uid;
|
||||
}
|
||||
|
||||
function isUserComponent(tag: string) {
|
||||
// TODO: 添加其他组件
|
||||
return tag === ClassComponent || tag === FunctionComponent;
|
||||
}
|
||||
|
||||
function getParentUserComponent(node: VNode) {
|
||||
let parent = node.parent;
|
||||
while(parent) {
|
||||
if (isUserComponent(parent.tag)) {
|
||||
break;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
function parseTreeRoot(treeRoot: VNode) {
|
||||
const result: any[] = [];
|
||||
travelVNodeTree(treeRoot, (node: VNode) => {
|
||||
const tag = node.tag;
|
||||
if (isUserComponent(tag)) {
|
||||
const id = generateUid();
|
||||
result.push(id);
|
||||
const name = node.type.name;
|
||||
result.push(name);
|
||||
const parent = getParentUserComponent(node);
|
||||
if (parent) {
|
||||
const parentId = VNodeToIdMap.get(parent);
|
||||
result.push(parentId);
|
||||
} else {
|
||||
result.push('');
|
||||
}
|
||||
const key = node.key;
|
||||
if (key !== null) {
|
||||
result.push(key);
|
||||
} else {
|
||||
result.push('');
|
||||
}
|
||||
VNodeToIdMap.set(node, id);
|
||||
IdToVNodeMap.set(id, node);
|
||||
}
|
||||
}, null, treeRoot, null);
|
||||
return result;
|
||||
}
|
||||
|
||||
export default parseTreeRoot;
|
|
@ -0,0 +1,17 @@
|
|||
interface IArrow {
|
||||
direction: 'up' | 'down'
|
||||
}
|
||||
|
||||
export default function Arrow({ direction: director }: IArrow) {
|
||||
let d: string;
|
||||
if (director === 'up') {
|
||||
d = 'M4 9.5 L5 10.5 L8 7.5 L11 10.5 L12 9.5 L8 5.5 z';
|
||||
} else if (director === 'down') {
|
||||
d = 'M5 5.5 L4 6.5 L8 10.5 L12 6.5 L11 5.5 L8 8.5z';
|
||||
}
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<path d={d} fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
export default function Close() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<path d='M4 3 L3 4 L7 8 L3 12 L4 13 L8 9 L12 13 L13 12 L9 8 L13 4 L12 3 L8 7z' fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
export default function Copy() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<path d='M0 0 H16 V16 H0 z M2 2 H8 V8 H2 V2z' fill='currentColor' fill-rule='evenodd' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
export default function Debug() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<path d='m2 0l12 8l-12 8 z' fill='#000'/>
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
export default function Eye() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<ellipse cx="8" cy="8" rx="8" ry="6" />
|
||||
<circle cx="8" cy="8" r="4" fill="rgb(255, 255, 255)" />
|
||||
<circle cx="8" cy="8" r="2" fill="#000000" />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
export default function Select() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<path d='M14 6 V3 C14 2.5 13.5 2 13 2 H3 C2.5 2 2 2.5 2 3 V13 C2 13.5 2.5 14 3 14H6 V13 H3 V3 H13 V6z M7 7 L9 15 L11 12 L14 15 L15 14 L12 11 L15 9z' fill='#000' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
interface IArrow {
|
||||
director: 'right' | 'down'
|
||||
}
|
||||
|
||||
export default function Triangle({ director }: IArrow) {
|
||||
let d: string;
|
||||
if (director === 'right') {
|
||||
d = 'm2 0l12 8l-12 8 z';
|
||||
} else if (director === 'down') {
|
||||
d = 'm0 2h16 l-8 12 z';
|
||||
}
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='8px' height='8px'>
|
||||
<path d={d} fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
export function createRegExp(expression: string) {
|
||||
let str = expression;
|
||||
if (str[0] === '/') {
|
||||
str = str.slice(1);
|
||||
}
|
||||
if (str[str.length - 1] === '/') {
|
||||
str = str.slice(0, str.length - 1);
|
||||
}
|
||||
try {
|
||||
return new RegExp(str, 'i');
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
// 用于 panel 页面开发
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: {
|
||||
panel: path.join(__dirname, './src/panel/index.tsx'),
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
filename: '[name].js'
|
||||
},
|
||||
devtool: 'source-map',
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env',
|
||||
'@babel/preset-typescript',
|
||||
['@babel/preset-react', {
|
||||
runtime: 'classic',
|
||||
'pragma': 'Horizon.createElement',
|
||||
'pragmaFrag': 'Horizon.Fragment',
|
||||
}]],
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.less/i,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: true,
|
||||
|
||||
}
|
||||
},
|
||||
'less-loader'],
|
||||
}]
|
||||
},
|
||||
externals: {
|
||||
'horizon': 'Horizon',
|
||||
},
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, 'dist'),
|
||||
},
|
||||
open: 'panel.html',
|
||||
port: 9000,
|
||||
magicHtml: true,
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'panel.html',
|
||||
template: './src/panel/panel.html'
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"development"',
|
||||
isDev: 'true',
|
||||
}),
|
||||
],
|
||||
};
|
|
@ -1,12 +1,20 @@
|
|||
import {
|
||||
Children,
|
||||
createRef,
|
||||
Component,
|
||||
PureComponent,
|
||||
createContext,
|
||||
forwardRef,
|
||||
lazy,
|
||||
memo,
|
||||
TYPE_FRAGMENT as Fragment,
|
||||
TYPE_PROFILER as Profiler,
|
||||
TYPE_STRICT_MODE as StrictMode,
|
||||
TYPE_SUSPENSE as Suspense,
|
||||
} from './src/external/JSXElementType';
|
||||
|
||||
import { Component, PureComponent } from './src/renderer/components/BaseClassComponent';
|
||||
import { createRef } from './src/renderer/components/CreateRef';
|
||||
import { Children } from './src/external/ChildrenUtil';
|
||||
import { createElement, cloneElement, isValidElement } from './src/external/JSXElement';
|
||||
import { createContext } from './src/renderer/components/context/CreateContext';
|
||||
import { lazy } from './src/renderer/components/Lazy';
|
||||
import { forwardRef } from './src/renderer/components/ForwardRef';
|
||||
import { memo } from './src/renderer/components/Memo';
|
||||
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
|
@ -16,14 +24,19 @@ import {
|
|||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
Fragment,
|
||||
Profiler,
|
||||
StrictMode,
|
||||
Suspense,
|
||||
createElement,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
} from './src/external/Horizon';
|
||||
useDebugValue
|
||||
} from './src/renderer/hooks/HookExternal';
|
||||
import { launchUpdateFromVNode as _launchUpdateFromVNode, 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/hooks/BaseHook';
|
||||
|
||||
// act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。
|
||||
const act = fun => {
|
||||
asyncUpdates(fun);
|
||||
callRenderQueueImmediate();
|
||||
runAsyncEffects();
|
||||
};
|
||||
|
||||
import {
|
||||
render,
|
||||
|
@ -42,6 +55,7 @@ const Horizon = {
|
|||
forwardRef,
|
||||
lazy,
|
||||
memo,
|
||||
useDebugValue,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
|
@ -63,6 +77,9 @@ const Horizon = {
|
|||
unstable_batchedUpdates,
|
||||
findDOMNode,
|
||||
unmountComponentAtNode,
|
||||
act,
|
||||
_launchUpdateFromVNode,
|
||||
_getProcessingVNode,
|
||||
};
|
||||
|
||||
export {
|
||||
|
@ -74,6 +91,7 @@ export {
|
|||
forwardRef,
|
||||
lazy,
|
||||
memo,
|
||||
useDebugValue,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
|
@ -95,6 +113,11 @@ export {
|
|||
unstable_batchedUpdates,
|
||||
findDOMNode,
|
||||
unmountComponentAtNode,
|
||||
act,
|
||||
|
||||
// 暂时给HorizonX使用
|
||||
_launchUpdateFromVNode,
|
||||
_getProcessingVNode,
|
||||
};
|
||||
|
||||
export default Horizon;
|
||||
|
|
|
@ -6,7 +6,7 @@ import {Props} from '../DOMOperator';
|
|||
* @param doc 指定 document
|
||||
*/
|
||||
export function getFocusedDom(doc?: Document): HorizonDom | null {
|
||||
let currentDocument = doc ?? document;
|
||||
const currentDocument = doc ?? document;
|
||||
|
||||
return currentDocument.activeElement ?? currentDocument.body;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import * as Horizon from '../../external/Horizon';
|
||||
import {IProperty} from '../utils/Interface';
|
||||
import { Children } from '../../external/ChildrenUtil';
|
||||
import { IProperty } from '../utils/Interface';
|
||||
|
||||
// 把 const a = 'a'; <option>gir{a}ffe</option> 转成 giraffe
|
||||
function concatChildren(children) {
|
||||
let content = '';
|
||||
Horizon.Children.forEach(children, function(child) {
|
||||
Children.forEach(children, function(child) {
|
||||
content += child;
|
||||
});
|
||||
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import {
|
||||
TYPE_FRAGMENT,
|
||||
TYPE_PROFILER,
|
||||
TYPE_STRICT_MODE,
|
||||
TYPE_SUSPENSE,
|
||||
} from './JSXElementType';
|
||||
|
||||
import {Component, PureComponent} from '../renderer/components/BaseClassComponent';
|
||||
import {createRef} from '../renderer/components/CreateRef';
|
||||
import {Children} from './ChildrenUtil';
|
||||
import {
|
||||
createElement,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
} from './JSXElement';
|
||||
import {createContext} from '../renderer/components/context/CreateContext';
|
||||
import {lazy} from '../renderer/components/Lazy';
|
||||
import {forwardRef} from '../renderer/components/ForwardRef';
|
||||
import {memo} from '../renderer/components/Memo';
|
||||
import hookMapping from '../renderer/hooks/HookMapping';
|
||||
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from '../renderer/hooks/HookExternal';
|
||||
|
||||
export {
|
||||
Children,
|
||||
createRef,
|
||||
Component,
|
||||
PureComponent,
|
||||
createContext,
|
||||
forwardRef,
|
||||
lazy,
|
||||
memo,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
TYPE_FRAGMENT as Fragment,
|
||||
TYPE_PROFILER as Profiler,
|
||||
TYPE_STRICT_MODE as StrictMode,
|
||||
TYPE_SUSPENSE as Suspense,
|
||||
createElement,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
hookMapping,
|
||||
};
|
|
@ -1,162 +1,60 @@
|
|||
/**
|
||||
* 保存与深度遍历相关的一些context。
|
||||
* 在深度遍历过程中,begin阶段会修改一些全局的值,在complete阶段会恢复。
|
||||
* 在深度遍历过程中,capture阶段会修改一些全局的值,在bubble阶段会恢复。
|
||||
*/
|
||||
|
||||
import type {VNode, ContextType} from './Types';
|
||||
import type {Container} from '../dom/DOMOperator';
|
||||
import type { VNode, ContextType } from './Types';
|
||||
import type { Container } from '../dom/DOMOperator';
|
||||
|
||||
import {getNSCtx} from '../dom/DOMOperator';
|
||||
import {ContextProvider} from './vnode/VNodeTags';
|
||||
import { getNSCtx } from '../dom/DOMOperator';
|
||||
import { ContextProvider } from './vnode/VNodeTags';
|
||||
|
||||
// 保存的是“http://www.w3.org/1999/xhtml”或“http://www.w3.org/2000/svg”,
|
||||
// 用于识别是使用document.createElement()还是使用document.createElementNS()创建DOM
|
||||
const CTX_NAMESPACE = 'CTX_NAMESPACE';
|
||||
|
||||
// 保存的是Horizon.createContext()的值,或Provider重新设置的值
|
||||
const CTX_CONTEXT = 'CTX_CONTEXT';
|
||||
|
||||
// 旧版context API,是否更改。
|
||||
const CTX_OLD_CHANGE = 'CTX_OLD_CHANGE';
|
||||
// 旧版context API,保存的是的当前组件提供给子组件使用的context。
|
||||
const CTX_OLD_CONTEXT = 'CTX_OLD_CONTEXT';
|
||||
// 旧版context API,保存的是的上一个提供者提供给后代组件使用的context。
|
||||
const CTX_OLD_PREVIOUS_CONTEXT = 'CTX_OLD_PREVIOUS_CONTEXT';
|
||||
|
||||
let ctxNamespace: string = '';
|
||||
|
||||
let ctxOldContext: Object = {};
|
||||
let ctxOldChange: Boolean = false;
|
||||
let ctxOldPreviousContext: Object = {};
|
||||
|
||||
function setContext(vNode: VNode, contextName, value) {
|
||||
if (vNode.contexts === null) {
|
||||
vNode.contexts = {
|
||||
[contextName]: value,
|
||||
};
|
||||
} else {
|
||||
vNode.contexts[contextName] = value;
|
||||
}
|
||||
}
|
||||
function getContext(vNode: VNode, contextName) {
|
||||
if (vNode.contexts !== null) {
|
||||
return vNode.contexts[contextName];
|
||||
}
|
||||
}
|
||||
let ctxNamespace = '';
|
||||
|
||||
// capture阶段设置
|
||||
function setNamespaceCtx(vNode: VNode, dom?: Container) {
|
||||
export function setNamespaceCtx(vNode: VNode, dom?: Container) {
|
||||
const nextContext = getNSCtx(ctxNamespace, vNode.type, dom);
|
||||
|
||||
setContext(vNode, CTX_NAMESPACE, ctxNamespace);
|
||||
vNode.context = ctxNamespace;
|
||||
|
||||
ctxNamespace = nextContext;
|
||||
}
|
||||
|
||||
// bubble阶段恢复
|
||||
function resetNamespaceCtx(vNode: VNode) {
|
||||
ctxNamespace = getContext(vNode, CTX_NAMESPACE);
|
||||
export function resetNamespaceCtx(vNode: VNode) {
|
||||
ctxNamespace = vNode.context;
|
||||
}
|
||||
|
||||
function getNamespaceCtx(): string {
|
||||
export function getNamespaceCtx(): string {
|
||||
return ctxNamespace;
|
||||
}
|
||||
|
||||
function setContextCtx<T>(providerVNode: VNode, nextValue: T) {
|
||||
export function setContext<T>(providerVNode: VNode, nextValue: T) {
|
||||
const context: ContextType<T> = providerVNode.type._context;
|
||||
|
||||
setContext(providerVNode, CTX_CONTEXT, context.value);
|
||||
providerVNode.context = context.value;
|
||||
|
||||
context.value = nextValue;
|
||||
}
|
||||
|
||||
function resetContextCtx(providerVNode: VNode) {
|
||||
export function resetContext(providerVNode: VNode) {
|
||||
const context: ContextType<any> = providerVNode.type._context;
|
||||
|
||||
context.value = getContext(providerVNode, CTX_CONTEXT);
|
||||
context.value = providerVNode.context;
|
||||
}
|
||||
|
||||
// 在局部更新时,恢复父节点的context
|
||||
function recoverParentsContextCtx(vNode: VNode) {
|
||||
export function recoverParentContext(vNode: VNode) {
|
||||
let parent = vNode.parent;
|
||||
|
||||
while (parent !== null) {
|
||||
if (parent.tag === ContextProvider) {
|
||||
const newValue = parent.props.value;
|
||||
setContextCtx(parent, newValue);
|
||||
setContext(parent, parent.props.value);
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
}
|
||||
|
||||
// ctxOldContext是 旧context提供者的context
|
||||
function setVNodeOldContext(providerVNode: VNode, context: Object) {
|
||||
setContext(providerVNode, CTX_OLD_CONTEXT, context);
|
||||
}
|
||||
|
||||
function getVNodeOldContext(vNode: VNode) {
|
||||
return getContext(vNode, CTX_OLD_CONTEXT);
|
||||
}
|
||||
|
||||
function setOldContextCtx(providerVNode: VNode, context: Object) {
|
||||
setVNodeOldContext(providerVNode, context);
|
||||
ctxOldContext = context;
|
||||
}
|
||||
|
||||
function getOldContextCtx() {
|
||||
return ctxOldContext;
|
||||
}
|
||||
|
||||
function resetOldContextCtx(vNode: VNode) {
|
||||
ctxOldContext = getVNodeOldContext(vNode);
|
||||
}
|
||||
|
||||
function setVNodeOldPreviousContext(providerVNode: VNode, context: Object) {
|
||||
setContext(providerVNode, CTX_OLD_PREVIOUS_CONTEXT, context);
|
||||
}
|
||||
|
||||
function getVNodeOldPreviousContext(vNode: VNode) {
|
||||
return getContext(vNode, CTX_OLD_PREVIOUS_CONTEXT);
|
||||
}
|
||||
|
||||
function setOldPreviousContextCtx(context: Object) {
|
||||
ctxOldPreviousContext = context;
|
||||
}
|
||||
|
||||
function getOldPreviousContextCtx() {
|
||||
return ctxOldPreviousContext;
|
||||
}
|
||||
|
||||
function setContextChangeCtx(providerVNode: VNode, didChange: boolean) {
|
||||
setContext(providerVNode, CTX_OLD_CHANGE, didChange);
|
||||
ctxOldChange = didChange;
|
||||
}
|
||||
|
||||
function getContextChangeCtx() {
|
||||
return ctxOldChange;
|
||||
}
|
||||
|
||||
function resetContextChangeCtx(vNode: VNode) {
|
||||
ctxOldChange = getContext(vNode, CTX_OLD_CHANGE);
|
||||
}
|
||||
|
||||
export {
|
||||
getNamespaceCtx,
|
||||
resetNamespaceCtx,
|
||||
setNamespaceCtx,
|
||||
setContextCtx,
|
||||
resetContextCtx,
|
||||
recoverParentsContextCtx,
|
||||
setOldContextCtx,
|
||||
getOldContextCtx,
|
||||
resetOldContextCtx,
|
||||
setContextChangeCtx,
|
||||
getContextChangeCtx,
|
||||
resetContextChangeCtx,
|
||||
setOldPreviousContextCtx,
|
||||
getOldPreviousContextCtx,
|
||||
setVNodeOldContext,
|
||||
getVNodeOldContext,
|
||||
setVNodeOldPreviousContext,
|
||||
getVNodeOldPreviousContext,
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* 异常错误处理
|
||||
*/
|
||||
|
||||
import type {VNode} from './Types';
|
||||
import type { PromiseType, VNode } from './Types';
|
||||
import type {Update} from './UpdateHandler';
|
||||
|
||||
import {ClassComponent, TreeRoot} from './vnode/VNodeTags';
|
||||
|
@ -62,7 +62,9 @@ function createClassErrorUpdate(
|
|||
}
|
||||
return update;
|
||||
}
|
||||
|
||||
function isPromise(error: any): error is PromiseType<any> {
|
||||
return error !== null && typeof error === 'object' && typeof error.then === 'function'
|
||||
}
|
||||
// 处理capture和bubble阶段抛出的错误
|
||||
export function handleRenderThrowError(
|
||||
sourceVNode: VNode,
|
||||
|
@ -74,7 +76,7 @@ export function handleRenderThrowError(
|
|||
sourceVNode.dirtyNodes = null;
|
||||
|
||||
// error是个promise
|
||||
if (error !== null && typeof error === 'object' && typeof error.then === 'function') {
|
||||
if (isPromise(error)) {
|
||||
// 抛出异常的节点,向上寻找,是否有suspense组件
|
||||
const foundSuspense = handleSuspenseChildThrowError(sourceVNode.parent, sourceVNode, error);
|
||||
if (foundSuspense) {
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
isExecuting,
|
||||
setExecuteMode
|
||||
} from './ExecuteMode';
|
||||
import { recoverParentsContextCtx, resetNamespaceCtx, setNamespaceCtx } from './ContextSaver';
|
||||
import { recoverParentContext, resetNamespaceCtx, setNamespaceCtx } from './ContextSaver';
|
||||
import {
|
||||
updateChildShouldUpdate,
|
||||
updateParentsChildShouldUpdate,
|
||||
|
@ -231,7 +231,7 @@ function buildVNodeTree(treeRoot: VNode) {
|
|||
}
|
||||
|
||||
// 恢复父节点的context
|
||||
recoverParentsContextCtx(startVNode);
|
||||
recoverParentContext(startVNode);
|
||||
}
|
||||
|
||||
// 重置环境变量,为重新进行深度遍历做准备
|
||||
|
|
|
@ -54,3 +54,10 @@ export interface PromiseType<R> {
|
|||
): void | PromiseType<U>;
|
||||
}
|
||||
|
||||
export interface SuspenseState {
|
||||
promiseSet: Set<PromiseType<any>> | null; // suspense组件的promise列表
|
||||
childStatus: string;
|
||||
oldChildStatus: string; // 上一次Suspense的Children是否显示
|
||||
didCapture: boolean; // suspense是否捕获了异常
|
||||
promiseResolved: boolean; // suspense的promise是否resolve
|
||||
}
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
import type {VNode} from '../../Types';
|
||||
|
||||
import {
|
||||
setOldContextCtx,
|
||||
setContextChangeCtx,
|
||||
getOldContextCtx,
|
||||
resetOldContextCtx,
|
||||
resetContextChangeCtx,
|
||||
setOldPreviousContextCtx,
|
||||
getOldPreviousContextCtx,
|
||||
setVNodeOldContext,
|
||||
getVNodeOldContext,
|
||||
setVNodeOldPreviousContext,
|
||||
getVNodeOldPreviousContext,
|
||||
} from '../../ContextSaver';
|
||||
|
||||
const emptyObject = {};
|
||||
|
||||
// 判断是否是过时的context的提供者
|
||||
export function isOldProvider(comp: Function): boolean {
|
||||
// @ts-ignore
|
||||
const childContextTypes = comp.childContextTypes;
|
||||
return childContextTypes !== null && childContextTypes !== undefined;
|
||||
}
|
||||
|
||||
// 判断是否是过时的context的消费者
|
||||
export function isOldConsumer(comp: Function): boolean {
|
||||
// @ts-ignore
|
||||
const contextTypes = comp.contextTypes;
|
||||
return contextTypes !== null && contextTypes !== undefined;
|
||||
}
|
||||
|
||||
// 如果是旧版context提供者,则缓存两个全局变量,上一个提供者提供的context和当前提供者提供的context
|
||||
export function cacheOldCtx(processing: VNode, hasOldContext: any): void {
|
||||
// 每一个context提供者都会更新ctxOldContext
|
||||
if (hasOldContext) {
|
||||
setOldPreviousContextCtx(getOldContextCtx());
|
||||
|
||||
const vNodeContext = getVNodeOldContext(processing) || emptyObject;
|
||||
setOldContextCtx(processing, vNodeContext);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前组件可以消费的context
|
||||
export function getOldContext(processing: VNode, clazz: Function, ifProvider: boolean) {
|
||||
const type = processing.type;
|
||||
// 不是context消费者, 则直接返回空对象
|
||||
if (!isOldConsumer(type)) {
|
||||
return emptyObject;
|
||||
}
|
||||
|
||||
// 当组件既是提供者,也是消费者时,取上一个context,不能直接取最新context,因为已经被更新为当前组件的context;
|
||||
// 当组件只是消费者时,则取最新context
|
||||
const parentContext = (ifProvider && isOldProvider(clazz)) ?
|
||||
getOldPreviousContextCtx() :
|
||||
getOldContextCtx();
|
||||
|
||||
// 除非父级context更改,否则不需要重新创建子context,直接取对应节点上存的。
|
||||
if (getVNodeOldPreviousContext(processing) === parentContext) {
|
||||
return getVNodeOldContext(processing);
|
||||
}
|
||||
|
||||
// 从父的context中取出子定义的context
|
||||
const context = {};
|
||||
for (const key in type.contextTypes) {
|
||||
context[key] = parentContext[key];
|
||||
}
|
||||
|
||||
// 缓存当前组件的context,最近祖先传递下来context,当前可消费的context
|
||||
setVNodeOldPreviousContext(processing, parentContext);
|
||||
setVNodeOldContext(processing, context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
// 重置context
|
||||
export function resetOldCtx(vNode: VNode): void {
|
||||
resetOldContextCtx(vNode);
|
||||
resetContextChangeCtx(vNode);
|
||||
}
|
||||
|
||||
// 当前组件是提供者,则需要合并祖先context和当前组件提供的context
|
||||
function handleContext(vNode: VNode, parentContext: Object): Object {
|
||||
const instance = vNode.realNode;
|
||||
|
||||
if (typeof instance.getChildContext !== 'function') {
|
||||
return parentContext;
|
||||
}
|
||||
|
||||
// 合并祖先提供的context和当前组件提供的context
|
||||
return {...parentContext, ...instance.getChildContext()};
|
||||
}
|
||||
|
||||
// 当前组件是context提供者,更新时,需要合并祖先context和当前组件提供的context
|
||||
export function updateOldContext(vNode: VNode): void {
|
||||
const ctx = handleContext(vNode, getOldPreviousContextCtx());
|
||||
// 更新context,给子组件用的context
|
||||
setOldContextCtx(vNode, ctx);
|
||||
// 标记更改
|
||||
setContextChangeCtx(vNode, true);
|
||||
}
|
|
@ -1,17 +1,15 @@
|
|||
import type {ContextType} from '../Types';
|
||||
|
||||
import hookMapping from './HookMapping';
|
||||
import {useRefImpl} from './UseRefHook';
|
||||
import {useEffectImpl, useLayoutEffectImpl} from './UseEffectHook';
|
||||
import {useCallbackImpl} from './UseCallbackHook';
|
||||
import {useMemoImpl} from './UseMemoHook';
|
||||
import {useImperativeHandleImpl} from './UseImperativeHook';
|
||||
|
||||
const {
|
||||
UseContextHookMapping,
|
||||
UseReducerHookMapping,
|
||||
UseStateHookMapping
|
||||
} = hookMapping;
|
||||
import {useReducerImpl} from './UseReducerHook';
|
||||
import {useStateImpl} from './UseStateHook';
|
||||
import {getNewContext} from '../components/context/Context';
|
||||
import {getProcessingVNode} from './BaseHook';
|
||||
import {Ref, Trigger} from './HookType';
|
||||
|
||||
type BasicStateAction<S> = ((S) => S) | S;
|
||||
type Dispatch<A> = (A) => void;
|
||||
|
@ -20,22 +18,23 @@ type Dispatch<A> = (A) => void;
|
|||
export function useContext<T>(
|
||||
Context: ContextType<T>,
|
||||
): T {
|
||||
return UseContextHookMapping.val.useContext(Context);
|
||||
const processingVNode = getProcessingVNode();
|
||||
return getNewContext(processingVNode!, Context, true);
|
||||
}
|
||||
|
||||
export function useState<S>(initialState: (() => S) | S,): [S, Dispatch<BasicStateAction<S>>] {
|
||||
return UseStateHookMapping.val.useState(initialState);
|
||||
return useStateImpl(initialState);
|
||||
}
|
||||
|
||||
export function useReducer<S, I, A>(
|
||||
reducer: (S, A) => S,
|
||||
initialArg: I,
|
||||
init?: (I) => S,
|
||||
): [S, Dispatch<A>] {
|
||||
return UseReducerHookMapping.val.useReducer(reducer, initialArg, init);
|
||||
): [S, Trigger<A>] | void {
|
||||
return useReducerImpl(reducer, initialArg, init);
|
||||
}
|
||||
|
||||
export function useRef<T>(initialValue: T): {current: T} {
|
||||
export function useRef<T>(initialValue: T): Ref<T> {
|
||||
return useRefImpl(initialValue);
|
||||
}
|
||||
|
||||
|
@ -74,3 +73,6 @@ export function useImperativeHandle<T>(
|
|||
): void {
|
||||
return useImperativeHandleImpl(ref, create, deps);
|
||||
}
|
||||
|
||||
// 兼容react-redux
|
||||
export const useDebugValue = () => {};
|
||||
|
|
|
@ -1,26 +1,15 @@
|
|||
import type {VNode} from '../Types';
|
||||
import hookMapping from './HookMapping';
|
||||
|
||||
const {
|
||||
UseStateHookMapping,
|
||||
UseReducerHookMapping,
|
||||
UseContextHookMapping,
|
||||
} = hookMapping;
|
||||
|
||||
import {getNewContext} from '../components/context/Context';
|
||||
import {
|
||||
getLastTimeHook,
|
||||
getProcessingVNode,
|
||||
setLastTimeHook,
|
||||
setProcessingVNode,
|
||||
setCurrentHook, getNextHook
|
||||
} from './BaseHook';
|
||||
import {useStateImpl} from './UseStateHook';
|
||||
import {useReducerImpl} from './UseReducerHook';
|
||||
import {HookStage, setHookStage} from './HookStage';
|
||||
|
||||
// hook对外入口
|
||||
export function exeFunctionHook<Props extends Record<string, any>, Arg>(
|
||||
export function runFunctionWithHooks<Props extends Record<string, any>, Arg>(
|
||||
funcComp: (props: Props, arg: Arg) => any,
|
||||
props: Props,
|
||||
arg: Arg,
|
||||
|
@ -29,9 +18,6 @@ export function exeFunctionHook<Props extends Record<string, any>, Arg>(
|
|||
// 重置全局变量
|
||||
resetGlobalVariable();
|
||||
|
||||
// 初始化hook实现函数
|
||||
initHookMapping();
|
||||
|
||||
setProcessingVNode(processing);
|
||||
|
||||
processing.oldHooks = processing.hooks;
|
||||
|
@ -71,8 +57,3 @@ function resetGlobalVariable() {
|
|||
setCurrentHook(null);
|
||||
}
|
||||
|
||||
export function initHookMapping() {
|
||||
UseContextHookMapping.val = {useContext: context => getNewContext(getProcessingVNode(), context, true)};
|
||||
UseReducerHookMapping.val = {useReducer: useReducerImpl};
|
||||
UseStateHookMapping.val = {useState: useStateImpl};
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
/**
|
||||
* 暂时用于解决测试代码无法运行问题,估计是:测试代码会循环或者重复依赖
|
||||
*/
|
||||
|
||||
import type {
|
||||
UseContextHookType,
|
||||
UseReducerHookType,
|
||||
UseStateHookType
|
||||
} from '../Types';
|
||||
|
||||
const UseStateHookMapping: {val: (null | UseStateHookType)} = {val: null};
|
||||
const UseReducerHookMapping: {val: (null | UseReducerHookType)} = {val: null};
|
||||
const UseContextHookMapping: {val: (null | UseContextHookType)} = {val: null};
|
||||
|
||||
const hookMapping = {
|
||||
UseStateHookMapping,
|
||||
UseReducerHookMapping,
|
||||
UseContextHookMapping,
|
||||
}
|
||||
|
||||
export default hookMapping;
|
|
@ -1,15 +1,13 @@
|
|||
import type { VNode } from '../Types';
|
||||
|
||||
import {cacheOldCtx, isOldProvider} from '../components/context/CompatibleContext';
|
||||
import {
|
||||
ClassComponent,
|
||||
ContextProvider,
|
||||
DomComponent,
|
||||
DomPortal,
|
||||
TreeRoot,
|
||||
SuspenseComponent,
|
||||
} from '../vnode/VNodeTags';
|
||||
import { getContextChangeCtx, setContextCtx, setNamespaceCtx } from '../ContextSaver';
|
||||
import { setContext, setNamespaceCtx } from '../ContextSaver';
|
||||
import { FlagUtils } from '../vnode/VNodeFlags';
|
||||
import {onlyUpdateChildVNodes} from '../vnode/VNodeCreator';
|
||||
import componentRenders from './index';
|
||||
|
@ -23,17 +21,12 @@ function handlerContext(processing: VNode) {
|
|||
case DomComponent:
|
||||
setNamespaceCtx(processing);
|
||||
break;
|
||||
case ClassComponent: {
|
||||
const isOldCxtExist = isOldProvider(processing.type);
|
||||
cacheOldCtx(processing, isOldCxtExist);
|
||||
break;
|
||||
}
|
||||
case DomPortal:
|
||||
setNamespaceCtx(processing, processing.realNode);
|
||||
break;
|
||||
case ContextProvider: {
|
||||
const newValue = processing.props.value;
|
||||
setContextCtx(processing, newValue);
|
||||
setContext(processing, newValue);
|
||||
break;
|
||||
}
|
||||
// No Default
|
||||
|
@ -48,7 +41,6 @@ export function captureVNode(processing: VNode): VNode | null {
|
|||
if (
|
||||
!processing.isCreated &&
|
||||
processing.oldProps === processing.props &&
|
||||
!getContextChangeCtx() &&
|
||||
!processing.shouldUpdate
|
||||
) {
|
||||
// 复用还需对stack进行处理
|
||||
|
|
|
@ -2,13 +2,6 @@ import type { VNode } from '../Types';
|
|||
|
||||
import { mergeDefaultProps } from './LazyComponent';
|
||||
import { getNewContext, resetDepContexts } from '../components/context/Context';
|
||||
import {
|
||||
cacheOldCtx,
|
||||
getOldContext,
|
||||
isOldProvider,
|
||||
resetOldCtx,
|
||||
updateOldContext,
|
||||
} from '../components/context/CompatibleContext';
|
||||
import {
|
||||
callComponentWillMount,
|
||||
callComponentWillReceiveProps,
|
||||
|
@ -25,39 +18,39 @@ import { markRef } from './BaseComponent';
|
|||
import {
|
||||
processUpdates,
|
||||
} from '../UpdateHandler';
|
||||
import { getContextChangeCtx, setContextChangeCtx } from '../ContextSaver';
|
||||
import { setProcessingClassVNode } from '../GlobalVar';
|
||||
import { onlyUpdateChildVNodes } from '../vnode/VNodeCreator';
|
||||
import { createChildrenByDiff } from '../diff/nodeDiffComparator';
|
||||
|
||||
const emptyContextObj = {};
|
||||
// 获取当前节点的context
|
||||
export function getCurrentContext(clazz, processing: VNode) {
|
||||
const context = clazz.contextType;
|
||||
return typeof context === 'object' && context !== null
|
||||
? getNewContext(processing, context)
|
||||
: getOldContext(processing, clazz, true);
|
||||
: emptyContextObj;
|
||||
}
|
||||
|
||||
// 挂载实例
|
||||
function mountInstance(clazz, processing: VNode, nextProps: object) {
|
||||
function mountInstance(ctor, processing: VNode, nextProps: object) {
|
||||
if (!processing.isCreated) {
|
||||
processing.isCreated = true;
|
||||
FlagUtils.markAddition(processing);
|
||||
}
|
||||
|
||||
// 构造实例
|
||||
const inst = callConstructor(processing, clazz, nextProps);
|
||||
const inst = callConstructor(processing, ctor, nextProps);
|
||||
|
||||
inst.props = nextProps;
|
||||
inst.state = processing.state;
|
||||
inst.context = getCurrentContext(clazz, processing);
|
||||
inst.context = getCurrentContext(ctor, processing);
|
||||
inst.refs = {};
|
||||
|
||||
processUpdates(processing, inst, nextProps);
|
||||
inst.state = processing.state;
|
||||
|
||||
// 在调用类组建的渲染方法之前调用 并且在初始挂载及后续更新时都会被调用
|
||||
callDerivedStateFromProps(processing, clazz.getDerivedStateFromProps, nextProps);
|
||||
callDerivedStateFromProps(processing, ctor.getDerivedStateFromProps, nextProps);
|
||||
callComponentWillMount(processing, inst, nextProps);
|
||||
|
||||
markComponentDidMount(processing);
|
||||
|
@ -93,7 +86,7 @@ function callUpdateLifeCycle(processing: VNode, nextProps: object, clazz) {
|
|||
}
|
||||
}
|
||||
|
||||
function markLifeCycle(processing: VNode, nextProps: object, shouldUpdate: Boolean) {
|
||||
function markLifeCycle(processing: VNode, nextProps: object, shouldUpdate: boolean) {
|
||||
if (processing.isCreated) {
|
||||
markComponentDidMount(processing);
|
||||
} else if (processing.state !== processing.oldState || shouldUpdate) {
|
||||
|
@ -104,28 +97,30 @@ function markLifeCycle(processing: VNode, nextProps: object, shouldUpdate: Boole
|
|||
|
||||
// 用于类组件
|
||||
export function captureRender(processing: VNode): VNode | null {
|
||||
let clazz = processing.type;
|
||||
const ctor = processing.type;
|
||||
let nextProps = processing.props;
|
||||
if (processing.isLazyComponent) {
|
||||
nextProps = mergeDefaultProps(clazz, nextProps);
|
||||
if (processing.promiseResolve) { // 该函数被 lazy 组件使用,未加载的组件需要加载组件的真实内容
|
||||
clazz = clazz._load(clazz._content);
|
||||
}
|
||||
nextProps = mergeDefaultProps(ctor, nextProps);
|
||||
}
|
||||
const isOldCxtExist = isOldProvider(clazz);
|
||||
cacheOldCtx(processing, isOldCxtExist);
|
||||
|
||||
resetDepContexts(processing);
|
||||
|
||||
// suspense打断后,再次render只需初次渲染
|
||||
if (processing.isSuspended) {
|
||||
mountInstance(ctor, processing, nextProps);
|
||||
processing.isSuspended = false;
|
||||
return createChildren(ctor, processing);
|
||||
}
|
||||
|
||||
// 通过 shouldUpdate 判断是否要复用 children,该值和props,state,context的变化,shouldComponentUpdate,forceUpdate api的调用结果有关
|
||||
let shouldUpdate;
|
||||
const inst = processing.realNode;
|
||||
if (inst === null) {
|
||||
// 挂载新组件,一定会更新
|
||||
mountInstance(clazz, processing, nextProps);
|
||||
mountInstance(ctor, processing, nextProps);
|
||||
shouldUpdate = true;
|
||||
} else { // 更新
|
||||
const newContext = getCurrentContext(clazz, processing);
|
||||
const newContext = getCurrentContext(ctor, processing);
|
||||
|
||||
// 子节点抛出异常时,如果本class是个捕获异常的处理节点,这时候oldProps是null,所以需要使用props
|
||||
const oldProps = (processing.flags & DidCapture) === DidCapture ? processing.props : processing.oldProps;
|
||||
|
@ -140,18 +135,17 @@ export function captureRender(processing: VNode): VNode | null {
|
|||
// 如果 props, state, context 都没有变化且 isForceUpdate 为 false则不需要更新
|
||||
shouldUpdate = oldProps !== processing.props ||
|
||||
inst.state !== processing.state ||
|
||||
getContextChangeCtx() ||
|
||||
processing.isForceUpdate;
|
||||
|
||||
if (shouldUpdate) {
|
||||
// derivedStateFromProps会修改nextState,因此需要调用
|
||||
callDerivedStateFromProps(processing, clazz.getDerivedStateFromProps, nextProps);
|
||||
callDerivedStateFromProps(processing, ctor.getDerivedStateFromProps, nextProps);
|
||||
if (!processing.isForceUpdate) {
|
||||
// 业务可以通过 shouldComponentUpdate 函数进行优化阻止更新
|
||||
shouldUpdate = callShouldComponentUpdate(processing, oldProps, nextProps, processing.state, newContext);
|
||||
}
|
||||
if (shouldUpdate) {
|
||||
callUpdateLifeCycle(processing, nextProps, clazz);
|
||||
callUpdateLifeCycle(processing, nextProps, ctor);
|
||||
}
|
||||
inst.state = processing.state;
|
||||
inst.context = newContext;
|
||||
|
@ -170,27 +164,10 @@ export function captureRender(processing: VNode): VNode | null {
|
|||
|
||||
// 不复用
|
||||
if (shouldUpdate) {
|
||||
// 更新context
|
||||
if (isOldCxtExist) {
|
||||
updateOldContext(processing);
|
||||
}
|
||||
return createChildren(clazz, processing);
|
||||
return createChildren(ctor, processing);
|
||||
} else {
|
||||
if (isOldCxtExist) {
|
||||
setContextChangeCtx(processing, false);
|
||||
}
|
||||
return onlyUpdateChildVNodes(processing);
|
||||
}
|
||||
}
|
||||
|
||||
export function bubbleRender(processing: VNode) {
|
||||
if (isOldProvider(processing.type)) {
|
||||
resetOldCtx(processing);
|
||||
}
|
||||
}
|
||||
|
||||
// 用于未完成的类组件
|
||||
export function getIncompleteClassComponent(clazz, processing: VNode, nextProps: object): VNode | null {
|
||||
mountInstance(clazz, processing, nextProps);
|
||||
return createChildren(clazz, processing);
|
||||
}
|
||||
export function bubbleRender() {}
|
||||
|
|
|
@ -4,9 +4,8 @@ import { isSame } from '../utils/compare';
|
|||
import { ClassComponent, ContextProvider } from '../vnode/VNodeTags';
|
||||
import { pushForceUpdate } from '../UpdateHandler';
|
||||
import {
|
||||
getContextChangeCtx,
|
||||
resetContextCtx,
|
||||
setContextCtx,
|
||||
resetContext,
|
||||
setContext,
|
||||
} from '../ContextSaver';
|
||||
import { travelVNodeTree } from '../vnode/VNodeUtils';
|
||||
import { launchUpdateFromVNode } from '../TreeBuilder';
|
||||
|
@ -75,14 +74,14 @@ function captureContextProvider(processing: VNode): VNode | null {
|
|||
const newCtx = newProps.value;
|
||||
|
||||
// 更新processing的context值为newProps.value
|
||||
setContextCtx(processing, newCtx);
|
||||
setContext(processing, newCtx);
|
||||
|
||||
if (oldProps !== null) {
|
||||
const oldCtx = oldProps.value;
|
||||
const isSameContext = isSame(oldCtx, newCtx);
|
||||
if (isSameContext) {
|
||||
// context没有改变,复用
|
||||
if (oldProps.children === newProps.children && !getContextChangeCtx()) {
|
||||
if (oldProps.children === newProps.children) {
|
||||
return onlyUpdateChildVNodes(processing);
|
||||
}
|
||||
} else {
|
||||
|
@ -101,6 +100,6 @@ export function captureRender(processing: VNode): VNode | null {
|
|||
}
|
||||
|
||||
export function bubbleRender(processing: VNode) {
|
||||
resetContextCtx(processing);
|
||||
resetContext(processing);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type {VNode} from '../Types';
|
||||
import {captureRender as funCaptureRender} from './FunctionComponent';
|
||||
|
||||
export function captureRender(processing: VNode, shouldUpdate?: boolean): VNode | null {
|
||||
return funCaptureRender(processing, shouldUpdate);
|
||||
export function captureRender(processing: VNode): VNode | null {
|
||||
return funCaptureRender(processing);
|
||||
}
|
||||
|
||||
export function bubbleRender() {}
|
||||
|
|
|
@ -1,43 +1,24 @@
|
|||
import type {VNode} from '../Types';
|
||||
import type { VNode } from '../Types';
|
||||
|
||||
import {mergeDefaultProps} from './LazyComponent';
|
||||
import {getOldContext} from '../components/context/CompatibleContext';
|
||||
import {resetDepContexts} from '../components/context/Context';
|
||||
import {exeFunctionHook} from '../hooks/HookMain';
|
||||
import {ForwardRef} from '../vnode/VNodeTags';
|
||||
import {FlagUtils, Update} from '../vnode/VNodeFlags';
|
||||
import {getContextChangeCtx} from '../ContextSaver';
|
||||
import {onlyUpdateChildVNodes} from '../vnode/VNodeCreator';
|
||||
import { mergeDefaultProps } from './LazyComponent';
|
||||
import { resetDepContexts } from '../components/context/Context';
|
||||
import { runFunctionWithHooks } from '../hooks/HookMain';
|
||||
import { ForwardRef } from '../vnode/VNodeTags';
|
||||
import { FlagUtils, Update } from '../vnode/VNodeFlags';
|
||||
import { onlyUpdateChildVNodes } from '../vnode/VNodeCreator';
|
||||
import { createChildrenByDiff } from '../diff/nodeDiffComparator';
|
||||
|
||||
// 在useState, useReducer的时候,会触发state变化
|
||||
let stateChange = false;
|
||||
|
||||
export function bubbleRender() {}
|
||||
export function bubbleRender() {
|
||||
}
|
||||
|
||||
// 判断children是否可以复用
|
||||
function checkIfCanReuseChildren(processing: VNode, shouldUpdate?: boolean) {
|
||||
let isCanReuse = true;
|
||||
|
||||
if (!processing.isCreated) {
|
||||
const oldProps = processing.oldProps;
|
||||
const newProps = processing.props;
|
||||
|
||||
// 如果props或者context改变了
|
||||
if (oldProps !== newProps || getContextChangeCtx() || processing.isDepContextChange) {
|
||||
isCanReuse = false;
|
||||
} else {
|
||||
if (shouldUpdate && processing.suspenseChildThrow) {
|
||||
// 使用完后恢复
|
||||
processing.suspenseChildThrow = false;
|
||||
isCanReuse = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isCanReuse = false;
|
||||
}
|
||||
|
||||
return isCanReuse;
|
||||
function checkIfCanReuseChildren(processing: VNode) {
|
||||
return !processing.isCreated &&
|
||||
processing.oldProps === processing.props &&
|
||||
!processing.isDepContextChange;
|
||||
}
|
||||
|
||||
export function setStateChange(isUpdate) {
|
||||
|
@ -52,38 +33,42 @@ export function captureFunctionComponent(
|
|||
processing: VNode,
|
||||
funcComp: any,
|
||||
nextProps: any,
|
||||
shouldUpdate?: boolean
|
||||
) {
|
||||
let context;
|
||||
if (processing.tag !== ForwardRef) {
|
||||
context = getOldContext(processing, funcComp, true);
|
||||
}
|
||||
// 函数组件内已完成异步动作
|
||||
if (processing.isSuspended) {
|
||||
// 由于首次被打断,应仍为首次渲染
|
||||
processing.isCreated = true;
|
||||
FlagUtils.markAddition(processing);
|
||||
|
||||
processing.isSuspended = false;
|
||||
}
|
||||
resetDepContexts(processing);
|
||||
|
||||
const isCanReuse = checkIfCanReuseChildren(processing, shouldUpdate);
|
||||
const isCanReuse = checkIfCanReuseChildren(processing);
|
||||
// 在执行exeFunctionHook前先设置stateChange为false
|
||||
setStateChange(false);
|
||||
|
||||
const newElements = exeFunctionHook(
|
||||
const newElements = runFunctionWithHooks(
|
||||
processing.tag === ForwardRef ? funcComp.render : funcComp,
|
||||
nextProps,
|
||||
processing.tag === ForwardRef ? processing.ref : context,
|
||||
processing.tag === ForwardRef ? processing.ref : undefined,
|
||||
processing,
|
||||
);
|
||||
|
||||
// 这里需要判断是否可以复用,因为函数组件比起其他组价,多了context和stateChange两个因素
|
||||
if (isCanReuse && !isStateChange()) {
|
||||
if (isCanReuse && !isStateChange() && !processing.isStoreChange) {
|
||||
FlagUtils.removeFlag(processing, Update);
|
||||
|
||||
return onlyUpdateChildVNodes(processing);
|
||||
}
|
||||
|
||||
processing.isStoreChange = false;
|
||||
|
||||
processing.child = createChildrenByDiff(processing, processing.child, newElements, !processing.isCreated);
|
||||
return processing.child;
|
||||
}
|
||||
|
||||
export function captureRender(processing: VNode, shouldUpdate?: boolean): VNode | null {
|
||||
export function captureRender(processing: VNode): VNode | null {
|
||||
const Component = processing.type;
|
||||
const unresolvedProps = processing.props;
|
||||
const resolvedProps =
|
||||
|
@ -95,7 +80,6 @@ export function captureRender(processing: VNode, shouldUpdate?: boolean): VNode
|
|||
processing,
|
||||
Component,
|
||||
resolvedProps,
|
||||
shouldUpdate
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
import type {VNode} from '../Types';
|
||||
|
||||
import {mergeDefaultProps} from './LazyComponent';
|
||||
import {ClassComponent} from '../vnode/VNodeTags';
|
||||
import {resetDepContexts} from '../components/context/Context';
|
||||
import {getIncompleteClassComponent} from './ClassComponent';
|
||||
import {
|
||||
isOldProvider,
|
||||
resetOldCtx,
|
||||
cacheOldCtx,
|
||||
} from '../components/context/CompatibleContext';
|
||||
|
||||
function captureIncompleteClassComponent(processing, Component, nextProps) {
|
||||
processing.tag = ClassComponent;
|
||||
|
||||
const hasOldContext = isOldProvider(Component);
|
||||
cacheOldCtx(processing, hasOldContext);
|
||||
|
||||
resetDepContexts(processing);
|
||||
|
||||
return getIncompleteClassComponent(Component, processing, nextProps);
|
||||
}
|
||||
|
||||
export function captureRender(processing: VNode): VNode | null {
|
||||
const Component = processing.type;
|
||||
const unresolvedProps = processing.props;
|
||||
const resolvedProps =
|
||||
processing.isLazyComponent
|
||||
? mergeDefaultProps(Component, unresolvedProps)
|
||||
: unresolvedProps;
|
||||
|
||||
return captureIncompleteClassComponent(processing, Component, resolvedProps);
|
||||
}
|
||||
|
||||
export function bubbleRender(processing: VNode) {
|
||||
// 处理与类组件相同。
|
||||
const Component = processing.type;
|
||||
if (isOldProvider(Component)) {
|
||||
resetOldCtx(processing);
|
||||
}
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
import type {VNode, PromiseType} from '../Types';
|
||||
import type { VNode, PromiseType } from '../Types';
|
||||
|
||||
import {FlagUtils, Interrupted} from '../vnode/VNodeFlags';
|
||||
import {onlyUpdateChildVNodes, updateVNode, createFragmentVNode} from '../vnode/VNodeCreator';
|
||||
import { FlagUtils, Interrupted } from '../vnode/VNodeFlags';
|
||||
import { onlyUpdateChildVNodes, updateVNode, createFragmentVNode } from '../vnode/VNodeCreator';
|
||||
import {
|
||||
ClassComponent,
|
||||
IncompleteClassComponent,
|
||||
ForwardRef,
|
||||
FunctionComponent,
|
||||
SuspenseComponent,
|
||||
} from '../vnode/VNodeTags';
|
||||
import {pushForceUpdate} from '../UpdateHandler';
|
||||
import {launchUpdateFromVNode, tryRenderFromRoot} from '../TreeBuilder';
|
||||
import {updateShouldUpdateOfTree} from '../vnode/VNodeShouldUpdate';
|
||||
import {getContextChangeCtx} from '../ContextSaver';
|
||||
import { pushForceUpdate } from '../UpdateHandler';
|
||||
import { launchUpdateFromVNode, tryRenderFromRoot } from '../TreeBuilder';
|
||||
import { updateShouldUpdateOfTree } from '../vnode/VNodeShouldUpdate';
|
||||
import { markVNodePath } from '../utils/vNodePath';
|
||||
|
||||
export enum SuspenseChildStatus {
|
||||
|
@ -21,7 +21,7 @@ export enum SuspenseChildStatus {
|
|||
|
||||
// 创建fallback子节点
|
||||
function createFallback(processing: VNode, fallbackChildren) {
|
||||
const childFragment: VNode = processing.child;
|
||||
const childFragment: VNode = processing.child!;
|
||||
let fallbackFragment;
|
||||
childFragment.childShouldUpdate = false;
|
||||
|
||||
|
@ -46,7 +46,7 @@ function createFallback(processing: VNode, fallbackChildren) {
|
|||
fallbackFragment.eIndex = 1;
|
||||
fallbackFragment.cIndex = 1;
|
||||
markVNodePath(fallbackFragment);
|
||||
processing.suspenseChildStatus = SuspenseChildStatus.ShowFallback;
|
||||
processing.suspenseState.childStatus = SuspenseChildStatus.ShowFallback;
|
||||
|
||||
return fallbackFragment;
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ function createSuspenseChildren(processing: VNode, newChildren) {
|
|||
processing.dirtyNodes = [oldFallbackFragment];
|
||||
}
|
||||
// SuspenseComponent 中使用
|
||||
processing.suspenseChildStatus = SuspenseChildStatus.ShowChild;
|
||||
processing.suspenseState.childStatus = SuspenseChildStatus.ShowChild;
|
||||
} else {
|
||||
childFragment = createFragmentVNode(null, newChildren);
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ function createSuspenseChildren(processing: VNode, newChildren) {
|
|||
childFragment.cIndex = 0;
|
||||
markVNodePath(childFragment);
|
||||
processing.child = childFragment;
|
||||
processing.promiseResolve = false;
|
||||
processing.suspenseState.promiseResolved = false;
|
||||
return processing.child;
|
||||
}
|
||||
|
||||
|
@ -87,10 +87,10 @@ export function captureSuspenseComponent(processing: VNode) {
|
|||
const nextProps = processing.props;
|
||||
|
||||
// suspense被捕获后需要展示fallback
|
||||
const showFallback = processing.suspenseDidCapture;
|
||||
const showFallback = processing.suspenseState.didCapture;
|
||||
|
||||
if (showFallback) {
|
||||
processing.suspenseDidCapture = false;
|
||||
processing.suspenseState.didCapture = false;
|
||||
const nextFallbackChildren = nextProps.fallback;
|
||||
return createFallback(processing, nextFallbackChildren);
|
||||
} else {
|
||||
|
@ -100,15 +100,15 @@ export function captureSuspenseComponent(processing: VNode) {
|
|||
}
|
||||
|
||||
function updateFallback(processing: VNode): Array<VNode> | VNode | null {
|
||||
const childFragment: VNode | null= processing.child;
|
||||
const childFragment: VNode | null = processing.child;
|
||||
|
||||
if (childFragment?.childShouldUpdate) {
|
||||
if (processing.promiseResolve) {
|
||||
if (processing.suspenseState.promiseResolved) {
|
||||
// promise已完成,展示promise返回的新节点
|
||||
return captureSuspenseComponent(processing);
|
||||
} else {
|
||||
// promise未完成,继续显示fallback,不需要继续刷新子节点
|
||||
const fallbackFragment: VNode = processing.child.next;
|
||||
const fallbackFragment: VNode = processing.child!.next!;
|
||||
childFragment.childShouldUpdate = false;
|
||||
fallbackFragment.childShouldUpdate = false;
|
||||
return null;
|
||||
|
@ -129,10 +129,9 @@ export function captureRender(processing: VNode, shouldUpdate: boolean): Array<V
|
|||
if (
|
||||
!processing.isCreated &&
|
||||
processing.oldProps === processing.props &&
|
||||
!getContextChangeCtx() &&
|
||||
!shouldUpdate
|
||||
) {
|
||||
if (processing.suspenseChildStatus === SuspenseChildStatus.ShowFallback) {
|
||||
if (processing.suspenseState.childStatus === SuspenseChildStatus.ShowFallback) {
|
||||
// 当显示fallback时,suspense的子组件要更新
|
||||
return updateFallback(processing);
|
||||
}
|
||||
|
@ -143,8 +142,9 @@ export function captureRender(processing: VNode, shouldUpdate: boolean): Array<V
|
|||
}
|
||||
|
||||
export function bubbleRender(processing: VNode) {
|
||||
if (processing.suspenseChildStatus === SuspenseChildStatus.ShowFallback
|
||||
|| (!processing.isCreated && processing.oldSuspenseChildStatus === SuspenseChildStatus.ShowFallback)
|
||||
const { childStatus, oldChildStatus } = processing.suspenseState;
|
||||
if (childStatus === SuspenseChildStatus.ShowFallback
|
||||
|| (!processing.isCreated && oldChildStatus === SuspenseChildStatus.ShowFallback)
|
||||
) {
|
||||
FlagUtils.markUpdate(processing);
|
||||
}
|
||||
|
@ -153,22 +153,21 @@ export function bubbleRender(processing: VNode) {
|
|||
}
|
||||
|
||||
function canCapturePromise(vNode: VNode | null): boolean {
|
||||
return vNode?.suspenseChildStatus !== SuspenseChildStatus.ShowFallback && vNode?.props.fallback !== undefined;
|
||||
return vNode?.suspenseState.childStatus !== SuspenseChildStatus.ShowFallback && vNode?.props.fallback !== undefined;
|
||||
}
|
||||
|
||||
// 处理Suspense子组件抛出的promise
|
||||
export function handleSuspenseChildThrowError(parent: VNode, processing: VNode, error: any): boolean {
|
||||
export function handleSuspenseChildThrowError(parent: VNode, processing: VNode, promise: PromiseType<any>): boolean {
|
||||
let vNode = parent;
|
||||
|
||||
// 向上找到最近的不在fallback状态的Suspense,并触发重新渲染
|
||||
do {
|
||||
if (vNode.tag === SuspenseComponent && canCapturePromise(vNode)) {
|
||||
if (vNode.suspensePromises === null) {
|
||||
vNode.suspensePromises = new Set();
|
||||
if (vNode.suspenseState.promiseSet === null) {
|
||||
vNode.suspenseState.promiseSet = new Set();
|
||||
}
|
||||
vNode.suspensePromises.add(error);
|
||||
vNode.suspenseState.promiseSet.add(promise);
|
||||
|
||||
processing.suspenseChildThrow = true;
|
||||
|
||||
// 移除生命周期flag 和 中断flag
|
||||
FlagUtils.removeLifecycleEffectFlags(processing);
|
||||
|
@ -177,7 +176,7 @@ export function handleSuspenseChildThrowError(parent: VNode, processing: VNode,
|
|||
if (processing.tag === ClassComponent) {
|
||||
if (processing.isCreated) {
|
||||
// 渲染类组件场景,要标志未完成(否则会触发componentWillUnmount)
|
||||
processing.tag = IncompleteClassComponent;
|
||||
processing.isSuspended = true;
|
||||
} else {
|
||||
// 类组件更新,标记强制更新,否则被memo等优化跳过
|
||||
pushForceUpdate(processing);
|
||||
|
@ -185,10 +184,13 @@ export function handleSuspenseChildThrowError(parent: VNode, processing: VNode,
|
|||
}
|
||||
}
|
||||
|
||||
if (processing.tag === FunctionComponent || processing.tag === ForwardRef) {
|
||||
processing.isSuspended = true;
|
||||
}
|
||||
// 应该抛出promise未完成更新,标志待更新
|
||||
processing.shouldUpdate = true;
|
||||
|
||||
vNode.suspenseDidCapture = true;
|
||||
vNode.suspenseState.didCapture = true;
|
||||
launchUpdateFromVNode(vNode);
|
||||
|
||||
return true;
|
||||
|
@ -207,7 +209,7 @@ function resolvePromise(suspenseVNode: VNode, promise: PromiseType<any>) {
|
|||
if (promiseCache !== null) {
|
||||
promiseCache.delete(promise);
|
||||
}
|
||||
suspenseVNode.promiseResolve = true;
|
||||
suspenseVNode.suspenseState.promiseResolved = true;
|
||||
const root = updateShouldUpdateOfTree(suspenseVNode);
|
||||
if (root !== null) {
|
||||
tryRenderFromRoot(root);
|
||||
|
@ -216,14 +218,13 @@ function resolvePromise(suspenseVNode: VNode, promise: PromiseType<any>) {
|
|||
|
||||
// 对于每个promise,添加一个侦听器,以便当它resolve时,重新渲染
|
||||
export function listenToPromise(suspenseVNode: VNode) {
|
||||
const promises: Set<PromiseType<any>> | null = suspenseVNode.suspensePromises;
|
||||
const promises: Set<PromiseType<any>> | null = suspenseVNode.suspenseState.promiseSet;
|
||||
if (promises !== null) {
|
||||
suspenseVNode.suspensePromises = null;
|
||||
suspenseVNode.suspenseState.promiseSet = null;
|
||||
|
||||
// 记录已经监听的 promise
|
||||
let promiseCache = suspenseVNode.realNode;
|
||||
if (promiseCache === null) {
|
||||
// @ts-ignore
|
||||
promiseCache = new PossiblyWeakSet();
|
||||
suspenseVNode.realNode = new PossiblyWeakSet();
|
||||
}
|
||||
|
|
|
@ -2,13 +2,11 @@ import type {VNode} from '../Types';
|
|||
import {throwIfTrue} from '../utils/throwIfTrue';
|
||||
import {processUpdates} from '../UpdateHandler';
|
||||
import {resetNamespaceCtx, setNamespaceCtx} from '../ContextSaver';
|
||||
import {resetOldCtx} from '../components/context/CompatibleContext';
|
||||
import {onlyUpdateChildVNodes} from '../vnode/VNodeCreator';
|
||||
import { createChildrenByDiff } from '../diff/nodeDiffComparator';
|
||||
|
||||
export function bubbleRender(processing: VNode) {
|
||||
resetNamespaceCtx(processing);
|
||||
resetOldCtx(processing);
|
||||
}
|
||||
|
||||
function updateTreeRoot(processing) {
|
||||
|
|
|
@ -9,7 +9,6 @@ import * as DomComponentRender from './DomComponent';
|
|||
import * as DomPortalRender from './DomPortal';
|
||||
import * as TreeRootRender from './TreeRoot';
|
||||
import * as DomTextRender from './DomText';
|
||||
import * as IncompleteClassComponentRender from './IncompleteClassComponent';
|
||||
import * as LazyComponentRender from './LazyComponent';
|
||||
import * as MemoComponentRender from './MemoComponent';
|
||||
import * as SuspenseComponentRender from './SuspenseComponent';
|
||||
|
@ -25,15 +24,14 @@ import {
|
|||
DomPortal,
|
||||
TreeRoot,
|
||||
DomText,
|
||||
IncompleteClassComponent,
|
||||
LazyComponent,
|
||||
MemoComponent,
|
||||
SuspenseComponent,
|
||||
} from '../vnode/VNodeTags';
|
||||
|
||||
export {
|
||||
BaseComponentRender
|
||||
}
|
||||
BaseComponentRender,
|
||||
};
|
||||
|
||||
export default {
|
||||
[ClassComponent]: ClassComponentRender,
|
||||
|
@ -46,8 +44,7 @@ export default {
|
|||
[DomPortal]: DomPortalRender,
|
||||
[TreeRoot]: TreeRootRender,
|
||||
[DomText]: DomTextRender,
|
||||
[IncompleteClassComponent]: IncompleteClassComponentRender,
|
||||
[LazyComponent]: LazyComponentRender,
|
||||
[MemoComponent]: MemoComponentRender,
|
||||
[SuspenseComponent]: SuspenseComponentRender,
|
||||
}
|
||||
};
|
||||
|
|
|
@ -32,7 +32,7 @@ import {
|
|||
callEffectRemove,
|
||||
callUseEffects,
|
||||
callUseLayoutEffectCreate,
|
||||
callUseLayoutEffectRemove
|
||||
callUseLayoutEffectRemove,
|
||||
} from './HookEffectHandler';
|
||||
import { handleSubmitError } from '../ErrorHandler';
|
||||
import {
|
||||
|
@ -192,7 +192,8 @@ function unmountVNode(vNode: VNode): void {
|
|||
|
||||
const instance = vNode.realNode;
|
||||
// 当constructor中抛出异常时,instance会是null,这里判断一下instance是否为空
|
||||
if (instance && typeof instance.componentWillUnmount === 'function') {
|
||||
// suspense打断时不需要触发WillUnmount
|
||||
if (instance && typeof instance.componentWillUnmount === 'function' && !vNode.isSuspended) {
|
||||
callComponentWillUnmount(vNode, instance);
|
||||
}
|
||||
break;
|
||||
|
@ -212,11 +213,11 @@ function unmountVNode(vNode: VNode): void {
|
|||
// 卸载vNode,递归遍历子vNode
|
||||
function unmountNestedVNodes(vNode: VNode): void {
|
||||
travelVNodeTree(vNode, (node) => {
|
||||
unmountVNode(node);
|
||||
}, node =>
|
||||
// 如果是DomPortal,不需要遍历child
|
||||
node.tag === DomPortal
|
||||
, vNode, null);
|
||||
unmountVNode(node);
|
||||
}, node =>
|
||||
// 如果是DomPortal,不需要遍历child
|
||||
node.tag === DomPortal
|
||||
, vNode, null);
|
||||
}
|
||||
|
||||
function submitAddition(vNode: VNode): void {
|
||||
|
@ -329,7 +330,7 @@ function submitClear(vNode: VNode): void {
|
|||
// 但考虑到用户可能自定义其他属性,所以采用遍历赋值的方式
|
||||
const customizeKeys = Object.keys(realNode);
|
||||
const keyLength = customizeKeys.length;
|
||||
for(let i = 0; i < keyLength; i++) {
|
||||
for (let i = 0; i < keyLength; i++) {
|
||||
const key = customizeKeys[i];
|
||||
// 测试代码 mock 实例的全部可遍历属性都会被Object.keys方法读取到
|
||||
// children 属性被复制意味着复制了子节点,因此要排除
|
||||
|
@ -351,7 +352,7 @@ function submitClear(vNode: VNode): void {
|
|||
}
|
||||
let clearChild = vNode.clearChild as VNode; // 上次渲染的child保存在clearChild属性中
|
||||
// 卸载 clearChild 和 它的兄弟节点
|
||||
while(clearChild) {
|
||||
while (clearChild) {
|
||||
// 卸载子vNode,递归遍历子vNode
|
||||
unmountNestedVNodes(clearChild);
|
||||
clearVNode(clearChild);
|
||||
|
@ -399,9 +400,9 @@ function submitUpdate(vNode: VNode): void {
|
|||
}
|
||||
|
||||
function submitSuspenseComponent(vNode: VNode) {
|
||||
const suspenseChildStatus = vNode.suspenseChildStatus;
|
||||
if (suspenseChildStatus !== SuspenseChildStatus.Init) {
|
||||
hideOrUnhideAllChildren(vNode.child, suspenseChildStatus === SuspenseChildStatus.ShowFallback);
|
||||
const { childStatus } = vNode.suspenseState;
|
||||
if (childStatus !== SuspenseChildStatus.Init) {
|
||||
hideOrUnhideAllChildren(vNode.child, childStatus === SuspenseChildStatus.ShowFallback);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,24 @@
|
|||
/**
|
||||
* 虚拟DOM结构体
|
||||
*/
|
||||
import { TreeRoot, FunctionComponent, ClassComponent, DomPortal, DomText, ContextConsumer, ForwardRef, SuspenseComponent, LazyComponent, DomComponent, Fragment, ContextProvider, Profiler, MemoComponent, IncompleteClassComponent } from './VNodeTags';
|
||||
import {
|
||||
TreeRoot,
|
||||
FunctionComponent,
|
||||
ClassComponent,
|
||||
DomPortal,
|
||||
DomText,
|
||||
ContextConsumer,
|
||||
ForwardRef,
|
||||
SuspenseComponent,
|
||||
LazyComponent,
|
||||
DomComponent,
|
||||
Fragment,
|
||||
ContextProvider,
|
||||
Profiler,
|
||||
MemoComponent,
|
||||
} from './VNodeTags';
|
||||
import type { VNodeTag } from './VNodeTags';
|
||||
import type { RefType, ContextType } from '../Types';
|
||||
import type { RefType, ContextType, SuspenseState } from '../Types';
|
||||
import type { Hook } from '../hooks/HookType';
|
||||
import { InitFlag } from './VNodeFlags';
|
||||
|
||||
|
@ -24,16 +39,14 @@ export class VNode {
|
|||
ref: RefType | ((handle: any) => void) | null = null; // 包裹一个函数,submit阶段使用,比如将外部useRef生成的对象赋值到ref上
|
||||
oldProps: any = null;
|
||||
|
||||
suspensePromises: any; // suspense组件的promise列表
|
||||
changeList: any; // DOM的变更列表
|
||||
effectList: any[] | null; // useEffect 的更新数组
|
||||
updates: any[] | null; // TreeRoot和ClassComponent使用的更新数组
|
||||
stateCallbacks: any[] | null; // 存放存在setState的第二个参数和HorizonDOM.render的第三个参数所在的node数组
|
||||
isForceUpdate: boolean; // 是否使用强制更新
|
||||
|
||||
isSuspended = false; // 是否被suspense打断更新
|
||||
state: any; // ClassComponent和TreeRoot的状态
|
||||
hooks: Array<Hook<any, any>> | null; // 保存hook
|
||||
suspenseChildStatus = ''; // Suspense的Children是否显示
|
||||
depContexts: Array<ContextType<any>> | null; // FunctionComponent和ClassComponent对context的依赖列表
|
||||
isDepContextChange: boolean; // context是否变更
|
||||
dirtyNodes: Array<VNode> | null = null; // 需要改动的节点数组
|
||||
|
@ -42,7 +55,7 @@ export class VNode {
|
|||
task: any;
|
||||
|
||||
// 使用这个变量来记录修改前的值,用于恢复。
|
||||
contexts: any;
|
||||
context: any;
|
||||
// 因为LazyComponent会修改tag和type属性,为了能识别,增加一个属性
|
||||
isLazyComponent: boolean;
|
||||
|
||||
|
@ -55,17 +68,20 @@ export class VNode {
|
|||
oldHooks: Array<Hook<any, any>> | null; // 保存上一次执行的hook
|
||||
oldState: any;
|
||||
oldRef: RefType | ((handle: any) => void) | null = null;
|
||||
suspenseChildThrow: boolean;
|
||||
oldSuspenseChildStatus: string; // 上一次Suspense的Children是否显示
|
||||
oldChild: VNode | null = null;
|
||||
suspenseDidCapture: boolean; // suspense是否捕获了异常
|
||||
promiseResolve: boolean; // suspense的promise是否resolve
|
||||
|
||||
suspenseState: SuspenseState;
|
||||
|
||||
path = ''; // 保存从根到本节点的路径
|
||||
toUpdateNodes: Set<VNode> | null; // 保存要更新的节点
|
||||
|
||||
belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用
|
||||
|
||||
// 状态管理器使用
|
||||
isStoreChange: boolean;
|
||||
functionToObserver: FunctionToObserver | null; // 记录这个函数组件依赖哪些Observer
|
||||
|
||||
constructor(tag: VNodeTag, props: any, key: null | string, realNode) {
|
||||
this.tag = tag; // 对应组件的类型,比如ClassComponent等
|
||||
this.key = key;
|
||||
|
@ -81,7 +97,7 @@ export class VNode {
|
|||
this.stateCallbacks = null;
|
||||
this.state = null;
|
||||
this.oldState = null;
|
||||
this.contexts = null;
|
||||
this.context = null;
|
||||
break;
|
||||
case FunctionComponent:
|
||||
this.realNode = null;
|
||||
|
@ -90,6 +106,8 @@ export class VNode {
|
|||
this.depContexts = null;
|
||||
this.isDepContextChange = false;
|
||||
this.oldHooks = null;
|
||||
this.isStoreChange = false;
|
||||
this.functionToObserver = null;
|
||||
break;
|
||||
case ClassComponent:
|
||||
this.realNode = null;
|
||||
|
@ -100,30 +118,32 @@ export class VNode {
|
|||
this.depContexts = null;
|
||||
this.isDepContextChange = false;
|
||||
this.oldState = null;
|
||||
this.contexts = null;
|
||||
this.context = null;
|
||||
break;
|
||||
case DomPortal:
|
||||
this.realNode = null;
|
||||
this.contexts = null;
|
||||
this.context = null;
|
||||
break;
|
||||
case DomComponent:
|
||||
this.realNode = null;
|
||||
this.changeList = null;
|
||||
this.contexts = null;
|
||||
this.context = null;
|
||||
break;
|
||||
case DomText:
|
||||
this.realNode = null;
|
||||
break;
|
||||
case SuspenseComponent:
|
||||
this.realNode = null;
|
||||
this.suspensePromises = null;
|
||||
this.suspenseChildThrow = false;
|
||||
this.suspenseDidCapture = false;
|
||||
this.promiseResolve = false;
|
||||
this.oldSuspenseChildStatus = '';
|
||||
this.suspenseState = {
|
||||
promiseSet: null,
|
||||
didCapture: false,
|
||||
promiseResolved: false,
|
||||
oldChildStatus: '',
|
||||
childStatus: ''
|
||||
};
|
||||
break;
|
||||
case ContextProvider:
|
||||
this.contexts = null;
|
||||
this.context = null;
|
||||
break;
|
||||
case MemoComponent:
|
||||
this.effectList = null;
|
||||
|
@ -143,8 +163,6 @@ export class VNode {
|
|||
break;
|
||||
case Profiler:
|
||||
break;
|
||||
case IncompleteClassComponent:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,9 +38,9 @@ const typeMap = {
|
|||
[TYPE_LAZY]: LazyComponent,
|
||||
};
|
||||
|
||||
const newVirtualNode = function(tag: VNodeTag, key?: null | string, vNodeProps?: any, realNode?: any): VNode {
|
||||
function newVirtualNode(tag: VNodeTag, key?: null | string, vNodeProps?: any, realNode?: any): VNode {
|
||||
return new VNode(tag, vNodeProps, key, realNode);
|
||||
};
|
||||
}
|
||||
|
||||
function isClassComponent(comp: Function) {
|
||||
// 如果使用 getPrototypeOf 方法获取构造函数,不能兼容业务组组件继承组件的使用方式,会误认为是函数组件
|
||||
|
@ -56,7 +56,7 @@ export function getLazyVNodeTag(lazyComp: any): string {
|
|||
} else if (lazyComp !== undefined && lazyComp !== null && typeLazyMap[lazyComp.vtype]) {
|
||||
return typeLazyMap[lazyComp.vtype];
|
||||
}
|
||||
throw Error("Horizon can't resolve the content of lazy ");
|
||||
throw Error('Horizon can\'t resolve the content of lazy');
|
||||
}
|
||||
|
||||
// 创建processing
|
||||
|
@ -66,7 +66,7 @@ export function updateVNode(vNode: VNode, vNodeProps?: any): VNode {
|
|||
}
|
||||
|
||||
if (vNode.tag === SuspenseComponent) {
|
||||
vNode.oldSuspenseChildStatus = vNode.suspenseChildStatus;
|
||||
vNode.suspenseState.oldChildStatus = vNode.suspenseState.childStatus;
|
||||
vNode.oldChild = vNode.child;
|
||||
}
|
||||
|
||||
|
@ -201,7 +201,7 @@ export function onlyUpdateChildVNodes(processing: VNode): VNode | null {
|
|||
sibling = sibling.next;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
putChildrenIntoQueue(processing.child);
|
||||
|
||||
|
@ -210,7 +210,7 @@ export function onlyUpdateChildVNodes(processing: VNode): VNode | null {
|
|||
|
||||
markVNodePath(vNode);
|
||||
|
||||
putChildrenIntoQueue(vNode)
|
||||
putChildrenIntoQueue(vNode);
|
||||
}
|
||||
}
|
||||
// 子树无需工作
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* vNode结构的变化标志
|
||||
*/
|
||||
|
||||
import type { VNode } from '../Types';
|
||||
import type { VNode } from './VNode';
|
||||
|
||||
|
||||
export const InitFlag = /** */ 0;
|
||||
|
|
|
@ -53,7 +53,7 @@ export function setParentsChildShouldUpdate(parent: VNode | null) {
|
|||
// 设置节点的所有父节点的childShouldUpdate
|
||||
export function updateParentsChildShouldUpdate(vNode: VNode) {
|
||||
let node = vNode.parent;
|
||||
let isShouldUpdate = vNode.shouldUpdate || vNode.childShouldUpdate;
|
||||
const isShouldUpdate = vNode.shouldUpdate || vNode.childShouldUpdate;
|
||||
|
||||
if (isShouldUpdate) { // 开始节点是shouldUpdate或childShouldUpdate
|
||||
// 更新从当前节点到根节点的childShouldUpdate为true
|
||||
|
|
|
@ -82,7 +82,7 @@ export function clearVNode(vNode: VNode) {
|
|||
vNode.hooks = null;
|
||||
vNode.props = null;
|
||||
vNode.parent = null;
|
||||
vNode.suspensePromises = null;
|
||||
vNode.suspenseState = null;
|
||||
vNode.changeList = null;
|
||||
vNode.effectList = null;
|
||||
vNode.updates = null;
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { act } from '../../jest/customMatcher';
|
||||
|
||||
describe('useContext Hook Test', () => {
|
||||
const { useState, useContext } = Horizon;
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
const { useState, useContext, createContext, act, unmountComponentAtNode } = Horizon;
|
||||
|
||||
it('简单使用useContext', () => {
|
||||
const LanguageTypes = {
|
||||
|
@ -47,4 +45,38 @@ describe('useContext Hook Test', () => {
|
|||
act(() => setValue(LanguageTypes.JAVASCRIPT));
|
||||
expect(container.querySelector('p').innerHTML).toBe('JavaScript');
|
||||
});
|
||||
|
||||
it('更新后useContext仍能获取到context', () => {
|
||||
const Context = createContext({});
|
||||
const ref = Horizon.createRef();
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Context.Provider
|
||||
value={{
|
||||
text: 'context',
|
||||
}}
|
||||
>
|
||||
<Child />
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
let update;
|
||||
|
||||
function Child() {
|
||||
const context = useContext(Context);
|
||||
const [_, setState] = useState({});
|
||||
update = () => setState({});
|
||||
|
||||
return <div ref={ref}>{context.text}</div>;
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
expect(ref.current.innerHTML).toBe('context');
|
||||
|
||||
update();
|
||||
|
||||
expect(ref.current.innerHTML).toBe('context');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import * as LogUtils from '../../jest/logUtils';
|
||||
import { act } from '../../jest/customMatcher';
|
||||
import Text from '../../jest/Text';
|
||||
import { Text } from '../../jest/commonComponents';
|
||||
|
||||
describe('useEffect Hook Test', () => {
|
||||
const {
|
||||
|
@ -9,7 +8,8 @@ describe('useEffect Hook Test', () => {
|
|||
useLayoutEffect,
|
||||
useState,
|
||||
memo,
|
||||
forwardRef
|
||||
forwardRef,
|
||||
act,
|
||||
} = Horizon;
|
||||
|
||||
it('简单使用useEffect', () => {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import * as LogUtils from '../../jest/logUtils';
|
||||
import { act } from '../../jest/customMatcher';
|
||||
import Text from '../../jest/Text';
|
||||
import { Text } from '../../jest/commonComponents';
|
||||
|
||||
describe('useImperativeHandle Hook Test', () => {
|
||||
const {
|
||||
useState,
|
||||
useImperativeHandle,
|
||||
forwardRef
|
||||
forwardRef,
|
||||
act,
|
||||
} = Horizon;
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import * as LogUtils from '../../jest/logUtils';
|
||||
import { act } from '../../jest/customMatcher';
|
||||
import Text from '../../jest/Text';
|
||||
import { Text } from '../../jest/commonComponents';
|
||||
|
||||
describe('useLayoutEffect Hook Test', () => {
|
||||
const {
|
||||
useState,
|
||||
useEffect,
|
||||
useLayoutEffect
|
||||
useLayoutEffect,
|
||||
act,
|
||||
} = Horizon;
|
||||
|
||||
it('简单使用useLayoutEffect', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import * as LogUtils from '../../jest/logUtils';
|
||||
import Text from '../../jest/Text';
|
||||
import { Text } from '../../jest/commonComponents';
|
||||
|
||||
describe('useMemo Hook Test', () => {
|
||||
const { useMemo, useState } = Horizon;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import * as LogUtils from '../../jest/logUtils';
|
||||
import Text from '../../jest/Text';
|
||||
import { Text } from '../../jest/commonComponents';
|
||||
|
||||
describe('useRef Hook Test', () => {
|
||||
const { useState, useRef } = Horizon;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import * as LogUtils from '../../jest/logUtils';
|
||||
import { act } from '../../jest/customMatcher';
|
||||
import Text from '../../jest/Text';
|
||||
import { Text } from '../../jest/commonComponents';
|
||||
|
||||
describe('useState Hook Test', () => {
|
||||
const {
|
||||
useState,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
memo
|
||||
memo,
|
||||
act,
|
||||
} = Horizon;
|
||||
|
||||
it('简单使用useState', () => {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import * as LogUtils from '../jest/logUtils';
|
||||
import { act } from '../jest/customMatcher';
|
||||
|
||||
describe('合成焦点事件', () => {
|
||||
|
||||
|
@ -43,4 +42,4 @@ describe('合成焦点事件', () => {
|
|||
'onBlur: blur',
|
||||
]);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import * as LogUtils from '../jest/logUtils';
|
||||
|
||||
const Text = (props) => {
|
||||
LogUtils.log(props.text);
|
||||
return <p>{props.text}</p>;
|
||||
};
|
||||
|
||||
export default Text;
|
|
@ -0,0 +1,20 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import * as LogUtils from './logUtils';
|
||||
|
||||
export const App = (props) => {
|
||||
const Parent = props.parent;
|
||||
const Child = props.child;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Parent>
|
||||
<Child />
|
||||
</Parent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Text = (props) => {
|
||||
LogUtils.log(props.text);
|
||||
return <p id={props.id}>{props.text}</p>;
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import { runAsyncEffects } from '../../../libs/horizon/src/renderer/submit/HookEffectHandler';
|
||||
import { callRenderQueueImmediate } from '../../../libs/horizon/src/renderer/taskExecutor/RenderQueue';
|
||||
import { asyncUpdates } from '../../../libs/horizon/src/renderer/TreeBuilder';
|
||||
|
||||
function runAssertion(fn) {
|
||||
try {
|
||||
fn();
|
||||
} catch (error) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => error.message,
|
||||
};
|
||||
}
|
||||
return { pass: true };
|
||||
}
|
||||
|
||||
function toMatchValue(LogUtils, expectedValues) {
|
||||
return runAssertion(() => {
|
||||
const actualValues = LogUtils.getAndClear();
|
||||
expect(actualValues).toEqual(expectedValues);
|
||||
});
|
||||
}
|
||||
|
||||
const act = (fun) => {
|
||||
asyncUpdates(fun);
|
||||
callRenderQueueImmediate();
|
||||
runAsyncEffects();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
toMatchValue,
|
||||
act
|
||||
};
|
|
@ -17,7 +17,27 @@ global.afterEach(() => {
|
|||
LogUtils.clear();
|
||||
});
|
||||
|
||||
|
||||
function runAssertion(fn) {
|
||||
try {
|
||||
fn();
|
||||
} catch (error) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => error.message,
|
||||
};
|
||||
}
|
||||
return { pass: true };
|
||||
}
|
||||
|
||||
function toMatchValue(LogUtils, expectedValues) {
|
||||
return runAssertion(() => {
|
||||
const actualValues = LogUtils.getAndClear();
|
||||
expect(actualValues).toEqual(expectedValues);
|
||||
});
|
||||
}
|
||||
|
||||
// 使Jest感知自定义匹配器
|
||||
expect.extend({
|
||||
...require('./customMatcher'),
|
||||
toMatchValue,
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ export const stopBubbleOrCapture = (e, value) => {
|
|||
export const getEventListeners = (dom) => {
|
||||
let ret = true
|
||||
let keyArray = [];
|
||||
for (var key in dom) {
|
||||
for (let key in dom) {
|
||||
keyArray.push(key);
|
||||
}
|
||||
try {
|
||||
|
@ -23,4 +23,11 @@ export const getEventListeners = (dom) => {
|
|||
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
};
|
||||
|
||||
export function triggerClickEvent(container, id) {
|
||||
const event = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
});
|
||||
container.querySelector(`#${id}`).dispatchEvent(event);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue