From 527c5023988589296792dc7b3d6d200eec14a333 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 7 Nov 2023 11:06:56 +0800 Subject: [PATCH 1/3] =?UTF-8?q?[inula-dev-tools]=20=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=88=97=E8=A1=A8=E7=BB=84=E4=BB=B6=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/VList/ItemMap.ts | 8 ++++---- .../src/components/VList/VList.tsx | 10 +++++----- .../src/components/VList/index.ts | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 packages/inula-dev-tools/src/components/VList/index.ts diff --git a/packages/inula-dev-tools/src/components/VList/ItemMap.ts b/packages/inula-dev-tools/src/components/VList/ItemMap.ts index 81109df4..e4c43bc3 100644 --- a/packages/inula-dev-tools/src/components/VList/ItemMap.ts +++ b/packages/inula-dev-tools/src/components/VList/ItemMap.ts @@ -20,7 +20,7 @@ export default class ItemMap { // 不要用 indexOf 进行位置计算,它会遍历数组 - private lastRenderItemToIndexMap: Map; + private lastRenderItemToIndexMap: Map; constructor() { this.lastRenderItemToIndexMap = new Map(); @@ -34,10 +34,10 @@ export default class ItemMap { return nextItems; } - const nextRenderItems: T[] = []; + const nextRenderItems: (T | undefined)[] = []; const length = nextItems.length; - const nextRenderItemToIndexMap = new Map(); - const addItems = []; + const nextRenderItemToIndexMap = new Map(); + const addItems: T[] = []; // 遍历 nextItems 找到复用 item 和新增 item nextItems.forEach(item => { diff --git a/packages/inula-dev-tools/src/components/VList/VList.tsx b/packages/inula-dev-tools/src/components/VList/VList.tsx index 86c148c3..9efbbf89 100644 --- a/packages/inula-dev-tools/src/components/VList/VList.tsx +++ b/packages/inula-dev-tools/src/components/VList/VList.tsx @@ -48,7 +48,7 @@ function parseTranslate(data: T[], itemHeight: number) { export function VList(props: IProps) { const { data, maxDeep, height, width, children, itemHeight, scrollToItem, onRendered } = props; - const [scrollTop, setScrollTop] = useState(Math.max(data.indexOf(scrollToItem), 0) * itemHeight); + const [scrollTop, setScrollTop] = useState(Math.max(data.indexOf(scrollToItem!), 0) * itemHeight); const renderInfoRef: { current: RenderInfoType } = useRef({ visibleItems: [], }); @@ -75,7 +75,7 @@ export function VList(props: IProps) { const index = data.indexOf(scrollToItem); // 显示在页面中间 const top = Math.max(index * itemHeight - height / 2, 0); - containerRef.current.scrollTo({ top: top }); + containerRef.current?.scrollTo({ top: top }); } } }, [scrollToItem]); @@ -87,9 +87,9 @@ export function VList(props: IProps) { setScrollTop(scrollTop); }; const container = containerRef.current; - container.addEventListener('scroll', handleScroll); + container?.addEventListener('scroll', handleScroll); return () => { - container.removeEventListener('scroll', handleScroll); + container?.removeEventListener('scroll', handleScroll); }; }, []); @@ -117,7 +117,7 @@ export function VList(props: IProps) { } return (
diff --git a/packages/inula-dev-tools/src/components/VList/index.ts b/packages/inula-dev-tools/src/components/VList/index.ts new file mode 100644 index 00000000..944e8eb8 --- /dev/null +++ b/packages/inula-dev-tools/src/components/VList/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +export { VList } from './VList'; +export type { RenderInfoType } from './VList'; From 80c4282898c4398db4b264ebb1ed42e814e187c3 Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Tue, 7 Nov 2023 18:48:32 +0800 Subject: [PATCH 2/3] =?UTF-8?q?[inula-dev-tools]=20=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E8=A7=A3=E6=9E=90=E6=96=B9=E6=B3=95=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-dev-tools/src/parser/parseAttr.ts | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 packages/inula-dev-tools/src/parser/parseAttr.ts diff --git a/packages/inula-dev-tools/src/parser/parseAttr.ts b/packages/inula-dev-tools/src/parser/parseAttr.ts new file mode 100644 index 00000000..94473576 --- /dev/null +++ b/packages/inula-dev-tools/src/parser/parseAttr.ts @@ -0,0 +1,418 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { Hook } from '../../../inula/src/renderer/hooks/HookType'; +import { ModifyHooks, ModifyProps, ModifyState } from '../utils/constants'; +import { VNode } from '../../../inula/src/renderer/vnode/VNode'; +import { + ClassComponent, + FunctionComponent, + ContextConsumer, + ContextProvider, + ForwardRef, + SuspenseComponent, + MemoComponent, +} from '../../../inula/src/renderer/vnode/VNodeTags'; +import { helper } from '../injector'; +import { JSXElement, ContextType } from '../../../inula/src/renderer/Types'; +import { decycle } from 'json-decycle'; +import {arrify} from "ts-loader/dist/utils"; +import {add} from "../../../inula/src/renderer/taskExecutor/TaskQueue"; + +// 展示值为 string 的可编辑模型 +type EditableStringType = 'string' | 'number' | 'undefined' | 'null'; +// 展示值为 string 的不可编辑类型 +type UnEditableStringType = + | 'function' + | 'symbol' + | 'object' + | 'map' + | 'set' + | 'array' + | 'dom' // 值为 dom 元素的 ref 类型 + | 'ref'; // 值为其他数据的 ref 类型 + +type ShowAsStringType = EditableStringType | UnEditableStringType; + +export type IAttr = { + name: string | number; + indentation: number; + hIndex?: number; // 用于记录 hook 的 hIndex 值 +} & ( + | { + type: ShowAsStringType; + value: string; +} + | { + type: 'boolean'; + value: boolean; +} + ); + +type ShowType = ShowAsStringType | 'boolean'; + +const propsAndStateTag = [ClassComponent]; +const propsAndHooksTag = [FunctionComponent, ForwardRef]; +const propsTag = [ContextConsumer, ContextProvider, SuspenseComponent, MemoComponent]; +const MAX_TITLE_LENGTH = 50; + +function isJSXElement(obj: any): obj is JSXElement { + return !!(obj?.type && obj.vtype); +} + +const isCycle = (obj: any): boolean => { + return obj?.Consumer === obj; +}; + +const getObjectKeys = (attr: Record): Array => { + const keys: (string | symbol)[] = []; + let current = attr; + try { + while (current != null) { + const currentKeys = [ + ...Object.keys(current), + ...Object.getOwnPropertySymbols(current) + ]; + const descriptors = Object.getOwnPropertyDescriptors(current); + currentKeys.forEach(key => { + // @ts-ignore key 可以为 symbol 类型 + if (descriptors[key].enumerable) { + keys.push(key); + } + }); + current = Object.getPrototypeOf(current); + } + } catch (e) { + console.log(attr); + } + return keys; +}; + +// 用于比较两个 key 值的顺序 +export function sortKeys( + firstKey: string | number | symbol, + secondKey: string | number | symbol +): number { + if (firstKey.toString() > secondKey.toString()) { + return 1; + } else if (secondKey.toString() > firstKey.toString()) { + return -1; + } else { + return 0; + } +} + +const parseSubTitle = (attr: T) => { + const AttrType = typeof attr; + + if (Array.isArray(attr)) { + let title = ''; + // 当 i > 0 时多加一个逗号和空格,例如:Person: { name: 'XXX', age: xxx } + for (let i = 0; i < attr.length; i++) { + if (i > 0) { + title = `${title}, `; + } + title = `${title}${parseSubTitle(attr[i])}`; + if (title.length > MAX_TITLE_LENGTH) { + break; + } + } + + if (title.length > MAX_TITLE_LENGTH) { + title = `${title.substr(0, MAX_TITLE_LENGTH)}…`; + } + return `[${title}]`; + } else if (AttrType === 'string') { + return `"${attr}"`; + } else if (AttrType === 'function') { + const funcName = attr['name']; + return `ƒ ${funcName}() {}`; + } else if ( + AttrType === 'boolean' || + AttrType === 'number' || + AttrType === 'undefined' + ) { + return `${attr}`; + } else if (AttrType === 'object') { + if (attr === null) { + return 'null'; + } + + if (isCycle(attr)) { + attr = JSON.parse(JSON.stringify(attr, decycle())); + } + const keys = getObjectKeys(attr).sort(sortKeys); + let title = ''; + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + // 当 i > 0 时多加一个逗号和空格,例如:Person: { name: "xxx", age: xxx } + if (i > 0) { + title = `${title}, `; + } + title = `${title}${key.toString()}: ${parseSubTitle(attr[key])}`; + if (title.length > MAX_TITLE_LENGTH) { + break; + } + } + if (title.length > MAX_TITLE_LENGTH) { + title = `${title.substr(0, MAX_TITLE_LENGTH)}…`; + } + return `{${title}}`; + } else if (isJSXElement(attr)) { + let title = ''; + if (typeof attr.type === 'string') { + title = attr.type; + } else { + title = attr.type?.name ? attr.type.name : helper.getElementTag(attr); + } + return `${title} />`; + } +}; + +const parseSubAttr = ( + attr: any, + parentIndentation: number, + attrName: string, + result: IAttr[], + hIndex?: number +) => { + const AttrType = typeof attr; + let value: any; + let showType: any; + let addSubState; + + if ( + AttrType === 'boolean' || + AttrType === 'number' || + AttrType === 'undefined' || + AttrType === 'string' + ) { + value = attr; + showType = AttrType; + } else if (AttrType === 'function') { + const funcName = attr.name; + value = `ƒ ${funcName}() {}`; + } else if (AttrType === 'symbol') { + value = attr.description; + } else if (AttrType === 'object') { + if (attr === null) { + value = '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, result); + }); + }; + } 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), result); + }); + i++; + }; + } else if (Array.isArray(attr)) { + showType = 'array'; + value = parseSubTitle(attr); + addSubState = () => { + attr.forEach((attrValue, index) => { + if (isJSXElement(attrValue)) { + if (typeof attrValue.type === 'string') { + value = attrValue.type + ' />'; + } else { + value = attrValue.type?.name ? attrValue.type.name + ' />' : helper.getElementTag(attrValue) + ' />'; + } + showType = 'string'; + const arrayItem: IAttr = { + name: index, + type: showType, + value, + indentation: parentIndentation + 2, + }; + result.push(arrayItem); + } else { + parseSubAttr(attrValue, parentIndentation + 2, String(index), result); + } + }); + }; + } else if (attr instanceof Element) { + showType = 'dom'; + value = '<' + attr.tagName.toLowerCase() + ' />'; + } else { + if (isJSXElement(attr)) { + if (typeof attr.type === 'string') { + value = attr.type + ' />'; + } else { + value = attr.type?.name ? attr.type.name + ' />' : helper.getElementTag(attr) + ' />'; + } + showType = 'string'; + } else { + showType = AttrType; + value = Object.keys(attr).length === 0 ? '{}' : parseSubTitle(attr); + addSubState = () => { + // 判断是否为 Context 循环引用 + if (isCycle(attr)) { + attr = JSON.parse(JSON.stringify(attr, decycle())); + } + Object.entries(attr).map(([key, val]) => { + if (key === '_vNode') { + val = JSON.parse(JSON.stringify(val, decycle())); + } + parseSubAttr(val, parentIndentation + 2, key, result); + }); + }; + } + } + } + + const item: IAttr = { + name: attrName, + type: showType, + value, + indentation: parentIndentation + 1, + }; + if (hIndex !== undefined) { + item.hIndex = hIndex; + } + result.push(item); + if (addSubState) { + addSubState(); + } +}; + +// 将属性的值解析成固定格式, props 和类组件的 state 必须是一个对象 +export function parseAttr(rootAttr: any) { + const result: IAttr[] = []; + const indentation = 0; + if (typeof rootAttr === 'object' && rootAttr !== null) { + Object.keys(rootAttr).forEach(key => { + parseSubAttr(rootAttr[key], indentation, key, result); + }); + } + return result; +} + +export function parseHooks( + hooks: Hook[] | null, + depContexts: Array> | null, + getHookInfo +) { + const result: IAttr[] = []; + const indentation = 0; + if (depContexts !== null && depContexts?.length > 0) { + depContexts.forEach(context => { + parseSubAttr(context.value, indentation, 'Context', result); + }); + } + hooks?.forEach(hook => { + const hookInfo = getHookInfo(hook); + if (hookInfo) { + const { name, hIndex, value } = hookInfo; + parseSubAttr(value, indentation, name, result, hIndex); + } + }); + return result; +} + +export function parseVNodeAttrs(vNode: VNode, getHookInfo) { + const tag = vNode.tag; + + if (propsAndStateTag.includes(tag)) { + const { props, state, src } = vNode; + const parsedProps = parseAttr(props); + const parsedState = parseAttr(state); + return { + parsedProps, + parsedState, + src, + }; + } else if (propsAndHooksTag.includes(tag)) { + const { props, hooks, depContexts, src } = vNode; + const parsedProps = parseAttr(props); + const parsedHooks = parseHooks(hooks, depContexts, getHookInfo); + return { + parsedProps, + parsedHooks, + src, + }; + } else if (propsTag.includes(tag)) { + const { props, src } = vNode; + const parsedProps = parseAttr(props); + return { + parsedProps, + src, + }; + } +} + +// 计算属性的访问顺序 +function calculateAttrAccessPath(item: IAttr, index: number, attrs: IAttr[], isHook: boolean) { + let currentIndentation = item.indentation; + const path: (string | number | undefined)[] = [item.name]; + let hookRootItem: IAttr = item; + for (let i = index - 1; i >= 0; i--) { + const lastItem = attrs[i]; + const lastIndentation = lastItem.indentation; + if (lastIndentation < currentIndentation) { + hookRootItem = lastItem; + path.push(lastItem.name); + currentIndentation = lastIndentation; + } + } + path.reverse(); + if (isHook) { + if (hookRootItem) { + path[0] = hookRootItem.hIndex; + } else { + console.error('There is a bug, please report'); + } + } + return path; +} + +export function buildAttrModifyData( + parsedAttrsType: string, + attrs: IAttr[], + value, + item: IAttr, + index: number, + id: number +) { + let type; + if (parsedAttrsType === 'parsedProps') { + type = ModifyProps; + } else if (parsedAttrsType === 'parsedState') { + type = ModifyState; + } else if (parsedAttrsType === 'parsedHooks') { + type = ModifyHooks; + } else { + return null; + } + + const path = calculateAttrAccessPath(item, index, attrs, parsedAttrsType === 'parsedHooks'); + + return { + id: id, + type: type, + value: value, + path: path, + }; +} From 42e8bd11715a8c745b645dcefd59fc69aecdbffa Mon Sep 17 00:00:00 2001 From: 13659257719 <819781841@qq.com> Date: Thu, 9 Nov 2023 10:34:51 +0800 Subject: [PATCH 3/3] =?UTF-8?q?[inula-dev-tools]=20=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=20VNode=20tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inula-dev-tools/src/parser/parseAttr.ts | 2 - .../inula-dev-tools/src/parser/parseVNode.ts | 229 ++++++++++++++++++ 2 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 packages/inula-dev-tools/src/parser/parseVNode.ts diff --git a/packages/inula-dev-tools/src/parser/parseAttr.ts b/packages/inula-dev-tools/src/parser/parseAttr.ts index 94473576..59b47073 100644 --- a/packages/inula-dev-tools/src/parser/parseAttr.ts +++ b/packages/inula-dev-tools/src/parser/parseAttr.ts @@ -28,8 +28,6 @@ import { import { helper } from '../injector'; import { JSXElement, ContextType } from '../../../inula/src/renderer/Types'; import { decycle } from 'json-decycle'; -import {arrify} from "ts-loader/dist/utils"; -import {add} from "../../../inula/src/renderer/taskExecutor/TaskQueue"; // 展示值为 string 的可编辑模型 type EditableStringType = 'string' | 'number' | 'undefined' | 'null'; diff --git a/packages/inula-dev-tools/src/parser/parseVNode.ts b/packages/inula-dev-tools/src/parser/parseVNode.ts new file mode 100644 index 00000000..0b01799f --- /dev/null +++ b/packages/inula-dev-tools/src/parser/parseVNode.ts @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { VNode } from '../../../inula/src/renderer/vnode/VNode'; +import { + ClassComponent, + ContextConsumer, + ContextProvider, + ForwardRef, + FunctionComponent, + MemoComponent, + SuspenseComponent +} from '../../../inula/src/renderer/vnode/VNodeTags'; + +export type NameObj = { + itemName: string; + badge: Array; +}; + +// 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode +export let VNodeToIdMap: Map; +export let IdToVNodeMap: Map; + +if (!VNodeToIdMap) { + VNodeToIdMap = new Map(); +} + +if (!IdToVNodeMap) { + IdToVNodeMap = new Map(); +} + +let uid = 0; +function generateUid(vNode: VNode) { + const id = VNodeToIdMap.get(vNode); + if (id !== undefined) { + return id; + } + uid++; + return uid; +} + +const componentType = [ + ClassComponent, + FunctionComponent, + ContextProvider, + ContextConsumer, + ForwardRef, + SuspenseComponent, + MemoComponent, +]; + +const badgeNameArr: Array = [ + 'withRouter(', + 'SideEffect(', + 'Connect(', + 'injectIntl(', + 'Pure(', +]; + +export function isUserComponent(tag: string) { + return componentType.includes(tag); +} + +function getParentUserComponent(node: VNode) { + let parent = node.parent; + while (parent) { + if (isUserComponent(parent.tag)) { + break; + } + parent = parent.parent; + } + return parent; +} + +function getContextName(node: VNode, type: string) { + const contextType = type; + if (!node.type.displayName) { + if (node.type.value) { + if (typeof node.type.value === 'object') { + return `Context.${contextType}`; + } else { + return `${node.type.value}.${contextType}`; + } + } else { + if (node.type._context?.displayName) { + return `${node.type._context.displayName}.${contextType}`; + } + return `Context.${contextType}`; + } + } + return `${node.type.displayName}.${contextType}`; +} + +const getForwardRefName = (node: VNode): NameObj => { + const forwardRefName: NameObj = { + itemName: '', + badge: ['ForwardRef'], + }; + if (!node.type.render?.name) { + if (node.type.render?.name !== '') { + forwardRefName.itemName = node.type?.displayName ? node.type?.displayName : 'Anonymous'; + } else { + forwardRefName.itemName = 'Anonymous'; + } + } else { + forwardRefName.itemName = node.type.render?.name; + } + return forwardRefName; +}; + +// 用于结构组件名,例如: Pure(Memo(xxx)) => xxx 并且把 Pure Memo 加到 NameObj.badge 里 +const parseComponentName = (name: NameObj): NameObj => { + badgeNameArr.forEach(badgeName => { + if (name.itemName.startsWith(badgeName)) { + // 截断开头的高阶组件名,并把最后一个 ) 替换为 ''。例如: Pure(Memo(xxx)) => Memo(xxx)) => Memo(xxx) + name.itemName = name.itemName.substring(badgeName.length).replace(/(\))(?!.*\1)/, ''); + name.badge.push(badgeName.substring(0, badgeName.length - 1)); + } + }); + return name; +}; + +// 取字符串括号里的值 +const getValuesInParentheses = (name: string) => { + let result = name; + const regex = /\((.+?)\)/g; + const results = name.match(regex); + if (results) { + const option = results[0]; + if (option) { + result = option.substring(1, option.length - 1); + } + } + return result; +}; + +function isNullOrUndefined(prop) { + return !prop || typeof prop === 'undefined' || prop === 0; +} + +function parseTreeRoot(travelVNodeTree, treeRoot: VNode) { + const result: any[] = []; + travelVNodeTree(treeRoot, (node: VNode) => { + const tag = node.tag; + + if (isUserComponent(tag)) { + // 添加 ID + const id = generateUid(node); + result.push(id); + let nameObj: NameObj = { + itemName: '', + badge: [], + }; + // 拿到不同类型的展示名字 + if (tag === ContextProvider) { + nameObj.itemName = getContextName(node, 'Provider'); + result.push(nameObj); + } else if (tag === ContextConsumer) { + nameObj.itemName = getContextName(node, 'Consumer'); + result.push(nameObj); + } else if (tag === ForwardRef) { + const name = getForwardRefName(node); + result.push(name); + } else if (tag === SuspenseComponent) { + nameObj.itemName = 'Suspense'; + result.push(nameObj); + } else if (tag === MemoComponent) { + const name = node.type?.displayName || node.type?.name || node.type.render?.name; + nameObj.itemName = !isNullOrUndefined(name) ? name : 'Anonymous'; + nameObj.badge.push('Memo'); + nameObj = parseComponentName(nameObj); + result.push(nameObj); + } else { + const name = node.type.displayName || node.type?.name; + nameObj.itemName = !isNullOrUndefined(name) ? name : 'Anonymous'; + nameObj = parseComponentName(nameObj); + result.push(nameObj); + } + + // 添加父节点 ID + const parent = getParentUserComponent(node); + if (parent) { + const parentId = VNodeToIdMap.get(parent); + result.push(parentId); + } else { + result.push(''); + } + + // 添加节点 key 值 + const key = node.key; + if (key !== null) { + result.push(key); + } else { + result.push(''); + } + + VNodeToIdMap.set(node, id); + IdToVNodeMap.set(id, node); + } + }); + + return result; +} + +export function queryVNode(id: number): VNode | undefined { + return IdToVNodeMap.get(id); +} + +export function clearVNode(vNode: VNode) { + if (VNodeToIdMap.has(vNode)) { + const id = VNodeToIdMap.get(vNode); + VNodeToIdMap.delete(vNode); + IdToVNodeMap.delete(id); + } +} + +export default parseTreeRoot;