From dfffef41e20d8e2df8b06f5a76154d8a98ea1e58 Mon Sep 17 00:00:00 2001
From: * <*>
Date: Tue, 26 Sep 2023 15:06:19 +0800
Subject: [PATCH] Match-id-4cc3574b83fb955dd2c7b7f3a5eb4beab4f22c94
---
packages/inula/package.json | 2 +-
.../For/reactive-component-for-add.test.js | 162 +++
.../For/reactive-component-for-delete.test.js | 129 ++
.../For/reactive-component-for-update.test.js | 1172 +++++++++++++++++
.../For/reactive-component-for.test.js | 251 ++++
.../reactive-component-block.test.js | 94 ++
.../reactive-component-combination.test.js | 105 ++
.../reactive-component-rtext.test.js | 44 +
.../component/reactive-component-show.test.js | 200 +++
.../reactive-component-switch.test.js | 75 ++
.../__tests__/ReactivityTest/computed.test.js | 51 +
.../ReactivityTest/reactive-compute.test.js | 377 ++++++
.../ReactivityTest/reactive-memory.test.js | 77 ++
.../ReactivityTest/reactive-mix-use.test.js | 60 +
.../reactive-mixed-children.test.js | 64 +
.../ReactivityTest/reactive-object.test.js | 973 ++++++++++++++
.../ReactivityTest/reactive-primitive.test.js | 266 ++++
.../reactive-props-update.test.js | 96 ++
.../ReactivityTest/reactive-watch.test.js | 122 ++
.../inula/scripts/rollup/rollup.config.js | 2 +-
packages/inula/src/dom/DOMOperator.ts | 2 +-
.../DOMPropertiesHandler.ts | 55 +-
.../dom/DOMPropertiesHandler/StyleHandler.ts | 8 +-
.../DOMPropertiesHandler/UpdateCommonProp.ts | 5 +-
.../src/dom/valueHandler/InputValueHandler.ts | 14 +-
.../dom/valueHandler/TextareaValueHandler.ts | 6 +-
packages/inula/src/external/JSXElement.ts | 13 +-
packages/inula/src/index.ts | 42 +
packages/inula/src/reactive/Atom.ts | 78 ++
packages/inula/src/reactive/Batch.ts | 92 ++
packages/inula/src/reactive/Computed.ts | 55 +
packages/inula/src/reactive/DiffUtils.ts | 199 +++
packages/inula/src/reactive/RContext.ts | 355 +++++
.../inula/src/reactive/RContextCreator.ts | 202 +++
packages/inula/src/reactive/RNode.ts | 156 +++
packages/inula/src/reactive/Reactive.ts | 21 +
packages/inula/src/reactive/Utils.ts | 89 ++
packages/inula/src/reactive/Var.ts | 22 +
packages/inula/src/reactive/Watch.ts | 42 +
.../inula/src/reactive/components/Block.tsx | 35 +
.../inula/src/reactive/components/For.tsx | 91 ++
.../inula/src/reactive/components/RText.ts | 25 +
.../inula/src/reactive/components/Show.tsx | 38 +
.../inula/src/reactive/components/Switch.ts | 42 +
.../inula/src/reactive/proxy/RProxyHandler.ts | 146 ++
packages/inula/src/reactive/types.ts | 116 ++
packages/inula/src/renderer/TreeBuilder.ts | 76 +-
packages/inula/src/renderer/Types.ts | 73 +
.../src/renderer/diff/nodeDiffComparator.ts | 102 +-
.../inula/src/renderer/hooks/HookExternal.ts | 25 +
packages/inula/src/renderer/hooks/UseWatch.ts | 20 +
.../src/renderer/hooks/reactive/UseAtom.ts | 53 +
.../src/renderer/hooks/reactive/UseCompute.ts | 24 +
.../renderer/hooks/reactive/UseReactive.ts | 49 +
.../src/renderer/hooks/reactive/UseWatch.ts | 28 +
.../src/renderer/render/ClassComponent.ts | 3 +-
.../inula/src/renderer/render/DomComponent.ts | 4 +-
.../src/renderer/render/FunctionComponent.ts | 13 +-
.../src/renderer/render/ReactiveComponent.ts | 46 +
packages/inula/src/renderer/render/index.ts | 3 +
.../src/renderer/submit/LifeCycleHandler.ts | 203 ++-
packages/inula/src/renderer/submit/Submit.ts | 2 +
.../inula/src/renderer/utils/vNodePath.ts | 2 +-
packages/inula/src/renderer/vnode/VNode.ts | 300 ++---
.../inula/src/renderer/vnode/VNodeCreator.ts | 65 +-
.../inula/src/renderer/vnode/VNodeTags.ts | 1 +
.../inula/src/renderer/vnode/VNodeUtils.ts | 25 +-
67 files changed, 7077 insertions(+), 311 deletions(-)
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-add.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-delete.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-update.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-block.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-combination.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-rtext.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-show.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-switch.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/computed.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/reactive-compute.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/reactive-memory.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/reactive-mix-use.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/reactive-mixed-children.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/reactive-object.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/reactive-primitive.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/reactive-props-update.test.js
create mode 100644 packages/inula/scripts/__tests__/ReactivityTest/reactive-watch.test.js
create mode 100644 packages/inula/src/reactive/Atom.ts
create mode 100644 packages/inula/src/reactive/Batch.ts
create mode 100644 packages/inula/src/reactive/Computed.ts
create mode 100644 packages/inula/src/reactive/DiffUtils.ts
create mode 100644 packages/inula/src/reactive/RContext.ts
create mode 100644 packages/inula/src/reactive/RContextCreator.ts
create mode 100644 packages/inula/src/reactive/RNode.ts
create mode 100644 packages/inula/src/reactive/Reactive.ts
create mode 100644 packages/inula/src/reactive/Utils.ts
create mode 100644 packages/inula/src/reactive/Var.ts
create mode 100644 packages/inula/src/reactive/Watch.ts
create mode 100644 packages/inula/src/reactive/components/Block.tsx
create mode 100644 packages/inula/src/reactive/components/For.tsx
create mode 100644 packages/inula/src/reactive/components/RText.ts
create mode 100644 packages/inula/src/reactive/components/Show.tsx
create mode 100644 packages/inula/src/reactive/components/Switch.ts
create mode 100644 packages/inula/src/reactive/proxy/RProxyHandler.ts
create mode 100644 packages/inula/src/reactive/types.ts
create mode 100644 packages/inula/src/renderer/hooks/UseWatch.ts
create mode 100644 packages/inula/src/renderer/hooks/reactive/UseAtom.ts
create mode 100644 packages/inula/src/renderer/hooks/reactive/UseCompute.ts
create mode 100644 packages/inula/src/renderer/hooks/reactive/UseReactive.ts
create mode 100644 packages/inula/src/renderer/hooks/reactive/UseWatch.ts
create mode 100644 packages/inula/src/renderer/render/ReactiveComponent.ts
diff --git a/packages/inula/package.json b/packages/inula/package.json
index 6da6afac..5700fecb 100644
--- a/packages/inula/package.json
+++ b/packages/inula/package.json
@@ -1,5 +1,5 @@
{
- "name": "inulajs",
+ "name": "inulajs-reactive",
"description": "Inulajs is a JavaScript framework library.",
"keywords": [
"inulajs"
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-add.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-add.test.js
new file mode 100644
index 00000000..f6a91f8b
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-add.test.js
@@ -0,0 +1,162 @@
+/*
+ * 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 Inula, { render, createRef, useReactive, For } from '../../../../../src/index';
+import { beforeEach } from '@jest/globals';
+
+const Row = ({ item }) => {
+ return
{item.name} ;
+};
+
+let rObj;
+let ref;
+let appFn;
+let App;
+let itemFn;
+
+describe('测试 For 组件的新增', () => {
+ beforeEach(() => {
+ ref = createRef();
+ appFn = jest.fn();
+ itemFn = jest.fn();
+
+ App = () => {
+ const _rObj = useReactive({
+ items: [
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ ],
+ });
+ rObj = _rObj;
+
+ appFn();
+
+ return (
+
+
+ {item => {
+ itemFn();
+ return
;
+ }}
+
+
+ );
+ };
+ });
+
+ it('通过 push 在后面添加1行', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+
+ // 在后面添加一行
+ rObj.items.push({ id: 'id-3', name: 'p3' });
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(3);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行2次,push更新执行1次
+ expect(itemFn).toHaveBeenCalledTimes(3);
+ });
+
+ it('通过 unshift 在前面添加2行', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+
+ // 在前面添加2行
+ rObj.items.unshift({ id: 'id-3', name: 'p3' }, { id: 'id-4', name: 'p4' });
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(4);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行2次,unshift更新执行2次
+ expect(itemFn).toHaveBeenCalledTimes(4);
+ });
+
+ it('通过 set 在后面添加1行', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+
+ // 在后面添加一行
+ rObj.items.set([
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ { id: 'id-3', name: 'p3' },
+ ]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(3);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行2次,push更新执行1次
+ expect(itemFn).toHaveBeenCalledTimes(3);
+
+ let li = container.querySelector('#id-3');
+ expect(li.innerHTML).toEqual('p3');
+ });
+
+ it('For标签使用,使用push创建3000行表格数据', () => {
+ let reactiveObj;
+ const App = () => {
+ const sourceData = useReactive([]);
+ reactiveObj = sourceData;
+
+ return (
+
+
+
+ 序号
+ 名称
+ 年龄
+ 性别
+ 名族
+ 其他
+
+
+ {
+ eachItem => {
+ return (
+
+ {eachItem.value}
+ {eachItem.value}
+ {eachItem.value}
+ {eachItem.value}
+ {eachItem.value}
+ {eachItem.value}
+
+ );
+ }
+ }
+
+
+
+ );
+ };
+ render( , container);
+
+ // 不推荐:循环push
+ for (let i = 0; i < 2; i++) {
+ reactiveObj.push({ value: i, color: null });
+ }
+ expect(reactiveObj.get().length).toEqual(2);
+
+ let items = container.querySelectorAll('tr');
+ expect(items.length).toEqual(3);
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-delete.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-delete.test.js
new file mode 100644
index 00000000..b53be008
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-delete.test.js
@@ -0,0 +1,129 @@
+/*
+ * 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 Inula, { render, createRef, useReactive, For } from '../../../../../src/index';
+import { beforeEach } from '@jest/globals';
+
+const Row = ({ item }) => {
+ return {item.name} ;
+};
+
+let rObj;
+let ref;
+let appFn;
+let App;
+let itemFn;
+
+describe('测试 For 组件的删除', () => {
+ beforeEach(() => {
+ ref = createRef();
+ appFn = jest.fn();
+ itemFn = jest.fn();
+
+ App = () => {
+ const _rObj = useReactive({
+ items: [
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ { id: 'id-3', name: 'p3' },
+ { id: 'id-4', name: 'p4' },
+ { id: 'id-5', name: 'p5' },
+ ],
+ });
+ rObj = _rObj;
+
+ appFn();
+
+ return (
+
+
+ {item => {
+ itemFn();
+ return
;
+ }}
+
+
+ );
+ };
+ });
+
+ it('通过 pop 删除最后1行', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ // 删除最后一行
+ rObj.items.pop();
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(4);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行5次,pop无需更新
+ expect(itemFn).toHaveBeenCalledTimes(5);
+ });
+
+ it('通过 splice 删除中间2行', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ // 删除中间一行
+ rObj.items.splice(2, 2);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(3);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行5次,splice无需更新
+ expect(itemFn).toHaveBeenCalledTimes(5);
+ });
+
+ it('通过 splice 删除中间2行,增加1行', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ // 删除中间2行,增加1行
+ rObj.items.splice(2, 2, ...[{ id: 6, name: 'p6' }]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(4);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行5次,splice新增1行会执行1次
+ expect(itemFn).toHaveBeenCalledTimes(6);
+ });
+
+ it('通过 set 删除中间2行', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ // 删除中间2行
+ rObj.items.set([
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ { id: 'id-5', name: 'p5' },
+ ]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(3);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行5次,splice无需更新
+ expect(itemFn).toHaveBeenCalledTimes(5);
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-update.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-update.test.js
new file mode 100644
index 00000000..272c7f12
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-update.test.js
@@ -0,0 +1,1172 @@
+/*
+ * 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 Inula, { render, createRef, useReactive, reactive, For } from '../../../../../src/index';
+import { beforeEach } from '@jest/globals';
+import { getRNode } from '../../../../../src/reactive/Utils';
+
+const Row = ({ item }) => {
+ return (
+
+ {item.name}
+
+ );
+};
+
+const TableList = ({ item }) => {
+ return {item => } ;
+};
+
+const RowList = ({ item }) => {
+ return {item =>
} ;
+};
+
+let rObj;
+let ref;
+let appFn;
+let App;
+let itemFn;
+let globalData;
+
+describe('测试 For 组件的更新', () => {
+ beforeEach(() => {
+ ref = createRef();
+ appFn = jest.fn();
+ itemFn = jest.fn();
+
+ App = () => {
+ const _rObj = useReactive({
+ items: [
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ { id: 'id-3', name: 'p3' },
+ { id: 'id-4', name: 'p4' },
+ { id: 'id-5', name: 'p5' },
+ ],
+ });
+ rObj = _rObj;
+
+ appFn();
+
+ return (
+
+
+ {item => {
+ itemFn();
+ return
;
+ }}
+
+
+ );
+ };
+ });
+
+ it('通过 set 更新每行数据的id', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ let a = container.querySelector('#id-1');
+ expect(a.innerHTML).toEqual('p1');
+
+ // 更新id
+ rObj.items.set([
+ { id: 'id-11', name: 'p1' },
+ { id: 'id-22', name: 'p2' },
+ { id: 'id-33', name: 'p3' },
+ { id: 'id-44', name: 'p4' },
+ { id: 'id-55', name: 'p5' },
+ ]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 只有第一次渲染执行5次
+ expect(itemFn).toHaveBeenCalledTimes(5);
+
+ a = container.querySelector('#id-11');
+ expect(a.innerHTML).toEqual('p1');
+ });
+
+ it('等长 set 更新每行数据', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ let li = container.querySelector('#id-1');
+ expect(li.innerHTML).toEqual('p1');
+
+ // 更新
+ rObj.items.set([
+ { id: 'id-1', name: 'p11' },
+ { id: 'id-2', name: 'p2' },
+ { id: 'id-3', name: 'p33' },
+ { id: 'id-4', name: 'p4' },
+ { id: 'id-5', name: 'p55' },
+ ]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 只有第一次渲染执行5次
+ expect(itemFn).toHaveBeenCalledTimes(5);
+
+ li = container.querySelector('#id-1');
+ expect(li.innerHTML).toEqual('p11');
+ });
+
+ it('通过 reverse 反转数组', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ // 反转数组
+ rObj.items.reverse();
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行5次,反转需要5-1次
+ expect(itemFn).toHaveBeenCalledTimes(9);
+
+ let li1 = container.querySelector('li:nth-child(1)');
+ expect(li1.innerHTML).toEqual('p5');
+ let li2 = container.querySelector('li:nth-child(2)');
+ expect(li2.innerHTML).toEqual('p4');
+ let li3 = container.querySelector('li:nth-child(3)');
+ expect(li3.innerHTML).toEqual('p3');
+ let li4 = container.querySelector('li:nth-child(4)');
+ expect(li4.innerHTML).toEqual('p2');
+ let li5 = container.querySelector('li:nth-child(5)');
+ expect(li5.innerHTML).toEqual('p1');
+ });
+
+ it('通过 copyWithin 修改数组', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ // 反转数组
+ rObj.items.copyWithin(3, 1, 4);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ expect(itemFn).toHaveBeenCalledTimes(7);
+
+ // 结果是:
+ // { id: 'id-1', name: 'p1' },
+ // { id: 'id-2', name: 'p2' },
+ // { id: 'id-3', name: 'p3' },
+ // { id: 'id-2', name: 'p2' },
+ // { id: 'id-3', name: 'p3' },
+
+ let li1 = container.querySelector('li:nth-child(1)');
+ expect(li1.innerHTML).toEqual('p1');
+ let li2 = container.querySelector('li:nth-child(2)');
+ expect(li2.innerHTML).toEqual('p2');
+ let li3 = container.querySelector('li:nth-child(3)');
+ expect(li3.innerHTML).toEqual('p3');
+ let li4 = container.querySelector('li:nth-child(4)');
+ expect(li4.innerHTML).toEqual('p2');
+ let li5 = container.querySelector('li:nth-child(5)');
+ expect(li5.innerHTML).toEqual('p3');
+ });
+});
+
+describe('测试 For 组件的更新,3层数据', () => {
+ beforeEach(() => {
+ ref = createRef();
+ appFn = jest.fn();
+ itemFn = jest.fn();
+
+ App = () => {
+ const _rObj = useReactive({
+ items: [
+ {
+ id: 'id-1',
+ items: [
+ {
+ id: 'id-1',
+ items: [
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ { id: 'id-3', name: 'p3' },
+ ],
+ },
+ {
+ id: 'id-2',
+ items: [
+ { id: 'id-4', name: 'p4' },
+ { id: 'id-5', name: 'p5' },
+ { id: 'id-6', name: 'p6' },
+ ],
+ },
+ {
+ id: 'id-3',
+ items: [
+ { id: 'id-7', name: 'p7' },
+ { id: 'id-8', name: 'p8' },
+ { id: 'id-9', name: 'p9' },
+ ],
+ },
+ ],
+ },
+ {
+ id: 'id-2',
+ items: [
+ {
+ id: 'id-1',
+ items: [
+ { id: 'id-10', name: 'p10' },
+ { id: 'id-11', name: 'p11' },
+ { id: 'id-12', name: 'p12' },
+ ],
+ },
+ {
+ id: 'id-2',
+ items: [
+ { id: 'id-13', name: 'p13' },
+ { id: 'id-14', name: 'p14' },
+ { id: 'id-15', name: 'p15' },
+ ],
+ },
+ {
+ id: 'id-3',
+ items: [
+ { id: 'id-16', name: 'p16' },
+ { id: 'id-17', name: 'p17' },
+ { id: 'id-18', name: 'p18' },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ rObj = _rObj;
+
+ appFn();
+
+ return (
+
+
+ {item => {
+ itemFn();
+ return ;
+ }}
+
+
+ );
+ };
+ });
+
+ it('通过 set 更新第三层数据', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(18);
+
+ let li = container.querySelector('#id-4');
+ expect(li.innerHTML).toEqual('p4');
+
+ // 更新
+ rObj.items.set([
+ {
+ id: 'id-1',
+ items: [
+ {
+ id: 'id-1',
+ items: [
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ { id: 'id-3', name: 'p3' },
+ ],
+ },
+ {
+ id: 'id-2',
+ items: [
+ { id: 'id-4', name: 'p444' },
+ { id: 'id-5', name: 'p5' },
+ { id: 'id-6', name: 'p6' },
+ ],
+ },
+ {
+ id: 'id-3',
+ items: [
+ { id: 'id-7', name: 'p7' },
+ { id: 'id-8', name: 'p8' },
+ { id: 'id-9', name: 'p9' },
+ ],
+ },
+ ],
+ },
+ {
+ id: 'id-2',
+ items: [
+ {
+ id: 'id-1',
+ items: [
+ { id: 'id-10', name: 'p10' },
+ { id: 'id-11', name: 'p11' },
+ { id: 'id-12', name: 'p12' },
+ ],
+ },
+ {
+ id: 'id-2',
+ items: [
+ { id: 'id-13', name: 'p13' },
+ { id: 'id-14', name: 'p14' },
+ { id: 'id-15', name: 'p15' },
+ ],
+ },
+ {
+ id: 'id-3',
+ items: [
+ { id: 'id-16', name: 'p16' },
+ { id: 'id-17', name: 'p17' },
+ { id: 'id-18', name: 'p18' },
+ ],
+ },
+ ],
+ },
+ ]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(18);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 只有第一次渲染执行2次
+ expect(itemFn).toHaveBeenCalledTimes(2);
+
+ li = container.querySelector('#id-4');
+ expect(li.innerHTML).toEqual('p444');
+ });
+
+ it('通过 set 删除第3层数据', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(18);
+
+ let li = container.querySelector('#id-8');
+ expect(li.innerHTML).toEqual('p8');
+
+ // 更新
+ rObj.items.set([
+ {
+ id: 'id-1',
+ items: [
+ {
+ id: 'id-1',
+ items: [{ id: 'id-1', name: 'p1' }],
+ },
+ {
+ id: 'id-2',
+ items: [
+ { id: 'id-4', name: 'p4' },
+ { id: 'id-5', name: 'p5' },
+ ],
+ },
+ {
+ id: 'id-3',
+ items: [
+ { id: 'id-7', name: 'p7' },
+ { id: 'id-8', name: 'p888' },
+ { id: 'id-9', name: 'p9' },
+ ],
+ },
+ ],
+ },
+ {
+ id: 'id-2',
+ items: [
+ {
+ id: 'id-1',
+ items: [
+ { id: 'id-10', name: 'p10' },
+ { id: 'id-11', name: 'p11' },
+ { id: 'id-12', name: 'p12' },
+ ],
+ },
+ {
+ id: 'id-2',
+ items: [
+ { id: 'id-13', name: 'p13' },
+ { id: 'id-14', name: 'p14' },
+ { id: 'id-15', name: 'p15' },
+ ],
+ },
+ {
+ id: 'id-3',
+ items: [
+ { id: 'id-16', name: 'p16' },
+ { id: 'id-17', name: 'p17' },
+ { id: 'id-18', name: 'p18' },
+ ],
+ },
+ ],
+ },
+ ]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(15);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行2次,更新也触发了1次
+ expect(itemFn).toHaveBeenCalledTimes(3);
+
+ li = container.querySelector('#id-8');
+ expect(li.innerHTML).toEqual('p888');
+ });
+
+ it('通过 set 删除第1、3层数据', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(18);
+
+ let li = container.querySelector('#id-8');
+ expect(li.innerHTML).toEqual('p8');
+
+ // 更新
+ rObj.items.set([
+ {
+ id: 'id-1',
+ items: [
+ {
+ id: 'id-1',
+ items: [{ id: 'id-1', name: 'p1' }],
+ },
+ {
+ id: 'id-2',
+ items: [
+ { id: 'id-4', name: 'p4' },
+ { id: 'id-5', name: 'p5' },
+ ],
+ },
+ {
+ id: 'id-3',
+ items: [
+ { id: 'id-7', name: 'p7' },
+ { id: 'id-8', name: 'p888' },
+ { id: 'id-9', name: 'p9' },
+ ],
+ },
+ ],
+ },
+ ]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(6);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行2次,更新也触发了1次
+ expect(itemFn).toHaveBeenCalledTimes(3);
+
+ li = container.querySelector('#id-8');
+ expect(li.innerHTML).toEqual('p888');
+ });
+
+ it('通过 set 删除第1、2、3层数据', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(18);
+
+ let li = container.querySelector('#id-8');
+ expect(li.innerHTML).toEqual('p8');
+
+ // 更新
+ rObj.items.set([
+ {
+ id: 'id-1',
+ items: [
+ {
+ id: 'id-2',
+ items: [
+ { id: 'id-4', name: 'p4' },
+ { id: 'id-5', name: 'p5' },
+ ],
+ },
+ {
+ id: 'id-3',
+ items: [
+ { id: 'id-7', name: 'p7' },
+ { id: 'id-8', name: 'p888' },
+ { id: 'id-9', name: 'p9' },
+ ],
+ },
+ ],
+ },
+ ]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行2次,更新也触发了1次
+ expect(itemFn).toHaveBeenCalledTimes(3);
+
+ li = container.querySelector('#id-8');
+ expect(li.innerHTML).toEqual('p888');
+ });
+
+ it('通过 set 把数组设置成boolean和number', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(18);
+
+ // 更新
+ rObj.items.set([
+ {
+ id: 'id-1',
+ items: [
+ {
+ id: 'id-1',
+ items: [true],
+ },
+ {
+ id: 'id-2',
+ items: 11,
+ },
+ {
+ id: 'id-3',
+ items: [
+ { id: 'id-7', name: 'p7' },
+ { id: 'id-8', name: 'p8' },
+ { id: 'id-9', name: 'p9' },
+ ],
+ },
+ ],
+ },
+ ]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(4);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行2次,更新也触发了1次
+ expect(itemFn).toHaveBeenCalledTimes(3);
+ });
+
+ it('通过 set 把数组设置成boolean和number,再修改下面数据部分', () => {
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(18);
+
+ // 更新
+ rObj.items.set([
+ {
+ id: 'id-1',
+ items: [
+ {
+ id: 'id-1',
+ items: [true],
+ },
+ {
+ id: 'id-2',
+ items: 11, // 数组变数字,不会报错
+ xxx: 'xxx', // 多出来的数据不影响
+ },
+ {
+ id: 'id-3',
+ items: [
+ { id: 'id-7', name: 'p7' },
+ { id: 'id-8', name: 'p888' },
+ { id: 'id-9', name: 'p9' },
+ ],
+ },
+ ],
+ },
+ ]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(4);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行2次,更新也触发了1次
+ expect(itemFn).toHaveBeenCalledTimes(3);
+
+ let li = container.querySelector('#id-8');
+ expect(li.innerHTML).toEqual('p888');
+ });
+
+ it('通过 set 把对象中的数组设置成boolean', () => {
+ App = () => {
+ const _rObj = useReactive({
+ id: 'id-1',
+ items: [
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ { id: 'id-3', name: 'p3' },
+ ],
+ });
+ rObj = _rObj;
+
+ appFn();
+
+ return (
+
+
+ {item => {
+ itemFn();
+ return (
+
+ {item.name}
+
+ );
+ }}
+
+
+ );
+ };
+
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(3);
+
+ // 更新
+ rObj.set({
+ id: 'id-1',
+ items: [true],
+ });
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(1);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行3次,更新也触发了1次
+ expect(itemFn).toHaveBeenCalledTimes(4);
+ });
+});
+
+describe('测试 For 组件的更新,反复增删', () => {
+ beforeEach(() => {
+ ref = createRef();
+ appFn = jest.fn();
+ itemFn = jest.fn();
+
+ App = () => {
+ const _rObj = useReactive({
+ items: [],
+ });
+ rObj = _rObj;
+
+ appFn();
+
+ return (
+
+
+ {item => {
+ itemFn();
+ return
;
+ }}
+
+
+ );
+ };
+ });
+
+ it('先用 splice 删除1行,再通过 set 新增2行', () => {
+ render( , container);
+
+ function removeFirstRow() {
+ rObj.items.splice(0, 1);
+ }
+
+ removeFirstRow();
+
+ // 新增2行
+ rObj.items.set([
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ ]);
+
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+
+ // 再新增2行
+ rObj.items.set(
+ rObj.items.concat([
+ { id: 'id-3', name: 'p3' },
+ { id: 'id-4', name: 'p4' },
+ ])
+ );
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(4);
+ });
+
+ it('先用 set 新增6行,删除1行,交换两行位置', () => {
+ render( , container);
+
+ // 新增2行
+ rObj.items.set([
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ { id: 'id-3', name: 'p3' },
+ { id: 'id-4', name: 'p4' },
+ { id: 'id-5', name: 'p5' },
+ { id: 'id-6', name: 'p6' },
+ ]);
+
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(6);
+
+ // 删除一行
+ rObj.items.splice(0, 1);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ function swapRows() {
+ const arr = rObj.items.slice();
+ const tmp = arr[1];
+ arr[1] = arr[arr.length - 2];
+ arr[arr.length - 2] = tmp;
+ rObj.items.set(arr);
+ }
+
+ swapRows();
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ let li2 = container.querySelector('li:nth-child(2)');
+ expect(li2.innerHTML).toEqual('p5');
+
+ let li4 = container.querySelector('li:nth-child(4)');
+ expect(li4.innerHTML).toEqual('p3');
+ });
+
+ it('先用 set 新增4行,交换两行位置,删除1行', () => {
+ render( , container);
+
+ // 新增2行
+ rObj.items.set([
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ { id: 'id-3', name: 'p3' },
+ { id: 'id-4', name: 'p4' },
+ ]);
+
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(4);
+
+ function swapRows() {
+ const arr = rObj.items.slice();
+ const tmp = arr[1];
+ arr[1] = arr[arr.length - 2];
+ arr[arr.length - 2] = tmp;
+ rObj.items.set(arr);
+ }
+
+ // 前后边上第2行交换
+ swapRows();
+
+ // 删除一行
+ rObj.items.splice(0, 1);
+
+ // 结果是:
+ // { id: 'id-3', name: 'p3' },
+ // { id: 'id-2', name: 'p2' },
+ // { id: 'id-4', name: 'p4' },
+
+ let li2 = container.querySelector('li:nth-child(2)');
+ expect(li2.innerHTML).toEqual('p2');
+ });
+});
+
+describe('测试 For 组件的更新,直接修改raw数组对象', () => {
+ beforeEach(() => {
+ ref = createRef();
+ appFn = jest.fn();
+ itemFn = jest.fn();
+
+ globalData = {
+ items: [
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ { id: 'id-3', name: 'p3' },
+ { id: 'id-4', name: 'p4' },
+ { id: 'id-5', name: 'p5' },
+ ],
+ };
+
+ App = () => {
+ const _rObj = useReactive(globalData);
+ rObj = _rObj;
+
+ appFn();
+
+ return (
+
+
+ {item => {
+ itemFn();
+ return
;
+ }}
+
+
+ );
+ };
+ });
+
+ it('向原始数组中增加1行数据,再通过 set 更新响应式数据,是不会更新的', () => {
+ render( , container);
+
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ // 新增1行
+ globalData.items.push({ id: 'id-6', name: 'p6' });
+
+ // 无法触发更新,因为globalData.items的引用相同,不会触发监听
+ rObj.set(globalData);
+
+ items = container.querySelectorAll('li');
+ // 不会更新
+ expect(items.length).toEqual(5);
+ });
+
+ it('应该直接修改响应式数据的方式', () => {
+ render( , container);
+
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ // 直接修改响应式数据的方式,新增1行
+ rObj.items.push({ id: 'id-6', name: 'p6' });
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(6);
+ });
+});
+
+describe('更新多属性对象', () => {
+ beforeEach(() => {
+ ref = createRef();
+ appFn = jest.fn();
+ itemFn = jest.fn();
+
+ globalData = {
+ items: [
+ { id: 'id-1', name: 'p1', class: 'c1' },
+ { id: 'id-2', name: 'p2', class: 'c2' },
+ { id: 'id-3', name: 'p3', class: 'c3' },
+ { id: 'id-4', name: 'p4', class: 'c4' },
+ { id: 'id-5', name: 'p5', class: 'c5' },
+ ],
+ };
+
+ const Row = ({ item }) => {
+ return (
+
+ {item.name}
+
+ );
+ };
+
+ App = () => {
+ const _rObj = useReactive(globalData);
+ rObj = _rObj;
+
+ appFn();
+
+ return (
+
+
+ {item => {
+ itemFn();
+ return
;
+ }}
+
+
+ );
+ };
+ });
+
+ it('对象数据的属性类型变化,后面的属性更正常', () => {
+ render( , container);
+
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ let li = container.querySelector('#id-2');
+ expect(li.getAttribute('class')).toEqual('c2');
+
+ rObj.set({
+ items: [
+ { id: 'id-1', name: 'p1', class: 'c1' },
+ { id: 'id-2', name: [true], class: 'c2222' },
+ { id: 'id-3', name: 'p3', class: 'c3' },
+ { id: 'id-4', name: 'p4', class: 'c4' },
+ { id: 'id-5', name: 'p5', class: 'c5' },
+ ],
+ });
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ li = container.querySelector('#id-2');
+ expect(li.getAttribute('class')).toEqual('c2222');
+ });
+});
+
+describe('在class组件中使用for组件', () => {
+ it('在类中使用reactive数据', () => {
+ let rObj;
+ let appInst;
+ const ref = createRef();
+
+ class App extends Inula.Component {
+ constructor(props) {
+ super(props);
+
+ appInst = this;
+
+ this.state = {
+ name: 1,
+ };
+
+ this._rObj = reactive(1);
+ rObj = this._rObj;
+ }
+
+ render() {
+ return {this._rObj}
;
+ }
+ }
+
+ render( , container);
+
+ expect(ref.current.innerHTML).toEqual('1');
+
+ // 触发组件重新渲染
+ appInst.setState({ name: 2 });
+
+ rObj.set('2');
+
+ // rObj只应该有一个依赖
+ expect(rObj.usedRContexts.size).toEqual(1);
+
+ expect(ref.current.innerHTML).toEqual('2');
+ });
+
+ it('在类中使用reactive数组数据', () => {
+ let rObj;
+ let appInst;
+ const ref = createRef();
+
+ class Row extends Inula.Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const { item } = this.props;
+ return (
+
+ {item.name}
+
+ );
+ }
+ }
+
+ class App extends Inula.Component {
+ constructor(props) {
+ super(props);
+
+ appInst = this;
+
+ this.state = {
+ name: 1,
+ };
+
+ this._rObj = reactive({
+ items: [
+ { id: 'id-1', name: 'p1', class: 'c1' },
+ { id: 'id-2', name: 'p2', class: 'c2' },
+ { id: 'id-3', name: 'p3', class: 'c3' },
+ { id: 'id-4', name: 'p4', class: 'c4' },
+ { id: 'id-5', name: 'p5', class: 'c5' },
+ ],
+ });
+ rObj = this._rObj;
+ }
+
+ render() {
+ return (
+
+
+ {item => {
+ return
;
+ }}
+
+
+ );
+ }
+ }
+
+ render( , container);
+
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(5);
+
+ // 直接修改响应式数据的方式,新增1行
+ rObj.items.push({ id: 'id-6', name: 'p6' });
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(6);
+
+ // 触发组件重新渲染
+ appInst.setState({ name: 2 });
+
+ // rObj只应该有一个依赖
+ expect(getRNode(rObj.items).usedRContexts.size).toEqual(1);
+ });
+
+ describe('更新多属性对象', () => {
+ beforeEach(() => {
+ ref = createRef();
+ appFn = jest.fn();
+ itemFn = jest.fn();
+
+ globalData = {
+ items: [
+ { id: 'id-1', name: 'p1', class: 'c1' },
+ { id: 'id-2', name: 'p2', class: 'c2' },
+ ],
+ };
+
+ const Row = ({ item }) => {
+ return (
+
+ {item.name}
+
+ );
+ };
+
+ App = () => {
+ const _rObj = useReactive(globalData);
+ rObj = _rObj;
+
+ appFn();
+
+ return (
+
+
+ {item => {
+ itemFn();
+ return
;
+ }}
+
+
+ );
+ };
+ });
+
+ it('更新数组的一个原数据,调试subscribeAttr,只被调用一次', () => {
+ render( , container);
+
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+
+ let li = container.querySelector('#id-2');
+ expect(li.getAttribute('class')).toEqual('c2');
+
+ rObj.set({
+ items: [
+ { id: 'id-1', name: 'p1', class: 'c1' },
+ { id: 'id-2', name: 'p2', class: 'c222' },
+ ],
+ });
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+
+ li = container.querySelector('#id-2');
+ expect(li.getAttribute('class')).toEqual('c222');
+ });
+
+ it('更新数组的一个原数据', () => {
+ render( , container);
+
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+
+ let li = container.querySelector('#id-2');
+ expect(li.getAttribute('class')).toEqual('c2');
+
+ rObj.items[1].set({ id: 'id-2', name: 'p2', class: 'c222' });
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+
+ li = container.querySelector('#id-2');
+ expect(li.getAttribute('class')).toEqual('c222');
+
+ expect(itemFn).toHaveBeenCalledTimes(2);
+ });
+
+ it('For的数组是基本数据,更改其中一个,另外两个能精准更新', () => {
+ const rowFn = jest.fn();
+
+ const Row = ({ item, index }) => {
+ rowFn();
+ return {item} ;
+ };
+
+ const App = () => {
+ const _rObj = useReactive({
+ id: 'id-1',
+ items: [{ a: 1 }, 2, 3],
+ });
+ rObj = _rObj;
+
+ appFn();
+
+ return (
+
+
+ {(item, index) => {
+ itemFn();
+ return
;
+ }}
+
+
+ );
+ };
+
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(3);
+
+ // 更新
+ rObj.set({
+ id: 'id-1',
+ items: [2, 3, 4],
+ });
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(3);
+ expect(appFn).toHaveBeenCalledTimes(1);
+
+ // 第一次渲染执行3次,更新也触发了1次
+ expect(itemFn).toHaveBeenCalledTimes(4);
+
+ // 第一次渲染执行3次,更新也触发了1次
+ expect(rowFn).toHaveBeenCalledTimes(4);
+ });
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for.test.js
new file mode 100644
index 00000000..585b60ba
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for.test.js
@@ -0,0 +1,251 @@
+/*
+ * 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 Inula, { render, createRef, useReactive, reactive, memo, For } from '../../../../../src/index';
+
+const Item = ({ item }) => {
+ return {item.name} ;
+};
+
+describe('测试 For 组件', () => {
+ it('使用For组件遍历reactive“数组”', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const Item = ({ item }) => {
+ return {item.name} ;
+ };
+
+ const App = () => {
+ const _rObj = useReactive({
+ items: [
+ { name: 'p1', id: 1 },
+ { name: 'p2', id: 2 },
+ ],
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return (
+
+
+ {item => {
+ return ;
+ }}
+
+
+ );
+ };
+
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+
+ // 每次修改items都会触发整个组件刷新
+ rObj.items.set([{ name: 'p11', id: 1 }]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(1);
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ // 每次修改items都会触发整个组件刷新
+ rObj.items.push({ name: 'p22', id: 2 });
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('reactive“数组”从[]变成有值', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const Item = ({ item }) => {
+ return {item.name} ;
+ };
+
+ const App = () => {
+ const _rObj = useReactive({
+ items: [],
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return (
+
+
+ {item => {
+ return ;
+ }}
+
+
+ );
+ };
+
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(0);
+
+ // 每次修改items都会触发整个组件刷新
+ rObj.items.set([{ name: 'p11', id: 1 }]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(1);
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ // 每次修改items都会触发整个组件刷新
+ rObj.items.push({ name: 'p22', id: 2 });
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('数组3行变到4行', () => {
+ const state = reactive({
+ data: {
+ lines: [
+ { id: 'id-1', label: '1' },
+ { id: 'id-2', label: '2' },
+ { id: 'id-3', label: '3' },
+ ],
+ },
+ });
+
+ const Row = memo(({ item }) => {
+ return (
+
+ {item.id}
+
+ {item.label}
+
+
+ );
+ });
+
+ const RowList = () => {
+ return {item =>
} ;
+ };
+
+ const App = () => {
+ return (
+
+ );
+ };
+
+ render( , container);
+
+ let a = container.querySelector('#id-1');
+
+ expect(a.innerHTML).toEqual('1');
+ expect(state.data.lines.length).toEqual(3);
+ state.data.set({
+ lines: [
+ { id: 'id-4', label: '4' },
+ { id: 'id-5', label: '5' },
+ { id: 'id-6', label: '6' },
+ { id: 'id-7', label: '7' },
+ ],
+ });
+ expect(state.data.lines.length).toEqual(4);
+ a = container.querySelector('#id-4');
+
+ expect(a.innerHTML).toEqual('4');
+ const b = container.querySelector('#id-6');
+ expect(b.innerHTML).toEqual('6');
+ });
+
+ it('使用基本数据数组的loop方法', () => {
+ let rObj;
+ const fn = jest.fn();
+
+ const App = () => {
+ const _rObj = useReactive({
+ items: [1, 2, 3, 4],
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return (
+
+ {_rObj.items.map(rItem => {
+ return
{rItem} ;
+ })}
+
+ );
+ };
+
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(4);
+
+ // 每次修改items都会触发整个组件刷新
+ rObj.items.set([1, 2, 3]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(3);
+ expect(fn).toHaveBeenCalledTimes(2);
+ });
+});
+
+describe('数组reverse', () => {
+ it('调用数组的reverse方法', () => {
+ let rObj;
+ const fn = jest.fn();
+
+ const App = () => {
+ const _rObj = useReactive({
+ items: [
+ { id: 1, name: 'p1' },
+ { id: 2, name: 'p2' },
+ { id: 3, name: 'p3' },
+ ],
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return (
+
+
+ {item => {
+ return ;
+ }}
+
+
+ );
+ };
+
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(3);
+
+ // 反转
+ rObj.items.reverse();
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(3);
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-block.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-block.test.js
new file mode 100644
index 00000000..fa89e36e
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-block.test.js
@@ -0,0 +1,94 @@
+/*
+ * 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 Inula, { render, createRef, act, useReactive } from '../../../../src/index';
+import { Block } from '../../../../src/reactive/components/Block';
+
+describe('测试 Block 组件', () => {
+ it('使用 Block 控制更新范围', () => {
+ let rObj, rColor;
+ const ref = createRef();
+ const fn = jest.fn();
+ const fn1 = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({ count: 0 });
+ const _rColor = useReactive('blue');
+ rObj = _rObj;
+ rColor = _rColor;
+
+ fn();
+
+ return (
+
+ 111 222
+
+ {() => {
+ fn1();
+ const count = _rObj.count.get();
+ return (
+ <>
+ Count: {count}
+ {_rColor}
+ >
+ );
+ }}
+
+
+ );
+ };
+
+ render( , container);
+
+ expect(ref.current.innerHTML).toEqual('111 222Count: 0
blue
');
+
+ // 会触发View刷新
+ rObj.count.set(1);
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(fn1).toHaveBeenCalledTimes(2);
+ expect(ref.current.innerHTML).toEqual('111 222Count: 1
blue
');
+
+ // 不会触发View刷新
+ rColor.set('red');
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(fn1).toHaveBeenCalledTimes(2);
+ expect(ref.current.innerHTML).toEqual('111 222Count: 1
red
');
+ });
+
+ it('使用 Block 包裹一个Atom', () => {
+ let rObj;
+ const ref1 = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive('blue');
+ rObj = _rObj;
+
+ fn();
+
+ return (
+ // div下面有多个元素,_rObj就需要用RText包裹
+
+ 111 222
+ {_rObj}
+
+ );
+ };
+
+ render( , container);
+ expect(ref1.current.innerHTML).toEqual('111 222blue');
+ rObj.set('red');
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(ref1.current.innerHTML).toEqual('111 222red');
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-combination.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-combination.test.js
new file mode 100644
index 00000000..6871e89f
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-combination.test.js
@@ -0,0 +1,105 @@
+/*
+ * 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 Inula, { render, createRef, useReactive, useComputed, For, Show, Switch } from '../../../../src/index';
+
+describe('测试Switch、Show、For标签的组合使用时的组件渲染', () => {
+ it('Show、For标签的组合使用', () => {
+ const Item = ({ item }) => {
+ return {item.name} ;
+ };
+
+ let reactiveObj;
+ const ref = createRef();
+ const ref1 = createRef();
+ const fn = jest.fn();
+
+ const App = () => {
+ const dataList = useReactive([]);
+ reactiveObj = dataList;
+
+ const listLen = useComputed(() => {
+ return dataList.get().length;
+ });
+
+ fn();
+
+ return (
+ <>
+ dataList.get().length > 0} else={() =>
}>
+
+ {item => }
+
+
+ {listLen}
+ >
+ );
+ };
+ render( , container);
+
+ let liItems = container.querySelectorAll('li');
+ expect(liItems.length).toEqual(0);
+
+ reactiveObj.push({ id: 1, name: '1' });
+ expect(reactiveObj.get().length).toEqual(1);
+ liItems = container.querySelectorAll('li');
+ expect(liItems.length).toEqual(1);
+
+ reactiveObj.push({ id: 2, name: '2' });
+ expect(reactiveObj.get().length).toEqual(2);
+ liItems = container.querySelectorAll('li');
+ expect(liItems.length).toEqual(2);
+
+ expect(ref1.current.innerHTML).toEqual('2');
+
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('Switch、Show和For标签的组合使用', () => {
+ const Item = ({ item }) => {
+ return {item.name} ;
+ };
+
+ let reactiveObj;
+ const ref = createRef();
+ const App = () => {
+ const dataList = useReactive([]);
+ reactiveObj = dataList;
+
+ return (
+
+ dataList.get().length === 0}>
+
+
+ dataList.get().length > 0}>
+
+ {item => }
+
+
+
+ );
+ };
+ render( , container);
+
+ let liItems = container.querySelectorAll('li');
+ expect(liItems.length).toEqual(0);
+
+ reactiveObj.push({ id: 1, name: '1' });
+ expect(reactiveObj.get().length).toEqual(1);
+
+ liItems = container.querySelectorAll('li');
+ expect(liItems.length).toEqual(1);
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-rtext.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-rtext.test.js
new file mode 100644
index 00000000..167ef990
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-rtext.test.js
@@ -0,0 +1,44 @@
+/*
+ * 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 Inula, { render, createRef, act, useReactive, useCompute, reactive, RText } from '../../../../src/index';
+
+describe('测试 RText 组件', () => {
+ it('使用RText精准更新', () => {
+ let rObj;
+ const ref1 = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive('blue');
+ rObj = _rObj;
+
+ fn();
+
+ return (
+ // div下面有多个元素,_rObj就需要用RText包裹
+
+ 111 222
+ {_rObj}
+
+ );
+ };
+
+ render( , container);
+ expect(ref1.current.innerHTML).toEqual('111 222blue');
+ rObj.set('red');
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(ref1.current.innerHTML).toEqual('111 222red');
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-show.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-show.test.js
new file mode 100644
index 00000000..ba02ae46
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-show.test.js
@@ -0,0 +1,200 @@
+/*
+ * 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 Inula, { render, createRef, act, useReactive, useCompute, reactive, Show } from '../../../../src/index';
+
+describe('测试 Show 组件', () => {
+ it('if为primitive值', () => {
+ let rObj;
+ const ref1 = createRef();
+ const ref2 = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive('blue');
+ rObj = _rObj;
+
+ fn();
+
+ return (
+ // 如果else中的dom和children一个类型,需要增加key,否则会被框架当作同一个dom
+
+ Loading...
+
+ }
+ >
+
+ {_rObj}
+
+
+ );
+ };
+
+ render( , container);
+ expect(ref1.current.innerHTML).toEqual('blue');
+ rObj.set('');
+ expect(ref2.current.innerHTML).toEqual('Loading...');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('if为primitive值,没有else', () => {
+ let rObj;
+ const ref1 = createRef();
+ const ref2 = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive('blue');
+ rObj = _rObj;
+
+ fn();
+
+ return (
+ // 如果else中的dom和children一个类型,需要增加key,否则会被框架当作同一个dom
+
+ {_rObj}
+
+ );
+ };
+
+ render( , container);
+ expect(ref1.current.innerHTML).toEqual('blue');
+ rObj.set('');
+ expect(ref2.current).toEqual(null);
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('if为reactive object值', () => {
+ let rObj;
+ const ref1 = createRef();
+ const ref2 = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({
+ color: 'blue',
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return (
+ // 如果else中的dom和children一个类型,需要增加key,否则会被框架当作同一个dom
+
+ Loading...
+
+ }
+ >
+
+ {_rObj.color}
+
+
+ );
+ };
+
+ render( , container);
+ expect(ref1.current.innerHTML).toEqual('blue');
+ rObj.color.set('');
+ expect(ref2.current.innerHTML).toEqual('Loading...');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('if为函数', () => {
+ let rObj;
+ const ref1 = createRef();
+ const ref2 = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({
+ color: 'blue',
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return (
+ // 如果else中的dom和children一个类型,需要增加key,否则会被框架当作同一个dom
+ _rObj.color}
+ else={
+
+ Loading...
+
+ }
+ >
+
+ {_rObj.color}
+
+
+ );
+ };
+
+ render( , container);
+ expect(ref1.current.innerHTML).toEqual('blue');
+ rObj.color.set('');
+ expect(ref2.current.innerHTML).toEqual('Loading...');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('if的children、else是函数', () => {
+ const ref1 = createRef();
+ const ref2 = createRef();
+ const fn = jest.fn();
+ const _count = reactive(0);
+ const _rObj = reactive({
+ color: 'blue',
+ });
+
+ const App = () => {
+ fn();
+
+ return (
+ // 如果else中的dom和children一个类型,需要增加key,否则会被框架当作同一个dom
+ _rObj.color}
+ else={() => (
+
+ Loading...
+
+ )}
+ >
+ {() => {
+ const text = useCompute(() => {
+ return _rObj.color.get() + _count.get();
+ });
+
+ return (
+
+ {text}
+
+ );
+ }}
+
+ );
+ };
+
+ render( , container);
+ expect(ref1.current.innerHTML).toEqual('blue0');
+ // 修改children函数中使用到的响应式变量,也会触发Show组件更新
+ _count.set(1);
+ expect(ref1.current.innerHTML).toEqual('blue1');
+ _rObj.color.set('');
+ expect(ref2.current.innerHTML).toEqual('Loading...');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-switch.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-switch.test.js
new file mode 100644
index 00000000..de736293
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-switch.test.js
@@ -0,0 +1,75 @@
+/*
+ * 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 Inula, { render, createRef, act, useReactive, Show, Switch } from '../../../../src/index';
+
+describe('测试 Switch 组件', () => {
+ it('Switch 配合 Show 使用', () => {
+ let rObj;
+ const refBlue = createRef();
+ const refRed = createRef();
+ const refYellow = createRef();
+ const refNothing = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive('blue');
+ rObj = _rObj;
+
+ fn();
+
+ return (
+ nothing}>
+ {/*if不能写成 _rObj === 'red' 或者 _rObj.get() === 'red' */}
+ _rObj.get() === 'blue'}>
+
+ {_rObj}
+
+
+ _rObj.get() === 'red'}>
+
+ {_rObj}
+
+
+ _rObj.get() === 'yellow'}>
+
+ {_rObj}
+
+
+
+ );
+ };
+
+ render( , container);
+ expect(refBlue.current.innerHTML).toEqual('blue');
+ // rObj被3个RContext依赖,分别是Switch组件、Show组件、div[id=1]的Children
+ expect(rObj.usedRContexts.size).toEqual(3);
+
+ act(() => {
+ rObj.set('red');
+ });
+ expect(refRed.current.innerHTML).toEqual('red');
+ // rObj被3个Effect依赖,分别是Switch组件、Show组件、div[id=2]的Children
+ expect(rObj.usedRContexts.size).toEqual(3);
+
+ act(() => {
+ rObj.set('black');
+ });
+ expect(refNothing.current.innerHTML).toEqual('nothing');
+ // rObj被1个RContext依赖,分别是Switch组件
+ expect(rObj.usedRContexts.size).toEqual(1);
+
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/computed.test.js b/packages/inula/scripts/__tests__/ReactivityTest/computed.test.js
new file mode 100644
index 00000000..78ddac3c
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/computed.test.js
@@ -0,0 +1,51 @@
+import Inula, { computed, createRef, reactive, render } from '../../../src/index';
+
+describe('测试 computed', () => {
+ it('在class组件render中使用computed', () => {
+ let rObj;
+ let appInst;
+ const ref = createRef();
+ const fn = jest.fn();
+
+ class App extends Inula.Component {
+ constructor(props) {
+ super(props);
+
+ appInst = this;
+
+ this.state = {
+ name: 1,
+ };
+
+ this._rObj = reactive(1);
+ rObj = this._rObj;
+ }
+
+ render() {
+ const computedVal = computed(() => {
+ fn();
+ return this._rObj.get() + '!!!';
+ });
+
+ return {computedVal}
;
+ }
+ }
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('1!!!'); // computed执行2次
+ expect(fn).toHaveBeenCalledTimes(1);
+ rObj.set('2');
+ expect(ref.current.innerHTML).toEqual('2!!!');
+ expect(fn).toHaveBeenCalledTimes(2); // computed执行2次
+
+ // 触发组件重新渲染
+ appInst.setState({ name: 2 });
+
+ expect(fn).toHaveBeenCalledTimes(3); // 生成新的一个computation,再执行了1次,computed总共执行3次
+
+ rObj.set('3');
+ expect(ref.current.innerHTML).toEqual('3!!!');
+
+ expect(fn).toHaveBeenCalledTimes(5); // 两个computation各执行了一次,computed总共执行5次
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-compute.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-compute.test.js
new file mode 100644
index 00000000..716d36cb
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-compute.test.js
@@ -0,0 +1,377 @@
+/*
+ * 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 Inula, {
+ createRef,
+ For,
+ reactive,
+ render,
+ useCompute,
+ useReactive,
+ computed,
+} from '../../../src/index';
+
+describe('computed 基本使用', () => {
+ it('computed 返回的是一个响应式对象,用到的响应式对象是原始类型', () => {
+ const rObj = reactive('123');
+ const comp = computed(() => {
+ return rObj.get() + '!!!';
+ });
+ expect(comp.get()).toEqual('123!!!');
+
+ rObj.set('456');
+ expect(comp.get()).toEqual('456!!!');
+ });
+
+ it('computed 返回的是一个响应式对象,用到两个响应式对象', () => {
+ const rObj1 = reactive({ name: 'xiaoming' });
+ const rObj2 = reactive({ age: 18 });
+ const comp = computed(() => {
+ return rObj1.name.get() + ' is ' + rObj2.age.get();
+ });
+ expect(comp.get()).toEqual('xiaoming is 18');
+ rObj1.name.set('xiaowang');
+ rObj2.set(prev => ({ age: prev.age + 2 }));
+ expect(comp.get()).toEqual('xiaowang is 20');
+ });
+
+ it('computed 返回的是一个复杂响应式对象', () => {
+ const rObj = reactive({ array: [1, 2, 3, 4, 5, 6] });
+ const comp = computed(() => {
+ return { newArray: rObj.array.get().filter(x => x > 4) };
+ });
+ expect(comp.get()).toEqual({ newArray: [5, 6] });
+ expect(comp.newArray.get()).toEqual([5, 6]);
+ rObj.array.push(...[100]);
+ expect(comp.get()).toEqual({ newArray: [5, 6, 100] });
+ });
+
+ it('computed 返回的是一个响应式对象,用到的响应式对象是对象类型', () => {
+ const rObj = reactive({ array: [1, 2, 3, 4, 5, 6] });
+ const comp = computed(() => {
+ return rObj.array.get().filter(x => x > 4);
+ });
+ expect(comp.get()).toEqual([5, 6]);
+
+ rObj.array.set([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ expect(comp.get()).toEqual([5, 6, 7, 8, 9]);
+
+ rObj.array.push(...[10, 11]);
+ expect(comp.get()).toEqual([5, 6, 7, 8, 9, 10, 11]);
+
+ rObj.set({ array: [100, 101, 102] });
+ expect(comp.get()).toEqual([100, 101, 102]);
+ });
+
+ it('computed 返回的是一个复杂响应式对象2', () => {
+ const rObj = reactive({ array: [1, 2, 3, 4, 5, 6] });
+ const comp = computed(() => {
+ return { newArray: rObj.array.get().filter(x => x > 4) };
+ });
+ expect(comp.newArray.get()).toEqual([5, 6]);
+ rObj.array.push(...[7, 8]);
+ expect(comp.newArray.get()).toEqual([5, 6, 7, 8]);
+ rObj.array.set([1, 100, 101, 102]);
+ expect(comp.newArray.get()).toEqual([100, 101, 102]);
+ expect(comp.get()).toEqual({ newArray: [100, 101, 102] });
+ });
+});
+
+describe('测试 useCompute', () => {
+ it('useComputed基本使用 使用get方法(组件式更新)', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive('123');
+ rObj = _rObj;
+
+ const _cObj = useCompute(() => {
+ return _rObj.get() + '!!!';
+ });
+ fn();
+
+ return {_cObj.get()}
;
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('123!!!');
+ expect(fn).toHaveBeenCalledTimes(1);
+ rObj.set('456');
+ expect(ref.current.innerHTML).toEqual('456!!!');
+ expect(fn).toHaveBeenCalledTimes(2);
+ rObj.set('789');
+ expect(ref.current.innerHTML).toEqual('789!!!');
+ expect(fn).toHaveBeenCalledTimes(3);
+ });
+
+ it('useComputed基本使用 直接使用对象(Dom级更新)', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive('123');
+ rObj = _rObj;
+
+ const _cObj = useCompute(() => {
+ return _rObj.get() + '!!!';
+ });
+ fn();
+
+ return {_cObj}
;
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('123!!!');
+ expect(fn).toHaveBeenCalledTimes(1);
+ rObj.set('456');
+ expect(ref.current.innerHTML).toEqual('456!!!');
+ rObj.set('789');
+ expect(ref.current.innerHTML).toEqual('789!!!');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('useComputed 基本使用2', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const compFn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({ array: [1, 2, 3, 4, 5, 6] });
+ rObj = _rObj;
+
+ const cObj = useCompute(() => {
+ compFn();
+ return { len: _rObj.array.get().filter(x => x >= 4).length };
+ });
+
+ fn();
+
+ return {cObj.len}
;
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('3');
+ rObj.array.push(...[7, 8]);
+ expect(ref.current.innerHTML).toEqual('5');
+ expect(fn).toHaveBeenCalledTimes(1);
+ rObj.array.unshift(...[0, 100]);
+ expect(ref.current.innerHTML).toEqual('6');
+ rObj.set({ array: [1, 100, 101, 102, 103] });
+ expect(ref.current.innerHTML).toEqual('4');
+ expect(compFn).toHaveBeenCalledTimes(4);
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('连锁useComputed使用', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const compFn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive(1);
+ rObj = _rObj;
+
+ const double = useCompute(() => _rObj.get() * 2);
+ const dd = useCompute(() => {
+ compFn();
+ return double.get() * 2;
+ });
+
+ fn();
+
+ return {dd}
;
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('4');
+ expect(compFn).toHaveBeenCalledTimes(1);
+ rObj.set('2');
+ expect(ref.current.innerHTML).toEqual('8');
+ expect(compFn).toHaveBeenCalledTimes(2);
+ rObj.set('4');
+ expect(ref.current.innerHTML).toEqual('16');
+ expect(compFn).toHaveBeenCalledTimes(3);
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('useComputed中使用到了两个响应式对象', () => {
+ let _rObj1;
+ let _rObj2;
+ const ref = createRef();
+ const fn = jest.fn();
+ const compFn = jest.fn();
+ const App = () => {
+ const rObj1 = useReactive({ name: 'xiaoming' });
+ const rObj2 = useReactive({ age: 18 });
+ _rObj1 = rObj1;
+ _rObj2 = rObj2;
+
+ const words = useCompute(() => {
+ compFn();
+ return `${rObj1.name.get()} is ${rObj2.age.get()}`;
+ });
+
+ fn();
+
+ return {words}
;
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('xiaoming is 18');
+ expect(compFn).toHaveBeenCalledTimes(1);
+ _rObj1.name.set('xiaowang');
+ expect(ref.current.innerHTML).toEqual('xiaowang is 18');
+ expect(compFn).toHaveBeenCalledTimes(2);
+ _rObj2.set({ age: 20 });
+ expect(ref.current.innerHTML).toEqual('xiaowang is 20');
+ expect(compFn).toHaveBeenCalledTimes(3);
+ _rObj1.name.set('laowang');
+ _rObj2.set({ age: 30 });
+ expect(ref.current.innerHTML).toEqual('laowang is 30');
+ expect(compFn).toHaveBeenCalledTimes(5);
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('多个reactive的compute', () => {
+ let a;
+ const ref = createRef();
+ const compFn = jest.fn();
+ const computeFn = jest.fn();
+ const App = () => {
+ const _a = useReactive('a');
+ const b = useReactive('b');
+ const cond = useReactive(true);
+ a = _a;
+ const compute = useCompute(() => {
+ computeFn();
+ return cond.get() ? _a.get() : b.get();
+ });
+
+ compFn();
+
+ return (
+ {
+ cond.set(false);
+ }}
+ >
+ {compute}
+
+ );
+ };
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('a');
+ ref.current.click();
+ expect(ref.current.innerHTML).toEqual('b');
+ a.set('aa');
+
+ expect(computeFn).toHaveBeenCalledTimes(3);
+
+ expect(ref.current.innerHTML).toEqual('b');
+ });
+
+ it('useCompute返回一个数组对象', () => {
+ let rObj;
+ let cObj;
+ let ref = createRef();
+ let appFn = jest.fn();
+ let itemFn = jest.fn();
+
+ const App = () => {
+ const _rObj = useReactive([
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ { id: 'id-3', name: 'p3' },
+ ]);
+ rObj = _rObj;
+
+ const _cObj = useCompute(() => {
+ return _rObj.get().slice();
+ });
+ cObj = _cObj;
+
+ appFn();
+
+ return (
+
+
+ {item => {
+ itemFn();
+ return (
+
+ {item.name}
+
+ );
+ }}
+
+
+ );
+ };
+
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(3);
+
+ rObj.push({ id: 'id-4', name: 'p4' });
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(4);
+
+ // rObj[1].name.get();
+ rObj[1].set({ id: 'id-2', name: 'p222' });
+ let li = container.querySelector('#id-2');
+
+ expect(li.innerHTML).toEqual('p222');
+
+ // // 更新
+ // cObj.set([true]);
+ //
+ // items = container.querySelectorAll('li');
+ // expect(items.length).toEqual(1);
+ // expect(appFn).toHaveBeenCalledTimes(1);
+ //
+ // // 第一次渲染执行3次,更新也触发了1次
+ // expect(itemFn).toHaveBeenCalledTimes(4);
+ });
+
+ xit('测试compute在checkbox中的使用', () => {
+ let a;
+ const ref = createRef();
+ const compFn = jest.fn();
+ const computeFn = jest.fn();
+ const App = () => {
+ const rObj = useReactive({ checked: true });
+ const checked = useCompute(() => {
+ return rObj.checked.get();
+ });
+
+ compFn();
+
+ return ;
+ };
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('a');
+ ref.current.click();
+ expect(ref.current.innerHTML).toEqual('b');
+ a.set('aa');
+
+ expect(computeFn).toHaveBeenCalledTimes(3);
+
+ expect(ref.current.innerHTML).toEqual('b');
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-memory.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-memory.test.js
new file mode 100644
index 00000000..70877a52
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-memory.test.js
@@ -0,0 +1,77 @@
+/*
+ * 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 Inula, { render, createRef, act, useReactive } from '../../../src/index';
+import { Show } from '../../../src/reactive/components/Show';
+import { Switch } from '../../../src/reactive/components/Switch';
+
+describe('响应式数据usedRContexts', () => {
+ it('测试响应式数据的usedRContexts会随着VNode的删除而清除', () => {
+ let rObj;
+ const refBlue = createRef();
+ const refRed = createRef();
+ const refYellow = createRef();
+ const refNothing = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive('blue');
+ rObj = _rObj;
+
+ fn();
+
+ return (
+ nothing}>
+ {/*if不能写成 _rObj === 'red' 或者 _rObj.get() === 'red' */}
+ _rObj.get() === 'blue'}>
+
+ {_rObj}
+
+
+ _rObj.get() === 'red'}>
+
+ {_rObj}
+
+
+ _rObj.get() === 'yellow'}>
+
+ {_rObj}
+
+
+
+ );
+ };
+
+ render( , container);
+ expect(refBlue.current.innerHTML).toEqual('blue');
+ // rObj被3个RContext依赖,分别是Switch组件、Show组件、div[id=1]的Children
+ expect(rObj.usedRContexts.size).toEqual(3);
+
+ act(() => {
+ rObj.set('red');
+ });
+ expect(refRed.current.innerHTML).toEqual('red');
+ // rObj被3个Effect依赖,分别是Switch组件、Show组件、div[id=2]的Children
+ expect(rObj.usedRContexts.size).toEqual(3);
+
+ act(() => {
+ rObj.set('black');
+ });
+ expect(refNothing.current.innerHTML).toEqual('nothing');
+ // rObj被1个RContext依赖,分别是Switch组件
+ expect(rObj.usedRContexts.size).toEqual(1);
+
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-mix-use.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-mix-use.test.js
new file mode 100644
index 00000000..ef145de7
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-mix-use.test.js
@@ -0,0 +1,60 @@
+/*
+ * 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 Inula, {createRef, render, useReactive, useState, Show} from '../../../src/index';
+
+describe('传统API和响应式API混合使用', () => {
+ it('混合使用1', () => {
+ let rObj, isShow, update;
+ const ref = createRef();
+ const fn = jest.fn();
+
+ const App = () => {
+ const _isShow = useReactive(true);
+ isShow = _isShow;
+
+ const [_, setState] = useState({});
+
+ update = () => setState({});
+ return (
+
+
+
+ );
+ };
+
+ const Child = () => {
+ const _rObj = useReactive('blue');
+ rObj = _rObj;
+
+ fn();
+
+ return
;
+ };
+
+ render( , container);
+ expect(ref.current.className).toEqual('blue');
+
+ // 改变了DOM结构
+ isShow.set(false);
+ expect(ref.current).toEqual(null);
+
+ update();
+
+ expect(ref.current).toEqual(null);
+ });
+
+
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-mixed-children.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-mixed-children.test.js
new file mode 100644
index 00000000..c6d81e22
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-mixed-children.test.js
@@ -0,0 +1,64 @@
+/*
+ * 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 Inula, { render, createRef, act, useReactive } from '../../../src/index';
+
+describe('测试混合型的 children', () => {
+ it('children是 字符串+Atom 场景', () => {
+ let rObj;
+ const ref1 = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive(0);
+ rObj = _rObj;
+
+ fn();
+
+ return (
+ // div下面有多个元素
+ Count: {_rObj}
+ );
+ };
+
+ render( , container);
+ expect(ref1.current.innerHTML).toEqual('Count: 0');
+ rObj.set(1);
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(ref1.current.innerHTML).toEqual('Count: 1');
+ });
+
+ it('children是 字符串+Atom 场景2', () => {
+ let rObj;
+ const ref1 = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({ count: 0 });
+ rObj = _rObj;
+
+ fn();
+
+ return (
+ // div下面有多个元素
+ Count: {_rObj.count}
+ );
+ };
+
+ render( , container);
+ expect(ref1.current.innerHTML).toEqual('Count: 0');
+ rObj.count.set(1);
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(ref1.current.innerHTML).toEqual('Count: 1');
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-object.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-object.test.js
new file mode 100644
index 00000000..90ae29fc
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-object.test.js
@@ -0,0 +1,973 @@
+/*
+ * 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 Inula, {
+ render,
+ createRef,
+ useReactive,
+ useCompute,
+ reactive,
+ computed,
+ watchReactive,
+} from '../../../src/index';
+import { GET_R_NODE } from '../../../src/reactive/proxy/RProxyHandler';
+import { isAtom, isReactiveProxy, isRNode } from '../../../src/reactive/Utils';
+
+describe('测试 useReactive(对象)', () => {
+ it('reactive基本使用', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({
+ color: 'blue',
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return {_rObj.color}
;
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('blue');
+ rObj.color.set('red');
+ expect(rObj.color.get()).toEqual('red');
+ expect(ref.current.innerHTML).toEqual('red');
+ rObj.color.set(prev => prev + '!!');
+ expect(rObj.color.get()).toEqual('red!!');
+ expect(ref.current.innerHTML).toEqual('red!!');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('响应式对象赋值修改为一个对象', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({
+ data: { framework: 'Vue' },
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return {_rObj.data.framework}
;
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('Vue');
+ rObj.data.set({ framework: 'React' });
+ expect(rObj.data.framework.get()).toEqual('React');
+ expect(ref.current.innerHTML).toEqual('React');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('赋值修改复杂响应式对象', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({
+ data: { framework: { js: 'Vue' } },
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return {_rObj.data.framework.js}
;
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('Vue');
+ rObj.data.set({ framework: { js: 'React' } });
+ expect(rObj.data.framework.get()).toEqual({ js: 'React' });
+ expect(ref.current.innerHTML).toEqual('React');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('赋值修改响应式对象中Atom的值', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({
+ rdata: { framework: 'Vue' },
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return {_rObj.rdata.framework}
;
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('Vue');
+ rObj.rdata.framework.set('React');
+ expect(rObj.rdata.get()).toEqual({ framework: 'React' });
+ expect(ref.current.innerHTML).toEqual('React');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('把响应式属性传递到子组件', () => {
+ let rObj;
+ const ref = createRef();
+ const fn1 = jest.fn();
+ const fn2 = jest.fn();
+
+ const App = () => {
+ const _rObj = useReactive({
+ data: {
+ color: 'blue',
+ },
+ });
+ rObj = _rObj;
+
+ fn1();
+ return ;
+ };
+
+ const Child = ({ color }) => {
+ fn2();
+
+ const cl = useCompute(() => {
+ return 'cl-' + color.get();
+ });
+
+ return
;
+ };
+
+ render( , container);
+ expect(ref.current.className).toEqual('cl-blue');
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(fn2).toHaveBeenCalledTimes(1);
+ rObj.data.color.set('red');
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(fn2).toHaveBeenCalledTimes(1);
+ expect(ref.current.className).toEqual('cl-red');
+ });
+
+ it('reactive对象中“原始数据”被赋值为“对象”', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({
+ data: 'blue',
+ });
+ rObj = _rObj;
+
+ _rObj.data.set({ color: 'red' });
+
+ fn();
+
+ return {_rObj.data.color}
;
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('red');
+ rObj.data.color.set('blue');
+ expect(rObj.data.color.get()).toEqual('blue');
+ expect(ref.current.innerHTML).toEqual('blue');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('reactive对象中“对象”被赋值为“新对象”', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({
+ data: {
+ cl: 'blue',
+ },
+ });
+ rObj = _rObj;
+
+ _rObj.data.set({ color: 'red' });
+
+ fn();
+
+ return {_rObj.data.color}
;
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('red');
+ rObj.data.color.set('blue');
+ expect(rObj.data.color.get()).toEqual('blue');
+ expect(ref.current.innerHTML).toEqual('blue');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe('测试reactive数组', () => {
+ it('reactive“数组”length的使用', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({
+ data: [
+ { name: 'p1', age: 1 },
+ { name: 'p2', age: 2 },
+ ],
+ });
+ rObj = _rObj;
+
+ fn();
+
+ // 在DOM中使用length无法精细响应式
+ return {_rObj.data.length}
;
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('2');
+ rObj.data.set([{ name: 'p1', age: 1 }]);
+ expect(ref.current.innerHTML).toEqual('1');
+ expect(fn).toHaveBeenCalledTimes(2);
+ });
+
+ it('reactive“数组”的使用', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({
+ data: [
+ { name: 'p1', age: 1 },
+ { name: 'p2', age: 2 },
+ ],
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return {_rObj.data[0].name}
;
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('p1');
+ // 这种修改无法响应!
+ // rObj.data.set([
+ // { name: 'p11', age: 1 },
+ // ]);
+
+ // 直接修改数组中被使用属性
+ rObj.data[0].name.set('p11');
+ expect(ref.current.innerHTML).toEqual('p11');
+ // 在DOM中使用length无法精细响应式
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('jsx中通过items.get().map遍历reactive“数组”', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({
+ items: [
+ { name: 'p1', id: 1 },
+ { name: 'p2', id: 2 },
+ ],
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return (
+
+ {_rObj.items.get().map(item => {
+ return
{item.name} ;
+ })}
+
+ );
+ };
+
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+
+ // 每次修改items都会触发整个组件刷新
+ rObj.items.set([{ name: 'p11', age: 1 }]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(1);
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ // 每次修改items都会触发整个组件刷新
+ rObj.items.push({ name: 'p22', id: 2 });
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+ expect(fn).toHaveBeenCalledTimes(3);
+ });
+
+ it('jsx中通过items.get().map遍历reactive“数组”,孩子是Item', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const Item = ({ item }) => {
+ return {item.name} ;
+ };
+
+ const App = () => {
+ const _rObj = useReactive({
+ items: [
+ { name: 'p1', id: 1 },
+ { name: 'p2', id: 2 },
+ ],
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return (
+
+ {/*items必须要调用get()才能map*/}
+ {_rObj.items.get().map(item => {
+ return ;
+ })}
+
+ );
+ };
+
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ // 每次修改items都会触发整个组件刷新
+ rObj.items.set([{ name: 'p11', age: 1 }]);
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(1);
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ // 每次修改items都会触发整个组件刷新
+ rObj.items.push({ name: 'p22', id: 2 });
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(2);
+ expect(fn).toHaveBeenCalledTimes(3);
+ });
+
+ it('jsx中通过items.map遍历reactive“数组”,具有响应式', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const Item = ({ item }) => {
+ return {item.name} ;
+ };
+
+ const App = () => {
+ const _rObj = useReactive({
+ items: [
+ { name: 'p1', id: 1 },
+ { name: 'p2', id: 2 },
+ { name: 'p3', id: 3 },
+ ],
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return (
+
+ {_rObj.items.map(item => {
+ return ;
+ })}
+
+ );
+ };
+
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(3);
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ rObj.items.set([
+ { name: 'p11', age: 1 },
+ { name: 'p22', age: 2 },
+ ]);
+
+ items = container.querySelectorAll('li');
+ // 子元素不会响应式变化
+ expect(items.length).toEqual(2);
+ expect(fn).toHaveBeenCalledTimes(2);
+ });
+
+ it('jsx中通过items.map遍历reactive“数组”,孩子是Item,Item对象具有响应式', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const Item = ({ item }) => {
+ const id = useCompute(() => {
+ return `id-${item.id.get()}`;
+ });
+
+ return (
+
+ {item.name}
+
+ );
+ };
+
+ const App = () => {
+ const _rObj = useReactive({
+ items: [
+ { name: 'p1', id: 1 },
+ { name: 'p2', id: 2 },
+ ],
+ });
+ rObj = _rObj;
+
+ fn();
+
+ return (
+
+ {_rObj.items.map(item => {
+ return ;
+ })}
+
+ );
+ };
+
+ render( , container);
+ let item = container.querySelector('#id-1');
+ expect(item.innerHTML).toEqual('p1');
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ rObj.items[0].name.set('p111');
+ item = container.querySelector('#id-1');
+ // 子元素会响应式变化
+ expect(item.innerHTML).toEqual('p111');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('测试响应式数据', () => {
+ const obj = reactive({
+ data: [
+ {
+ id: '1',
+ value: 'val-1',
+ },
+ {
+ id: '2',
+ value: 'val-2',
+ },
+ ],
+ });
+
+ // 使用,让创建children
+ obj.data[1].value.read();
+
+ obj.set({
+ data: [
+ {
+ id: '11',
+ value: 'val-11',
+ },
+ ],
+ });
+
+ obj.set({
+ data: [
+ {
+ id: '111',
+ value: 'val-111',
+ },
+ {
+ id: '222',
+ value: 'val-222',
+ },
+ ],
+ });
+
+ expect(obj.data[1].value.get()).toEqual('val-222');
+ });
+
+ it('响应式对象为复杂对象时,使用set重新设置', () => {
+ const rObj = reactive({ data: [1, 2, 3, 4, 5, 6] });
+ rObj.data.push(...[7, 8]);
+ expect(rObj.data.get()).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
+ rObj.data.set([100, 101]);
+ expect(rObj.get()).toEqual({ data: [100, 101] });
+ });
+
+ it('使用set直接修改响应式对象数组中某个元素的值', () => {
+ const rObj = reactive({ data: [1, 2, 3] });
+ rObj.data.push(...[4, 5, 6]);
+ expect(rObj.data.get()).toEqual([1, 2, 3, 4, 5, 6]);
+
+ // 修改数组第4个元素
+ rObj.data[1].set({ val: 2 });
+ expect(rObj.get()).toEqual({ data: [1, { val: 2 }, 3, 4, 5, 6] });
+ });
+
+ it('使用set直接修改响应式对象数组中某个元素的值2', () => {
+ const rObj = reactive({ data: [1, 2, 3] });
+ rObj.data.push(...[4, 5, 6]);
+ expect(rObj.data.get()).toEqual([1, 2, 3, 4, 5, 6]);
+
+ // 修改数组第4个元素
+ rObj.data[4].set({ val: 2 });
+ expect(rObj.get()).toEqual({ data: [1, 2, 3, 4, { val: 2 }, 6] });
+ });
+
+ it('在删除数组中一个数字,再加一个对象,类型是RNode', () => {
+ const rObj = reactive({ data: [1, 2, 3, 4] });
+ // 使用最后一个数据,在children中创建出child
+ rObj.data[3].get();
+ // 删除最后一个数据
+ rObj.data.set([1, 2, 3]);
+ // 重新增加一个obj类型的数据
+ rObj.data.set([1, 2, 3, { val: 4 }]);
+
+ // rObj.data[3]是RNode
+ expect(isRNode(rObj.data[3][GET_R_NODE])).toBeTruthy();
+
+ expect(rObj.data[3].val.get()).toEqual(4);
+ });
+
+ xit('钻石问题', () => {
+ const fn = jest.fn();
+ const rObj = reactive(0);
+ const evenOrOdd = computed(() => (rObj.get() % 2 === 0 ? 'even' : 'odd'));
+
+ watchReactive(() => {
+ fn();
+ rObj.get();
+ evenOrOdd.get();
+ });
+
+ rObj.set(1);
+
+ // TODO
+ expect(fn).toHaveBeenCalledTimes(3);
+ });
+
+ it('数组中的数据由“对象”变成“字符串”', () => {
+ let fn = jest.fn();
+ let fn1 = jest.fn();
+ const rObj = reactive({
+ items: [
+ { name: 'p1', id: 1 },
+ { name: { n: 'p22' }, id: 2 },
+ ],
+ });
+
+ watchReactive(rObj.items[1].name, () => {
+ fn();
+ });
+ watchReactive(rObj.items[1].name.n, () => {
+ fn1();
+ });
+
+ rObj.items.set([
+ { name: 'p1', id: 1 },
+ { name: 'p2', id: 2 }, // name 改为 基本数据类型
+ ]);
+
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ // 无法触发fn1
+ expect(fn1).toHaveBeenCalledTimes(0);
+ });
+
+ it('数组中的数据由“字符串”变成“对象”', () => {
+ let fn = jest.fn();
+ let fn1 = jest.fn();
+ const rObj = reactive({
+ items: [
+ { name: 'p1', id: 1 },
+ { name: 'p2', id: 2 },
+ ],
+ });
+
+ watchReactive(rObj.items[1].name, () => {
+ fn();
+ });
+ // 允许使用或监听没有定义的属性
+ watchReactive(rObj.items[1].name.n, () => {
+ fn1();
+ });
+
+ rObj.items.set([
+ { name: 'p1', id: 1 },
+ { name: { n: 'p22' }, id: 2 },
+ ]);
+
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ // 可以触发fn1
+ expect(fn1).toHaveBeenCalledTimes(1);
+ });
+
+ it('访问一个不存在的属性,会抛出异常', () => {
+ let fn = jest.fn();
+ let fn1 = jest.fn();
+ const rObj = reactive({
+ items: [{ name: 'p1' }, { name: 'p2' }],
+ });
+
+ watchReactive(() => {
+ rObj.items[1].get();
+ fn();
+ });
+ watchReactive(() => {
+ // 会抛异常
+ rObj.items[1].name.n.get();
+ fn1();
+ });
+
+ rObj.items.set([{ name: 'p1' }, { name: { n: 'p22' } }]);
+
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ // 无法触发fn1
+ expect(fn1).toHaveBeenCalledTimes(2);
+ });
+
+ it('数组中的数据由“字符串”变成“对象”3', () => {
+ let fn = jest.fn();
+ let fn1 = jest.fn();
+ const rObj = reactive({
+ items: [{ a: 1 }, 2, 3],
+ });
+
+ watchReactive(() => {
+ rObj.items[1].get();
+ fn();
+ });
+
+ rObj.items.set([2, 3, 4]);
+
+ expect(fn).toHaveBeenCalledTimes(2);
+ });
+
+ it('数组中的数据由“数字”变成“对象”', () => {
+ let fn = jest.fn();
+ let fn1 = jest.fn();
+ const rObj = reactive({
+ items: [1, 2, 3],
+ });
+
+ watchReactive(() => {
+ rObj.items[0].get();
+ fn();
+ });
+
+ watchReactive(() => {
+ rObj.get();
+ fn1();
+ });
+
+ rObj.items.set([{ a: 1 }, 3, 4]);
+
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ // 父数据也会触发
+ expect(fn1).toHaveBeenCalledTimes(2);
+ });
+
+ it('数组中的数据由“对象”变成“数组”', () => {
+ let fn = jest.fn();
+ let fn1 = jest.fn();
+ const rObj = reactive({
+ items: [{ a: 1 }, 2, 3],
+ });
+
+ watchReactive(() => {
+ rObj.items[0].get();
+ fn();
+ });
+
+ watchReactive(() => {
+ rObj.get();
+ fn1();
+ });
+
+ rObj.items.set([[1], 3, 4]);
+
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ // 父数据也会触发
+ expect(fn1).toHaveBeenCalledTimes(2);
+ });
+
+ it('数组中的数据由“空数组”变成“空数组”', () => {
+ let fn = jest.fn();
+ let fn1 = jest.fn();
+ const rObj = reactive({
+ items: [[], 2, 3],
+ });
+
+ watchReactive(() => {
+ rObj.items[0].get();
+ fn();
+ });
+
+ watchReactive(() => {
+ rObj.get();
+ fn1();
+ });
+
+ rObj.items.set([[], 3, 4]);
+
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ // 父数据也会触发
+ expect(fn1).toHaveBeenCalledTimes(2);
+ });
+
+ it('数组中的2个数据由“对象”变成“对象”', () => {
+ let fn = jest.fn();
+ let fn1 = jest.fn();
+ const rObj = reactive({
+ items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3],
+ });
+
+ watchReactive(() => {
+ rObj.items[0].a.get();
+ rObj.items[0].b.get();
+ fn();
+ });
+
+ watchReactive(() => {
+ rObj.get();
+ fn1();
+ });
+
+ // 第一个a 由{b: 1} -> {b: 2}能够精准更新
+ rObj.items.set([{ a: { b: 2 }, b: { c: 3 } }, { a: 3 }, 4]);
+
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ // 父数据也会触发
+ expect(fn1).toHaveBeenCalledTimes(2);
+ });
+
+ it('数组中的2个数据由“对象”变成“对象”,前一个属性能精准更新,会触发后面那个', () => {
+ let fn = jest.fn();
+ let fn1 = jest.fn();
+ let fn2 = jest.fn();
+ let fn3 = jest.fn();
+ let fn4 = jest.fn();
+ const rObj = reactive({
+ items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3],
+ });
+
+ watchReactive(() => {
+ rObj.items[0].a.get();
+ fn();
+ });
+ watchReactive(() => {
+ // b由 { c: 2 } -> { c: 3 } 可以触发
+ rObj.items[0].b.get();
+ fn1();
+ });
+ watchReactive(() => {
+ // b由 1 -> undefined 可以触发
+ rObj.items[0].a.b.get();
+ fn2();
+ });
+ watchReactive(() => {
+ // c由 2 -> 3 可以触发
+ rObj.items[0].b.c.get();
+ fn3();
+ });
+ watchReactive(() => {
+ rObj.get();
+ fn4();
+ });
+
+ // 第一个a 由{b: 1} -> {d: 2}能够精准更新
+ rObj.items.set([{ a: { d: 2 }, b: { c: 3 } }, { a: 3 }, 4]);
+
+ expect(fn).toHaveBeenCalledTimes(2);
+ expect(fn1).toHaveBeenCalledTimes(2);
+ expect(fn2).toHaveBeenCalledTimes(2);
+ expect(fn3).toHaveBeenCalledTimes(2);
+
+ // 父数据也会触发
+ expect(fn4).toHaveBeenCalledTimes(2);
+ });
+
+ it('数组中的2个数据由“对象”变成“对象”,前一个属性不能精准更新,也不再触发后面那个', () => {
+ let fn = jest.fn();
+ let fn1 = jest.fn();
+ let fn2 = jest.fn();
+ let fn3 = jest.fn();
+ let fn4 = jest.fn();
+ const rObj = reactive({
+ items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3],
+ });
+
+ watchReactive(() => {
+ rObj.items[0].a.get();
+ fn();
+ });
+ watchReactive(() => {
+ // b由 { c: 2 } -> { c: 3 } 可以触发
+ rObj.items[0].b.get();
+ fn1();
+ });
+ watchReactive(() => {
+ rObj.items[0].a.b.get();
+ fn2();
+ });
+ watchReactive(() => {
+ rObj.items[0].b.c.get();
+ fn3();
+ });
+ watchReactive(() => {
+ rObj.get();
+ fn4();
+ });
+
+ // 第一个 a 由{b: 1} -> 1 不能够精准更新
+ rObj.items.set([{ a: 1, b: { c: 3 } }, { a: 3 }, 4]);
+
+ expect(fn).toHaveBeenCalledTimes(2);
+ expect(fn1).toHaveBeenCalledTimes(2);
+ // 由 { b: 1 } -> 1 是不会触发 b 精准更新
+ expect(fn2).toHaveBeenCalledTimes(1);
+ // 前一个属性不能精准更新,也不触发后面那个的精准更新
+ expect(fn3).toHaveBeenCalledTimes(1);
+
+ // 父数据也会触发
+ expect(fn4).toHaveBeenCalledTimes(2);
+ });
+
+ it('数组中的2个数据由“对象”变成“[]”,前一个属性不能精准更新,也不再触发后面那个', () => {
+ let fn = jest.fn();
+ let fn1 = jest.fn();
+ let fn2 = jest.fn();
+ let fn3 = jest.fn();
+ let fn4 = jest.fn();
+ const rObj = reactive({
+ items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3],
+ });
+
+ watchReactive(() => {
+ rObj.items[0].a.get();
+ fn();
+ });
+ watchReactive(() => {
+ // b由 { c: 2 } -> { c: 3 } 可以触发
+ rObj.items[0].b.get();
+ fn1();
+ });
+ watchReactive(() => {
+ rObj.items[0].a.b.get();
+ fn2();
+ });
+ watchReactive(() => {
+ rObj.items[0].b.c.get();
+ fn3();
+ });
+ watchReactive(() => {
+ rObj.get();
+ fn4();
+ });
+
+ // 第一个 a 由{b: 1} -> [] 不能够精准更新
+ rObj.items.set([{ a: [], b: { c: 3 } }, { a: 3 }, 4]);
+
+ expect(fn).toHaveBeenCalledTimes(2);
+ expect(fn1).toHaveBeenCalledTimes(2);
+ // 由 { b: 1 } -> 1 是不会触发 b 精准更新
+ expect(fn2).toHaveBeenCalledTimes(1);
+ // 前一个属性不能精准更新,也不触发后面那个的精准更新
+ expect(fn3).toHaveBeenCalledTimes(1);
+
+ // 父数据也会触发
+ expect(fn4).toHaveBeenCalledTimes(2);
+ });
+
+ it('数组中的2个数据由“对象”变成“null”,前一个属性不能精准更新,也不再触发后面那个', () => {
+ let fn = jest.fn();
+ let fn1 = jest.fn();
+ let fn2 = jest.fn();
+ let fn3 = jest.fn();
+ let fn4 = jest.fn();
+ const rObj = reactive({
+ items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3],
+ });
+
+ watchReactive(() => {
+ rObj.items[0].a.get();
+ fn();
+ });
+ watchReactive(() => {
+ // b由 { c: 2 } -> { c: 3 } 可以触发
+ rObj.items[0].b.get();
+ fn1();
+ });
+ watchReactive(() => {
+ rObj.items[0].a.b.get();
+ fn2();
+ });
+ watchReactive(() => {
+ rObj.items[0].b.c.get();
+ fn3();
+ });
+ watchReactive(() => {
+ rObj.get();
+ fn4();
+ });
+
+ // 第一个 a 由{b: 1} -> [] 不能够精准更新
+ rObj.items.set([{ a: null, b: { c: 3 } }, { a: 3 }, 4]);
+
+ expect(fn).toHaveBeenCalledTimes(2);
+ expect(fn1).toHaveBeenCalledTimes(2);
+ // 由 { b: 1 } -> 1 是不会触发 b 精准更新
+ expect(fn2).toHaveBeenCalledTimes(1);
+ // 前一个属性不能精准更新,也不触发后面那个的精准更新
+ expect(fn3).toHaveBeenCalledTimes(1);
+
+ // 父数据也会触发
+ expect(fn4).toHaveBeenCalledTimes(2);
+ });
+
+ it('执行基本数据数组的loop方法', () => {
+ let fn = jest.fn();
+ let fn1 = jest.fn();
+ const rObj = reactive({
+ items: [1, 2, 3, 4],
+ });
+
+ rObj.items.forEach(rItem => {
+ expect(isReactiveProxy(rItem)).toBeTruthy();
+ });
+
+ watchReactive(() => {
+ rObj.items.get();
+ fn();
+ });
+ watchReactive(() => {
+ rObj.get();
+ fn1();
+ });
+
+ rObj.items.set([1, 2, 3]);
+
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ // 父数据也会触发
+ expect(fn1).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-primitive.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-primitive.test.js
new file mode 100644
index 00000000..79f44515
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-primitive.test.js
@@ -0,0 +1,266 @@
+/*
+ * 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 Inula, { render, createRef, useState, useReactive, useCompute } from '../../../src/index';
+
+describe('测试 useReactive(原生数据)', () => {
+ it('reactive.get()作为children', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive('1');
+ rObj = _rObj;
+
+ fn();
+
+ return {_rObj.get()}
;
+ };
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('1');
+ rObj.set('2');
+ expect(ref.current.innerHTML).toEqual('2');
+ expect(fn).toHaveBeenCalledTimes(2);
+ });
+
+ it('reactive作为children', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive('1');
+ rObj = _rObj;
+
+ fn();
+
+ return {_rObj}
;
+ };
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('1');
+ rObj.set('2');
+ expect(ref.current.innerHTML).toEqual('2');
+ rObj.set(prev => prev + '??');
+ expect(ref.current.innerHTML).toEqual('2??');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('reactive.get()作为prop', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive(1);
+ rObj = _rObj;
+
+ fn();
+
+ return
;
+ };
+ render( , container);
+ expect(ref.current.className).toEqual('1');
+ rObj.set(2);
+ expect(ref.current.className).toEqual('2');
+ expect(fn).toHaveBeenCalledTimes(2);
+ });
+
+ it('reactive作为prop', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive(1);
+ rObj = _rObj;
+
+ fn();
+
+ return
;
+ };
+ render( , container);
+ expect(ref.current.className).toEqual('1');
+ rObj.set(2);
+ expect(ref.current.className).toEqual('2');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('reactive.get()传入style', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+
+ const App = () => {
+ const _rObj = useReactive('blue');
+ rObj = _rObj;
+
+ fn();
+
+ return
;
+ };
+ render( , container);
+ const style = window.getComputedStyle(ref.current);
+ expect(style.color).toEqual('blue');
+
+ rObj.set('red');
+ expect(ref.current.style.color).toEqual('red');
+
+ expect(fn).toHaveBeenCalledTimes(2);
+ });
+
+ it('reactive传入style', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+
+ const App = () => {
+ const _rObj = useReactive('blue');
+ rObj = _rObj;
+
+ fn();
+
+ return
;
+ };
+ render( , container);
+ const style = window.getComputedStyle(ref.current);
+ expect(style.color).toEqual('blue');
+
+ rObj.set('red');
+ expect(ref.current.style.color).toEqual('red');
+
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('reactive传入Input value', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+
+ const App = () => {
+ const _rObj = useReactive('blue');
+ rObj = _rObj;
+
+ fn();
+
+ return ;
+ };
+ render( , container);
+ expect(ref.current.value).toEqual('blue');
+
+ rObj.set('red');
+ expect(ref.current.value).toEqual('red');
+
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('reactive传入Textarea value', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+
+ const App = () => {
+ const _rObj = useReactive('blue');
+ rObj = _rObj;
+
+ fn();
+
+ return ;
+ };
+ render( , container);
+ expect(ref.current.value).toEqual('blue');
+
+ rObj.set('red');
+ expect(ref.current.value).toEqual('red');
+
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('reactive父组件刷新, effect不应该重新监听', () => {
+ let rObj, update;
+ const ref = createRef();
+ const fn = jest.fn();
+
+ const App = () => {
+ const [_, setState] = useState({});
+
+ update = () => setState({});
+ return ;
+ };
+
+ const Child = () => {
+ const _rObj = useReactive('blue');
+ rObj = _rObj;
+
+ fn();
+
+ return
;
+ };
+
+ render( , container);
+ expect(ref.current.className).toEqual('blue');
+ expect(fn).toHaveBeenCalledTimes(1);
+ update();
+ expect(fn).toHaveBeenCalledTimes(2);
+ rObj.set('red');
+ expect(fn).toHaveBeenCalledTimes(2);
+ expect(ref.current.className).toEqual('red');
+ });
+
+ it('不允许:从“原生数据”变成“对象”', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive('1');
+ rObj = _rObj;
+
+ fn();
+
+ const cp = useCompute(() => {
+ return _rObj.get() === '1' ? '1' : _rObj.data.get();
+ });
+
+ return {cp}
;
+ };
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('1');
+
+ // 不允许:从“原生数据”变成“对象”
+ expect(() => rObj.set({ data: '2' })).toThrow(Error('Not allowed Change Primitive to Object'));
+ });
+
+ it('允许:一个reactive属性从“原生数据”变成“对象”', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({
+ data: '1',
+ });
+ rObj = _rObj;
+
+ fn();
+
+ const cp = useCompute(() => {
+ return _rObj.data.get() === '1' ? '1' : _rObj.data.num.get();
+ });
+
+ return {cp}
;
+ };
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('1');
+
+ rObj.data.set({ num: '2' });
+ expect(ref.current.innerHTML).toEqual('2');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-props-update.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-props-update.test.js
new file mode 100644
index 00000000..e489877d
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-props-update.test.js
@@ -0,0 +1,96 @@
+/*
+ * 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 Inula, { createRef, render, useReactive } from '../../../src/index';
+
+describe('测试在DOM的props中使用响应式数据', () => {
+ it('在class props中使用响应式数据', () => {
+ let rObj;
+ const ref = createRef();
+
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({ class: 'c1', color: 'blue' });
+ rObj = _rObj;
+
+ fn();
+
+ return (
+
+ {_rObj.color}
+
+ );
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('blue');
+ expect(ref.current.getAttribute('class')).toEqual('c1');
+
+ rObj.class.set('c2');
+ expect(ref.current.getAttribute('class')).toEqual('c2');
+
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('在style中使用响应式数据', () => {
+ let rObj;
+ const ref = createRef();
+
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive({ class: 'c1', color: 'blue' });
+ rObj = _rObj;
+
+ fn();
+
+ return (
+
+ {_rObj.color}
+
+ );
+ };
+
+ render( , container);
+ expect(ref.current.innerHTML).toEqual('blue');
+ expect(ref.current.getAttribute('style')).toEqual('color: blue;');
+
+ rObj.color.set('red');
+ expect(ref.current.getAttribute('style')).toEqual('color: red;');
+
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('在input中使用响应式数据', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+
+ const App = () => {
+ const _rObj = useReactive({ class: 'c1', color: 'blue' });
+ rObj = _rObj;
+
+ fn();
+
+ return ;
+ };
+ render( , container);
+ expect(ref.current.value).toEqual('blue');
+
+ rObj.color.set('red');
+ expect(ref.current.value).toEqual('red');
+
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-watch.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-watch.test.js
new file mode 100644
index 00000000..63e5d257
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-watch.test.js
@@ -0,0 +1,122 @@
+import Inula, {render, createRef, useReactive, useWatch, useCompute, For} from '../../../src/index';
+
+describe('测试 watch', () => {
+ it('watch 一个参数', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive(1);
+ useWatch(() => {
+ _rObj.get();
+ fn();
+ });
+
+ rObj = _rObj;
+
+ return {_rObj}
;
+ };
+
+ render( , container);
+ rObj.set('2');
+ expect(fn).toHaveBeenCalledTimes(2);
+ });
+
+ it('watch 2个参数', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive(1);
+ useWatch(_rObj, () => {
+ fn();
+ });
+ rObj = _rObj;
+
+ return {_rObj}
;
+ };
+
+ render( , container);
+ rObj.set('2');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('watch 2个参数,第一个是函数', () => {
+ let rObj;
+ const ref = createRef();
+ const fn = jest.fn();
+ const App = () => {
+ const _rObj = useReactive(1);
+ useWatch(
+ () => {
+ _rObj.get();
+ },
+ () => {
+ fn();
+ }
+ );
+ rObj = _rObj;
+
+ return {_rObj}
;
+ };
+
+ render( , container);
+ rObj.set('2');
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('响应式数据的孩子变更,watch也应该被触发', () => {
+ let rObj;
+ let ref = createRef();
+ let fn = jest.fn();
+ let appFn = jest.fn();
+ let itemFn = jest.fn();
+
+ const App = () => {
+ const _rObj = useReactive([
+ { id: 'id-1', name: 'p1' },
+ { id: 'id-2', name: 'p2' },
+ { id: 'id-3', name: 'p3' },
+ ]);
+ rObj = _rObj;
+
+ useWatch(() => {
+ _rObj.get();
+ fn();
+ });
+
+ appFn();
+
+ return (
+
+
+ {item => {
+ itemFn();
+ return (
+
+ {item.name}
+
+ );
+ }}
+
+
+ );
+ };
+
+ render( , container);
+ let items = container.querySelectorAll('li');
+ expect(items.length).toEqual(3);
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ rObj.push({ id: 'id-4', name: 'p4' });
+
+ items = container.querySelectorAll('li');
+ expect(items.length).toEqual(4);
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ rObj[1].set({ id: 'id-2', name: 'p222' });
+ let li = container.querySelector('#id-2');
+ expect(li.innerHTML).toEqual('p222');
+ expect(fn).toHaveBeenCalledTimes(3);
+ });
+});
diff --git a/packages/inula/scripts/rollup/rollup.config.js b/packages/inula/scripts/rollup/rollup.config.js
index 489ed37d..4ab91071 100644
--- a/packages/inula/scripts/rollup/rollup.config.js
+++ b/packages/inula/scripts/rollup/rollup.config.js
@@ -23,7 +23,7 @@ import execute from 'rollup-plugin-execute';
import { terser } from 'rollup-plugin-terser';
import { version as inulaVersion } from '../../package.json';
-const extensions = ['.js', '.ts'];
+const extensions = ['.js', '.ts', '.tsx'];
const libDir = path.join(__dirname, '../..');
const rootDir = path.join(__dirname, '../..');
diff --git a/packages/inula/src/dom/DOMOperator.ts b/packages/inula/src/dom/DOMOperator.ts
index ba032254..56eef88f 100644
--- a/packages/inula/src/dom/DOMOperator.ts
+++ b/packages/inula/src/dom/DOMOperator.ts
@@ -188,7 +188,7 @@ export function submitDomUpdate(tag: string, vNode: VNode) {
}
export function clearText(dom: Element): void {
- dom.innerHTML = '';
+ dom.textContent = '';
}
// 添加child元素
diff --git a/packages/inula/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/packages/inula/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts
index 44a7ac8f..7bedfb83 100644
--- a/packages/inula/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts
+++ b/packages/inula/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts
@@ -19,6 +19,14 @@ import { setStyles } from './StyleHandler';
import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding';
import { isEventProp } from '../validators/ValidateProps';
import { getCurrentRoot } from '../../renderer/RootStack';
+import { getValue, isReactiveObj} from '../../reactive/Utils';
+import { handleReactiveProp } from '../../reactive/RContextCreator';
+import { ReactiveProxy } from '../../reactive/types';
+
+export function unwrapVal(propName: string, propVal: any, dom: Element, styleName?: string) {
+ const rawVal: any = handleReactiveProp(dom, propName, propVal, styleName);
+ return rawVal;
+}
// 初始化DOM属性和更新 DOM 属性
export function setDomProps(dom: Element, props: Record, isNativeTag: boolean, isInit: boolean): void {
@@ -43,8 +51,8 @@ export function setDomProps(dom: Element, props: Record, isNativeTa
} else if (propName === 'children') {
// 只处理纯文本子节点,其他children在VNode树中处理
const type = typeof propVal;
- if (type === 'string' || type === 'number') {
- dom.textContent = propVal;
+ if (type === 'string' || type === 'number' || isReactiveObj(propVal)) {
+ dom.textContent = unwrapVal(propName, propVal, dom);
}
} else if (propName === 'dangerouslySetInnerHTML') {
dom.innerHTML = propVal.__html;
@@ -55,9 +63,9 @@ export function setDomProps(dom: Element, props: Record, isNativeTa
}
// 找出两个 DOM 属性的差别,生成需要更新的属性集合
-export function compareProps(oldProps: Record, newProps: Record): Record {
+export function compareProps(oldProps: Record, newProps: Record, dom: Element): Record {
let updatesForStyle = {};
- const toUpdateProps = {};
+ const toUpdateProps: Record = {};
const keysOfOldProps = Object.keys(oldProps);
const keysOfNewProps = Object.keys(newProps);
@@ -103,11 +111,14 @@ export function compareProps(oldProps: Record, newProps: Record, newProps: Record, string>(
+ toUpdateProps,
+ propName,
+ newPropValue,
+ dom,
+ String
+ );
}
} else if (isEventProp(propName)) {
const currentRoot = getCurrentRoot();
if (!allDelegatedInulaEvents.has(propName)) {
- toUpdateProps[propName] = newPropValue;
+ appendToUpdateProps(toUpdateProps, propName, newPropValue, dom);
} else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) {
lazyDelegateOnRoot(currentRoot, propName);
}
} else {
- toUpdateProps[propName] = newPropValue;
+ appendToUpdateProps(toUpdateProps, propName, newPropValue, dom);
}
}
// 处理style
if (Object.keys(updatesForStyle).length > 0) {
- toUpdateProps['style'] = updatesForStyle;
+ appendToUpdateProps(toUpdateProps, 'style', updatesForStyle, dom);
}
return toUpdateProps;
}
+
+function appendToUpdateProps(
+ toUpdateProps: Record,
+ propName: string,
+ propVal: V,
+ dom: Element,
+ formatter?: (value: V) => R
+) {
+ const rawVal: any = handleReactiveProp(dom, propName, propVal);
+
+ toUpdateProps[propName] = formatter ? formatter(rawVal) : rawVal;
+}
diff --git a/packages/inula/src/dom/DOMPropertiesHandler/StyleHandler.ts b/packages/inula/src/dom/DOMPropertiesHandler/StyleHandler.ts
index 46496f2f..ae8bc0fc 100644
--- a/packages/inula/src/dom/DOMPropertiesHandler/StyleHandler.ts
+++ b/packages/inula/src/dom/DOMPropertiesHandler/StyleHandler.ts
@@ -13,6 +13,8 @@
* See the Mulan PSL v2 for more details.
*/
+import { unwrapVal } from './DOMPropertiesHandler';
+
/**
* 不需要加长度单位的 css 属性
*/
@@ -85,12 +87,14 @@ export function setStyles(dom, styles) {
const style = dom.style;
Object.keys(styles).forEach(name => {
const styleVal = styles[name];
+ const val = unwrapVal('style', styleVal, dom, name);
+
// 以--开始的样式直接设置即可
if (name.indexOf('--') === 0) {
- style.setProperty(name, styleVal);
+ style.setProperty(name, val);
} else {
// 使用这种赋值方式,浏览器可以将'WebkitLineClamp', 'backgroundColor'分别识别为'-webkit-line-clamp'和'backgroud-color'
- style[name] = adjustStyleValue(name, styleVal);
+ style[name] = adjustStyleValue(name, val);
}
});
}
diff --git a/packages/inula/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts b/packages/inula/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts
index 1e6c3e23..27cc30e0 100644
--- a/packages/inula/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts
+++ b/packages/inula/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts
@@ -18,6 +18,7 @@ import { isInvalidValue } from '../validators/ValidateProps';
import { getNamespaceCtx } from '../../renderer/ContextSaver';
import { NSS } from '../utils/DomCreator';
import { getDomTag } from '../utils/Common';
+import { unwrapVal } from './DOMPropertiesHandler';
// 不需要装换的svg属性集合
const svgHumpAttr = new Set();
@@ -120,7 +121,7 @@ export function updateCommonProp(dom: Element, attrName: string, value: any, isN
if (value === null) {
dom.removeAttribute(attrName);
} else {
- dom.setAttribute(attrName, String(value));
+ dom.setAttribute(attrName, String(unwrapVal(attrName, value, dom)));
}
} else if (['checked', 'multiple', 'muted', 'selected'].includes(propDetails.attrName)) {
if (value === null) {
@@ -141,7 +142,7 @@ export function updateCommonProp(dom: Element, attrName: string, value: any, isN
// 即可以用作标志又可以是属性值的属性
attributeValue = '';
} else {
- attributeValue = String(value);
+ attributeValue = String(unwrapVal(attrName, value, dom));
}
if (attrNS) {
diff --git a/packages/inula/src/dom/valueHandler/InputValueHandler.ts b/packages/inula/src/dom/valueHandler/InputValueHandler.ts
index 6d4821d3..cc6d6f8e 100644
--- a/packages/inula/src/dom/valueHandler/InputValueHandler.ts
+++ b/packages/inula/src/dom/valueHandler/InputValueHandler.ts
@@ -15,6 +15,8 @@
import { updateCommonProp } from '../DOMPropertiesHandler/UpdateCommonProp';
import { Props } from '../utils/Interface';
+import { getValue } from '../../reactive/Utils';
+import { handleReactiveProp } from '../../reactive/RContextCreator';
function getInitValue(dom: HTMLInputElement, props: Props) {
const { value, defaultValue, checked, defaultChecked } = props;
@@ -45,10 +47,12 @@ export function getInputPropsWithoutValue(dom: HTMLInputElement, props: Props) {
export function updateInputValue(dom: HTMLInputElement, props: Props) {
const { value, checked } = props;
- if (value !== undefined) {
+ const val = getValue(value);
+
+ if (val !== undefined) {
// 处理 dom.value 逻辑
- if (dom.value !== String(value)) {
- dom.value = String(value);
+ if (dom.value !== String(val)) {
+ dom.value = String(val);
}
} else if (checked !== undefined) {
updateCommonProp(dom, 'checked', checked, true);
@@ -62,7 +66,9 @@ export function setInitInputValue(dom: HTMLInputElement, props: Props) {
if (value !== undefined || defaultValue !== undefined) {
// value 的使用优先级 value 属性 > defaultValue 属性 > 空字符串
- const initValueStr = String(initValue);
+ const initValueStr = getValue(initValue);
+
+ handleReactiveProp(dom, 'value', value);
dom.value = initValueStr;
diff --git a/packages/inula/src/dom/valueHandler/TextareaValueHandler.ts b/packages/inula/src/dom/valueHandler/TextareaValueHandler.ts
index 4b7a178a..60015a51 100644
--- a/packages/inula/src/dom/valueHandler/TextareaValueHandler.ts
+++ b/packages/inula/src/dom/valueHandler/TextareaValueHandler.ts
@@ -14,6 +14,8 @@
*/
import { Props } from '../utils/Interface';
+import { getValue } from '../../reactive/Utils';
+import { handleReactiveProp } from '../../reactive/RContextCreator';
// 值的优先级 value > children > defaultValue
function getInitValue(props: Props) {
@@ -48,7 +50,9 @@ export function updateTextareaValue(dom: HTMLTextAreaElement, props: Props, isIn
if (isInit) {
const initValue = getInitValue(props);
if (initValue !== '') {
- dom.value = initValue;
+ dom.value = getValue(initValue);
+
+ handleReactiveProp(dom, 'value', props.value);
}
} else {
// 获取当前节点的 value 值
diff --git a/packages/inula/src/external/JSXElement.ts b/packages/inula/src/external/JSXElement.ts
index 89a7a3c7..31f412cf 100644
--- a/packages/inula/src/external/JSXElement.ts
+++ b/packages/inula/src/external/JSXElement.ts
@@ -18,6 +18,7 @@ import { getProcessingClassVNode } from '../renderer/GlobalVar';
import { Source } from '../renderer/Types';
import { BELONG_CLASS_VNODE_KEY } from '../renderer/vnode/VNode';
import { InulaElement, KVObject } from '../types';
+import { isReactiveObj } from '../reactive/Utils';
/**
* vtype 节点的类型,这里固定是element
@@ -75,7 +76,17 @@ const keyArray = ['key', 'ref', '__source', '__self'];
function buildElement(isClone, type, setting, children) {
// setting中的值优先级最高,clone情况下从 type 中取值,创建情况下直接赋值为 null
- const key = setting && setting.key !== undefined ? String(setting.key) : isClone ? type.key : null;
+ let key;
+ if (setting && setting.key !== undefined) {
+ if (isReactiveObj(setting.key)) {
+ key = setting.key;
+ } else {
+ key = String(setting.key);
+ }
+ } else {
+ key = isClone ? type.key : null;
+ }
+
const ref = setting && setting.ref !== undefined ? setting.ref : isClone ? type.ref : null;
const props = isClone ? { ...type.props } : {};
let vNode = isClone ? type[BELONG_CLASS_VNODE_KEY] : getProcessingClassVNode();
diff --git a/packages/inula/src/index.ts b/packages/inula/src/index.ts
index 116ab1f2..30def4cc 100644
--- a/packages/inula/src/index.ts
+++ b/packages/inula/src/index.ts
@@ -43,6 +43,11 @@ import {
useRef,
useState,
useDebugValue,
+ useAtom,
+ useCompute,
+ useComputed,
+ useReactive,
+ useWatch,
} from './renderer/hooks/HookExternal';
import {
isContextProvider,
@@ -72,6 +77,15 @@ import {
import { syncUpdates as flushSync } from './renderer/TreeBuilder';
import { toRaw } from './inulax/proxy/ProxyHandler';
+import { For } from './reactive/components/For';
+import { Show } from './reactive/components/Show';
+import { Switch } from './reactive/components/Switch';
+import { RText } from './reactive/components/RText';
+import { reactive } from './reactive/Reactive';
+import { computed } from './reactive/Computed';
+import { isReactiveObj } from './reactive/Utils';
+import { watch as watchReactive } from './reactive/Watch';
+
const Inula = {
Children,
createRef,
@@ -122,6 +136,20 @@ const Inula = {
Profiler,
StrictMode,
Suspense,
+ // reactive
+ reactive,
+ computed,
+ watchReactive,
+ isReactiveObj,
+ For,
+ Show,
+ Switch,
+ RText,
+ useAtom,
+ useReactive,
+ useCompute,
+ useComputed,
+ useWatch,
};
export const version = __VERSION__;
@@ -178,6 +206,20 @@ export {
Profiler,
StrictMode,
Suspense,
+ // reactive
+ reactive,
+ computed,
+ watchReactive,
+ isReactiveObj,
+ For,
+ Show,
+ Switch,
+ RText,
+ useAtom,
+ useReactive,
+ useCompute,
+ useComputed,
+ useWatch,
};
export * from './types';
diff --git a/packages/inula/src/reactive/Atom.ts b/packages/inula/src/reactive/Atom.ts
new file mode 100644
index 00000000..b1add441
--- /dev/null
+++ b/packages/inula/src/reactive/Atom.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 { RContextSet, triggerRContexts } from './RContext';
+import { getRNodeVal, trackReactiveData } from './RNode';
+import { isFunction, isObject } from './Utils';
+import {AtomNode, RNode, Root, RootRNode} from './types';
+
+export interface Atom extends AtomNode {
+ root: Root;
+ parent: RNode;
+ parentKey: string | symbol | null;
+ usedRContexts?: RContextSet;
+}
+
+const atomKey = Symbol('atomAccessKey');
+export const atomSymbol = Symbol('ReactiveAtom');
+
+// 对原始数据做响应式的时候使用
+export function Atom(value: T, parent: RNode | null = null, parentKey: string | symbol | null = null) {
+ if (parent === null && parentKey === null) {
+ this.parent = {
+ parent: null,
+ parentKey: null,
+ root: {
+ $: { [atomKey]: value },
+ },
+ type: atomSymbol,
+ } as RootRNode;
+ this.parentKey = atomKey;
+ this.root = this.parent.root;
+ } else {
+ this.parent = parent;
+ this.parentKey = parentKey;
+ this.root = this.parent.root;
+ }
+}
+
+Atom.prototype.get = function (): T {
+ trackReactiveData(this);
+ return this.read();
+};
+
+Atom.prototype.set = function (value: T | ((prev: T) => T)) {
+ // 修改Atom值与父元素值
+ const prevParent = getRNodeVal(this.parent);
+ const prevValue = prevParent[this.parentKey];
+
+ const newValue = isFunction(value) ? value(prevValue) : value;
+
+ // 如果要改为非原始对象切父元素类型不为atomSymbol,说明该对象是Atom对象,不允许:从“原生数据”变成“对象”
+ if (this.parent.type === atomSymbol && isObject(newValue)) {
+ throw Error('Not allowed Change Primitive to Object');
+ }
+
+ // 1) 修改Node底层原始值
+ prevParent[this.parentKey] = newValue;
+
+ // 2) 触发使用到它的RContexts
+ triggerRContexts(this, prevValue, newValue, false);
+ return this;
+};
+
+Atom.prototype.read = function (): T {
+ return getRNodeVal(this);
+};
diff --git a/packages/inula/src/reactive/Batch.ts b/packages/inula/src/reactive/Batch.ts
new file mode 100644
index 00000000..750851a8
--- /dev/null
+++ b/packages/inula/src/reactive/Batch.ts
@@ -0,0 +1,92 @@
+/*
+ * 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 { RContextCallback, RContextParam, Reactive } from './types';
+import { VNode } from '../renderer/Types';
+import { updateShouldUpdateOfTree } from '../renderer/vnode/VNodeShouldUpdate';
+
+export interface BatchItem {
+ callback: RContextCallback;
+ params: RContextParam;
+ reactive: Reactive;
+}
+
+let batchCount = 0;
+let _batch: BatchItem[] = [];
+let _batchMap = new Map();
+
+export function addToBatch(item: BatchItem) {
+ if (batchCount > 0) {
+ const existing = _batchMap.get(item.callback);
+
+ if (existing) {
+ // const params = existing.params;
+ // params.value = item.params.value;
+ } else {
+ _batch.push(item);
+ _batchMap.set(item.callback, true);
+ }
+ } else {
+ item.callback(item.params, item.reactive);
+ }
+}
+
+export function startBatch() {
+ batchCount++;
+}
+
+export function endBatch() {
+ batchCount--;
+
+ if (batchCount <= 0) {
+ batchCount = 0;
+ const batch = _batch;
+ _batch = [];
+ _batchMap = new Map();
+
+ const toUpdateVNodes: VNode[] = [];
+ const toUpdateVNodeItems: BatchItem[] = [];
+
+ for (let i = 0; i < batch.length; i++) {
+ const b = batch[i];
+
+ // 如果要刷新的是组件(函数组件或类组件)
+ if (b.params?.vNode) {
+ // 设置vNode为shouldUpdate
+ updateShouldUpdateOfTree(b.params.vNode);
+
+ b.params.vNode.isStoreChange = true;
+
+ toUpdateVNodes.push(b.params.vNode);
+ toUpdateVNodeItems.push(b);
+ }
+ }
+
+ for (let i = 0; i < batch.length; i++) {
+ const b = batch[i];
+ const { callback, reactive } = b;
+
+ if (!b.params?.vNode) {
+ callback(b.params, reactive);
+ }
+ }
+
+ if (toUpdateVNodes.length) {
+ const b = toUpdateVNodeItems[0];
+ const { callback, reactive } = b;
+ callback(b.params, reactive);
+ }
+ }
+}
diff --git a/packages/inula/src/reactive/Computed.ts b/packages/inula/src/reactive/Computed.ts
new file mode 100644
index 00000000..8c2fdfcb
--- /dev/null
+++ b/packages/inula/src/reactive/Computed.ts
@@ -0,0 +1,55 @@
+/*
+ * 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 { Computed } from './types';
+import { createReactiveObj, setRNodeVal } from './RNode';
+import { calculateReactive, getRNode, isPromise } from './Utils';
+import { RContext } from './RContext';
+
+function computed(fn: () => T | Promise): Computed {
+ const setComputed = (value: any, trigger: boolean) => {
+ root.readOnly = false;
+ const raw = getRNode(rObj);
+ setRNodeVal(raw, value, trigger);
+ root.readOnly = true;
+ };
+
+ // 依赖的响应式数据变化时调用
+ const update = (trigger: boolean) => {
+ const value = calculateReactive(fn);
+ if (isPromise(value)) {
+ value.then(val => setComputed(val, trigger));
+ } else {
+ setComputed(value, trigger);
+ }
+ };
+
+ const rContext = new RContext(() => update(true));
+ const end = rContext.start();
+ // 首次更新不触发usedRContexts
+ const value = calculateReactive(fn);
+ end();
+
+ const rObj = createReactiveObj(value);
+
+ const rawNode = getRNode(rObj);
+ const root = rawNode.root;
+ // 默认readOnly为true
+ root.readOnly = true;
+
+ return rObj as Computed;
+}
+
+export { computed };
diff --git a/packages/inula/src/reactive/DiffUtils.ts b/packages/inula/src/reactive/DiffUtils.ts
new file mode 100644
index 00000000..a3946996
--- /dev/null
+++ b/packages/inula/src/reactive/DiffUtils.ts
@@ -0,0 +1,199 @@
+/*
+ * 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 enum Operation {
+ // 数组长度不同
+ Nop = 0,
+ Insert = 1,
+ Delete = 2,
+ // 数组长度相同
+ Update = 3,
+ Exchange = 4,
+}
+
+export interface Diff {
+ action: Operation;
+ index: number;
+}
+
+export interface DiffOperator {
+ isOnlyNop: boolean;
+ opts: Diff[];
+}
+
+function longestCommonPrefix(arr1: T[], arr2: T[]): number {
+ if (!arr1.length || !arr2.length || arr1[0] !== arr1[0]) {
+ return 0;
+ }
+
+ let low = 0;
+ let start = 0;
+ // 最短数组的长度
+ let high = Math.min(arr1.length, arr2.length);
+ while (low <= high) {
+ const mid = (high + low) >> 1;
+ if (isArrayEqual(arr1, arr2, start, mid)) {
+ low = mid + 1;
+ start = mid;
+ } else {
+ high = mid - 1;
+ }
+ }
+ return low - 1;
+}
+
+function isArrayEqual(str1: T[], str2: T[], start: number, end: number): boolean {
+ for (let j = start; j < end; j++) {
+ if (str1[j] !== str2[j]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * @param origin 原始数组
+ * @param target 目标数组
+ * @returns 返回一个 Diff 数组,表示从原始数组转换为目标数组需要进行的操作
+ */
+export function arrayDiff(origin: T[], target: T[]): DiffOperator {
+ // 使用二分查找计算共同前缀与后缀
+ const prefixLen = longestCommonPrefix(origin, target);
+ const suffixLen = longestCommonPrefix([...origin].reverse(), [...target].reverse());
+ // 删除原数组与目标数组的共同前缀与后缀
+ const optimizedOrigin = origin.slice(prefixLen, origin.length - suffixLen);
+ const optimizedTarget = target.slice(prefixLen, target.length - suffixLen);
+
+ const originLen = optimizedOrigin.length;
+ const targetLen = optimizedTarget.length;
+
+ const dp: number[][] = Array.from(Array(originLen + 1), () => {
+ return Array(targetLen + 1).fill(0);
+ });
+ const pathMatrix: Operation[][] = Array.from(Array(originLen + 1), () => {
+ return Array(targetLen + 1).fill('');
+ });
+
+ let diffs: Diff[] = [];
+
+ // 计算最长公共子序列
+ for (let i = 1; i < originLen + 1; i++) {
+ for (let j = 1; j < targetLen + 1; j++) {
+ if (optimizedOrigin[i - 1] === optimizedTarget[j - 1]) {
+ dp[i][j] = dp[i - 1][j - 1] + 1;
+ // 如果相等,则表示不需要进行任何操作
+ pathMatrix[i][j] = Operation.Nop;
+ } else if (dp[i - 1][j] > dp[i][j - 1]) {
+ dp[i][j] = dp[i - 1][j];
+ // 如果不相等,则需要进行删除操作
+ pathMatrix[i][j] = Operation.Delete;
+ } else {
+ dp[i][j] = dp[i][j - 1];
+ // 如果不相等,则需要进行插入操作
+ pathMatrix[i][j] = Operation.Insert;
+ }
+ }
+ }
+
+ let hasDelete = false;
+ let hasInsert = false;
+
+ // 计算操作序列
+ function diff(oLen: number, tLen: number) {
+ const stack: Record[] = [{ i: oLen, j: tLen }];
+
+ while (stack.length > 0) {
+ const obj = stack.pop();
+ const { i, j } = obj!;
+ if (i === 0 || j === 0) {
+ if (i !== 0) {
+ diffs.unshift(
+ ...optimizedOrigin.slice(0, i).map((item, idx) => ({
+ action: Operation.Delete,
+ index: idx,
+ }))
+ );
+ hasDelete = true;
+ }
+ if (j !== 0) {
+ diffs.unshift(
+ ...optimizedTarget.slice(0, j).map((item, idx) => ({
+ action: Operation.Insert,
+ index: idx,
+ }))
+ );
+ hasInsert = true;
+ }
+ }
+
+ if (pathMatrix[i][j] === Operation.Nop) {
+ stack.push({ i: i - 1, j: j - 1 });
+ // 如果不需要进行任何操作,则表示是公共元素,将其添加到 diffs 中
+ diffs.unshift({ action: Operation.Nop, index: i - 1 });
+ } else if (pathMatrix[i][j] === Operation.Delete) {
+ stack.push({ i: i - 1, j });
+ // 如果需要进行删除操作,则将其添加到 diffs 中
+ diffs.unshift({ action: Operation.Delete, index: i - 1 });
+ hasDelete = true;
+ } else if (pathMatrix[i][j] === Operation.Insert) {
+ stack.push({ i, j: j - 1 });
+ // 如果需要进行插入操作,则将其添加到 diffs 中
+ diffs.unshift({ action: Operation.Insert, index: j - 1 });
+ hasInsert = true;
+ }
+ }
+ }
+
+ // 计算操作序列
+ diff(originLen, targetLen);
+
+ diffs.map(i => (i.index += prefixLen));
+
+ const prefixOpts = Array.from(Array(prefixLen), (_, index) => index).map(idx => ({
+ action: Operation.Nop,
+ index: idx,
+ }));
+
+ const suffixOpts = Array.from(Array(suffixLen), (_, index) => index).map(idx => ({
+ action: Operation.Nop,
+ index: origin.length - suffixLen + idx,
+ }));
+
+ diffs = prefixOpts.concat(diffs, suffixOpts);
+
+ return {
+ isOnlyNop: !hasDelete && !hasInsert,
+ opts: diffs,
+ };
+}
+
+// export function sameLenArrayDiff(origin: T[], target: T[]): Diff[] {
+// // 只要id相同就认为是同一个对象
+// const diff: Diff[] = [];
+// const hasId = origin.every(i => getIDField(i));
+// const arrayLength = origin.length;
+// if (hasId && arrayLength > 0) {
+// const idField = getIDField(origin[0])!;
+// for (let j = 0; j < target.length; j++) {
+// if (origin[j][idField] === target[j][idField]) {
+// diff.push({ action: Operation.Nop, index: j });
+// } else {
+// diff.push({ action: Operation.Update, index: j });
+// }
+// }
+// return diff;
+// }
+// return arrayDiff(origin, target);
+// }
diff --git a/packages/inula/src/reactive/RContext.ts b/packages/inula/src/reactive/RContext.ts
new file mode 100644
index 00000000..0a058f98
--- /dev/null
+++ b/packages/inula/src/reactive/RContext.ts
@@ -0,0 +1,355 @@
+/*
+ * 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 { isArray } from '../inulax/CommonUtils';
+import { isObject, isPrimitive } from './Utils';
+import { arrayDiff, DiffOperator, Operation } from './DiffUtils';
+import { ArrayState, RContextCallback, RContextParam, Reactive, RNode } from './types';
+import { getOrCreateChildNode } from './RNode';
+import {addToBatch, startBatch, endBatch, BatchItem} from './Batch';
+
+export let currentDependent: RContext | null = null;
+
+const reactiveContextStack: RContext[] = [];
+export type RContextSet = Set;
+
+/**
+ * 响应式上下文,有4种情况:1、组件级。2、Block级。3、watch/computed。4、DOM级。
+ */
+export class RContext {
+ callback: RContextCallback;
+
+ // 记录该Context使用时的参数
+ params: RContextParam;
+
+ // 记录该RContext中使用到的Reactive中的RContextSet
+ reactiveDependents: Set | null = null;
+
+ constructor(callback: RContextCallback, params: RContextParam = {}) {
+ this.callback = callback;
+ this.params = params;
+ }
+
+ setParam(params: RContextParam) {
+ this.params = params;
+ }
+
+ start() {
+ cleanupRContext(this);
+ currentDependent = this;
+ reactiveContextStack.push(this);
+
+ return endEffect;
+ }
+}
+
+function endEffect() {
+ reactiveContextStack.pop();
+ currentDependent = reactiveContextStack[reactiveContextStack.length - 1] ?? null;
+}
+
+// 清除 RContext和响应式数据的绑定,双向清除
+export function cleanupRContext(rContext: RContext) {
+ if (rContext.reactiveDependents !== null) {
+ for (const usedRContexts of rContext.reactiveDependents) {
+ usedRContexts.delete(rContext);
+ }
+
+ rContext.reactiveDependents.clear();
+ rContext.reactiveDependents = null;
+ }
+}
+
+// 绑定RContext和响应式数据,双向绑定
+export function bindReactiveWithContext(reactive: Reactive, rContext: RContext) {
+ if (reactive.usedRContexts === undefined) {
+ reactive.usedRContexts = new Set();
+ }
+ reactive.usedRContexts.add(rContext);
+
+ if (rContext.reactiveDependents === null) {
+ rContext.reactiveDependents = new Set();
+ }
+ rContext.reactiveDependents.add(reactive.usedRContexts);
+}
+
+// 递归触发依赖这reactive数据的所有RContext
+export function triggerRContexts(reactive: Reactive, prevValue: any, value: any, isFromArrModify?: boolean) {
+ const isObj = isObject(value);
+ const isPrevObj = isObject(prevValue);
+
+ startBatch();
+
+ if (isObj && isPrevObj) {
+ triggerChildrenContexts(reactive as RNode, value, prevValue, isFromArrModify);
+ }
+
+ callRContexts(reactive);
+
+ // 触发父数据的RContext,不希望触发组件刷新(只触发computed和watch)
+ triggerParents(reactive.parent);
+
+ endBatch();
+}
+
+function triggerParents(reactive: Reactive | null) {
+ if (reactive) {
+ // 在触发父数据的时候,不希望触发组件刷新(只触发computed和watch)
+ callRContexts(reactive, true);
+
+ triggerParents(reactive.parent);
+ }
+}
+
+// 当value和prevValue都是对象或数组时,才触发
+function triggerChildrenContexts(rNode: RNode, value: any, prevValue: any, isFromArrModify?: boolean): boolean {
+ // 可以精准更新
+ let canPreciseUpdate = true;
+
+ const isArr = isArray(value);
+ const isPrevArr = isArray(prevValue);
+
+ // 1、变化来自数组的Modify方法(某些行可能完全不变)
+ if (isFromArrModify) {
+ // 获取数组间差异,RNode只能增删不能修改,修改会导致Effect不会随数据的位置变化
+ const diffOperator = arrayDiff(prevValue, value);
+ const states: ArrayState[] = [];
+
+ let childIndex = 0;
+
+ for (const opt of diffOperator.opts) {
+ switch (opt.action) {
+ // 从已有RNode中取值
+ case Operation.Nop: {
+ const childRNode = rNode.children?.get(String(opt.index));
+
+ // children没有使用时,可以为undefined或没有该child
+ if (childRNode !== undefined) {
+ childRNode.parentKey = String(childIndex);
+ states.push(ArrayState.Fresh);
+ childIndex++;
+
+ // 删除旧的,重设新值。处理场景:元素还在,但是在数组中的位置变化了。
+ rNode.children?.delete(String(opt.index));
+ rNode.children?.set(childRNode.parentKey, childRNode);
+ }
+ break;
+ }
+ // 从Value中新建RNode
+ case Operation.Insert: {
+ getOrCreateChildNode(value[opt.index], rNode, String(opt.index));
+ states.push(ArrayState.NotFresh);
+ childIndex++;
+ break;
+ }
+ case Operation.Delete: {
+ rNode.children?.delete(String(opt.index));
+ break;
+ }
+ }
+ }
+
+ rNode.diffOperator = diffOperator;
+ if (!rNode.diffOperators) {
+ rNode.diffOperators = [];
+ }
+ rNode.diffOperators.push(diffOperator);
+ // 记录:新数据,哪些需要处理,哪些不需要
+ rNode.states = states;
+ // 数组长度不同,确定会产生变化,调用callDependents一次
+ callRContexts(rNode);
+
+ return canPreciseUpdate;
+ }
+
+ // 2、都是数组
+ if (isArr && isPrevArr) {
+ const minLen = Math.min(value.length, prevValue.length);
+
+ // 遍历数组或对象,触发子数据的Effects
+ const canPreciseUpdates = updateSameLengthArray(rNode, value, prevValue, minLen);
+
+ const maxLen = Math.max(value.length, prevValue.length);
+ if (maxLen !== minLen || canPreciseUpdates.includes(false)) {
+ canPreciseUpdate = false;
+ }
+
+ // 在reactive中保存opts
+ const diffOperator: DiffOperator = {
+ isOnlyNop: false,
+ opts: [],
+ };
+ const states: ArrayState[] = [];
+
+ // 相同长度的部分
+ for (let i = 0; i < minLen; i++) {
+ diffOperator.opts.push({ action: Operation.Nop, index: i });
+ // 如果该行数据无法精准更新,设置为NotFresh
+ states.push(canPreciseUpdates[i] ? ArrayState.Fresh : ArrayState.NotFresh);
+ }
+
+ // 超出部分:新增
+ if (value.length > prevValue.length) {
+ for (let i = minLen; i < maxLen; i++) {
+ diffOperator.opts.push({ action: Operation.Insert, index: i });
+ states.push(ArrayState.NotFresh);
+ getOrCreateChildNode(value[i], rNode, String(i));
+ }
+ } else if (value.length < prevValue.length) { // 减少部分:删除
+ for (let i = minLen; i < maxLen; i++) {
+ diffOperator.opts.push({ action: Operation.Delete, index: i });
+ states.push(ArrayState.NotFresh);
+ }
+ }
+
+ diffOperator.isOnlyNop = !states.includes(ArrayState.NotFresh);
+ rNode.diffOperator = diffOperator;
+ rNode.states = states;
+
+ return canPreciseUpdate;
+ }
+
+ // 都是对象
+ if (!isArr && !isPrevArr) {
+ const keys = Object.keys(value);
+ const prevKeys = Object.keys(prevValue);
+
+ // 合并keys和prevKeys
+ const keySet = new Set(keys.concat(prevKeys));
+
+ keySet.forEach((key) => {
+ const val = value[key];
+ const prevVal = prevValue[key];
+ const isChanged = val !== prevVal;
+
+ // 如果数据有变化,就触发Effects
+ if (isChanged) {
+ const childRNode = rNode.children?.get(key);
+
+ const isObj = isObject(val);
+ const isPrevObj = isObject(prevVal);
+ // val和prevVal都是对象或数组
+ if (isObj) {
+ // 1、如果上一个属性无法精准更新,就不再递归下一个属性了
+ // 2、如果childRNode为空,说明这个数据未被引用过,也不需要调用RContexts
+ if (canPreciseUpdate && childRNode !== undefined) {
+ canPreciseUpdate = triggerChildrenContexts(childRNode as RNode, val, prevVal);
+ }
+ } else if (!isObj && !isPrevObj) { // val和prevVal都不是对象或数组
+ canPreciseUpdate = true;
+ } else { // 类型不同(一个是对象或数组,另外一个不是)
+ canPreciseUpdate = false;
+ }
+
+ // 有childRNode,说明这个数据被使引用过
+ if (childRNode !== undefined) {
+ callRContexts(childRNode);
+ }
+ }
+ });
+
+ return canPreciseUpdate;
+ }
+
+ // 一个是对象,一个是数组
+ canPreciseUpdate = false;
+
+ return canPreciseUpdate;
+}
+
+// 对于数组的变更,尽量尝试精准更新,会记录每行数据是否能够精准更新
+function updateSameLengthArray(rNode: RNode, value: any, prevValue: any, len: number): boolean[] {
+ const canPreciseUpdates: boolean[] = [];
+
+ // 遍历数组或对象,触发子数据的RContexts
+ for (let i = 0; i < len; i++) {
+ const val = value[i];
+ const prevVal = prevValue[i];
+ const isChanged = val !== prevVal;
+
+ // 如果数据有变化,就触发RContexts
+ if (isChanged) {
+ const childRNode = rNode.children?.get(String(i));
+
+ const isObj = isObject(val);
+ const isPrevObj = isObject(prevVal);
+ // val和prevVal都是对象或数组时
+ if (isObj && isPrevObj) {
+ // 如果childRNode为空,说明这个数据未被引用过,也不需要调用RContexts
+ if (childRNode !== undefined) {
+ canPreciseUpdates[i] = triggerChildrenContexts(childRNode as RNode, val, prevVal);
+ }
+ } else if (!isObj && !isPrevObj) { // val和prevVal都不是对象或数组
+ canPreciseUpdates[i] = true;
+ } else { // 类型不同(一个是对象或数组,另外一个不是)
+ canPreciseUpdates[i] = false;
+ }
+
+ // 有childRNode,说明这个数据被使引用过
+ if (childRNode) {
+ callRContexts(childRNode);
+ }
+ } else {
+ canPreciseUpdates[i] = true;
+ }
+ }
+
+ return canPreciseUpdates;
+}
+
+// 调用:响应式数据reactive所收集的依赖(RContext)
+function callRContexts(reactive: Reactive, isNoComponentRContext = false) {
+ if (reactive.usedRContexts !== undefined && reactive.usedRContexts.size) {
+ // Array.from 浅克隆防止callback中扩缩usedRContexts数组
+ const usedRContexts = Array.from(reactive.usedRContexts);
+ const len = usedRContexts.length;
+ let rContext: RContext;
+
+ for (let i = 0; i < len; i++) {
+ rContext = usedRContexts[i];
+
+ // dep.callback可能被清除
+ if (rContext.callback) {
+ // 在触发父数据的时候,不希望触发组件刷新
+ if (isNoComponentRContext && rContext.params.vNode) {
+ continue;
+ }
+
+ const batchItem: BatchItem = {
+ callback: rContext.callback,
+ params: rContext.params,
+ reactive: reactive,
+ };
+
+ addToBatch(batchItem);
+ }
+ }
+ }
+}
+
+/**
+ * 清理Reactive的dependents
+ * @param reactive
+ */
+export function disposeReactive(reactive: Reactive) {
+ if (reactive.usedRContexts) {
+ const usedRContexts = reactive.usedRContexts;
+ for (const rContext of usedRContexts) {
+ cleanupRContext(rContext);
+ }
+ delete reactive.usedRContexts;
+ }
+}
+
diff --git a/packages/inula/src/reactive/RContextCreator.ts b/packages/inula/src/reactive/RContextCreator.ts
new file mode 100644
index 00000000..7513bbd4
--- /dev/null
+++ b/packages/inula/src/reactive/RContextCreator.ts
@@ -0,0 +1,202 @@
+/*
+ * 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 { launchUpdateFromVNode } from '../renderer/TreeBuilder';
+import { bindReactiveWithContext, cleanupRContext, RContext } from './RContext';
+import { For } from './components/For';
+import { VNode } from '../renderer/Types';
+import { RContextParam, Reactive, RNode } from './types';
+import { getRNodeVal } from './RNode';
+import { getVNode } from '../dom/DOMInternalKeys';
+import { updateInputValue } from '../dom/valueHandler/InputValueHandler';
+import { updateTextareaValue } from '../dom/valueHandler/TextareaValueHandler';
+import { setDomProps } from '../dom/DOMPropertiesHandler/DOMPropertiesHandler';
+import { getRNodeFromProxy, isAtom, isReactiveProxy } from './Utils';
+
+const vNodeEffectMap = new WeakMap();
+
+/**
+ * 创建组件(函数组件或Class组件)级别Dependent
+ * @param renderFn 函数组件 或 Class的render
+ * @param vNode
+ */
+export function createComponentDependent(renderFn: () => T, vNode: VNode): T {
+ let compRContext = vNode.compRContext;
+
+ if (!compRContext) {
+ compRContext = new RContext(
+ (params, reactive) => {
+ vNode.isStoreChange = true;
+
+ // 如果是For组件
+ if (For === vNode.type && reactive.diffOperator) {
+ // 如果Reactive的For组件
+ const { isOnlyNop } = reactive.diffOperator;
+
+ // 如果没有需要处理
+ if (isOnlyNop) {
+ return;
+ }
+ }
+
+ // 同步刷新
+ // syncUpdates(() =>{
+ // // 触发vNode更新
+ // launchUpdateFromVNode(vNode);
+ // });
+
+ // 触发vNode更新
+ launchUpdateFromVNode(vNode);
+ },
+ { vNode }
+ );
+
+ vNode.compRContext = compRContext;
+ }
+
+ const endRContext = compRContext.start();
+
+ const result = renderFn();
+
+ endRContext();
+
+ return result;
+}
+
+/**
+ * 订阅DOM的属性(props或children),创建一个专门更新该属性的上下文,当响应式数据变化就触发该上下文的callback
+ * @param dom DOM元素
+ * @param propName 属性名字
+ * @param propVal 属性值,是响应式数据
+ * @param styleName style里面的某个属性
+ */
+function subscribeAttr(dom: Element, propName: string, propVal: Reactive, styleName?: string) {
+ const attrRContext = new RContext(
+ params => {
+ let changeList;
+ if (propName === 'style' && styleName) {
+ changeList = {
+ style: {
+ [styleName]: getRNodeVal(params.reactive!),
+ },
+ };
+ } else {
+ changeList = {
+ [propName]: getRNodeVal(params.reactive!),
+ };
+ }
+
+ const type = getVNode(dom)?.type;
+ if (type === 'input' && propName === 'value') {
+ updateInputValue(dom as HTMLInputElement, changeList);
+ } else if (type === 'textarea' && propName === 'value') {
+ updateTextareaValue(dom as HTMLTextAreaElement, changeList);
+ } else {
+ setDomProps(dom, changeList, true, false);
+ }
+ },
+ { reactive: propVal }
+ );
+
+ bindReactiveWithContext(propVal, attrRContext);
+
+ // vNode保存RContext,用于cleanup
+ const vNode = getVNode(dom);
+ saveAttrRContexts(vNode, attrRContext);
+}
+
+export function handleReactiveProp(dom: Element, propName: string, propVal: any, styleName?: string): any {
+ let rawVal = propVal;
+ const isA = isAtom(propVal);
+ const isProxy = isReactiveProxy(propVal);
+
+ if (isA || isProxy) {
+ let reactive = propVal;
+ if (isProxy) {
+ reactive = getRNodeFromProxy(propVal);
+ }
+ rawVal = getRNodeVal(reactive as Reactive);
+ subscribeAttr(dom, propName, reactive, styleName);
+ }
+
+ return rawVal;
+}
+
+/**
+ * 创建DOM Key Dependent
+ * @param callback 用于修改VNode的key
+ * @param params Effect调用所需要的参数
+ */
+export function createKeyDependent(callback: (params: RContextParam) => void, params?: RContextParam) {
+ return new RContext(callback, params);
+}
+
+/**
+ * 处理 Count: {_rObj}
+ * @param textDom 用于修改VNode的key
+ * @param rText Effect调用所需要的参数
+ */
+export function subscribeReactiveComponent(textDom: Element, rText: Reactive) {
+ const textContext = new RContext(
+ params => {
+ textDom.textContent = getRNodeVal(params.reactive as Reactive);
+ },
+ { reactive: rText }
+ );
+
+ bindReactiveWithContext(rText, textContext);
+
+ // vNode保存RContext,用于cleanup
+ const vNode = getVNode(textDom);
+ saveAttrRContexts(vNode, textContext);
+}
+
+// TODO 删除
+export function cleanupVNodeEffect(vNode: VNode) {
+ const effect = vNodeEffectMap.get(vNode);
+ if (effect) {
+ cleanupRContext(effect);
+ vNodeEffectMap.delete(vNode);
+ }
+}
+
+/**
+ * 创建DOM Key Dependent
+ * @param vNode
+ * @param rKey
+ */
+export function subscribeKeyEffect(vNode: VNode, rKey: RNode) {
+ const keyContext = new RContext(
+ params => {
+ vNode.key = getRNodeVal(rKey);
+ },
+ { reactive: rKey }
+ );
+
+ bindReactiveWithContext(rKey, keyContext);
+
+ // vNode保存RContext,用于cleanup
+ saveAttrRContexts(vNode, keyContext);
+}
+
+// vNode保存RContext,用于cleanup
+function saveAttrRContexts(vNode: VNode | null, rContext: RContext) {
+ if (vNode) {
+ if (!vNode.attrRContexts) {
+ vNode.attrRContexts = new Set();
+ }
+ vNode.attrRContexts.add(rContext);
+ }
+}
diff --git a/packages/inula/src/reactive/RNode.ts b/packages/inula/src/reactive/RNode.ts
new file mode 100644
index 00000000..fe0a20f0
--- /dev/null
+++ b/packages/inula/src/reactive/RNode.ts
@@ -0,0 +1,156 @@
+/*
+ * 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 { bindReactiveWithContext, currentDependent, triggerRContexts } from './RContext';
+import { isAtom, isFunction, isRNode, isPrimitive } from './Utils';
+import { createProxy } from './proxy/RProxyHandler';
+import { Atom } from './Atom';
+import { isSame } from '../renderer/utils/compare';
+import { ProxyRNode, Reactive, ReactiveProxy, RNode, RootRNode } from './types';
+
+export const nodeSymbol = Symbol('ReactiveNode');
+
+export function createReactiveObj(raw?: T): ReactiveProxy {
+ if (isPrimitive(raw) || raw === null || raw === undefined) {
+ return new Atom(raw);
+ } else {
+ const node = createRootRNode(raw);
+ const proxyObj = createProxy(node);
+ node.proxy = proxyObj;
+ return proxyObj as ReactiveProxy;
+ }
+}
+
+export function createRootRNode(raw?: T): RootRNode {
+ const root: RootRNode = {
+ type: nodeSymbol,
+ root: { $: raw },
+ parent: null,
+ parentKey: null,
+ };
+
+ return root;
+}
+
+export function getOrCreateChildRNode(node: RNode, key: string | symbol): RNode {
+ let child = node.children?.get(key);
+
+ if (!child || isAtom(child)) {
+ child = {
+ type: nodeSymbol,
+ root: node.root,
+ parent: node,
+ parentKey: key,
+ };
+ if (!node.children) {
+ node.children = new Map();
+ }
+
+ node.children.set(key, child);
+ }
+ return child;
+}
+
+export function getOrCreateChildAtom(node: RNode, key: string | symbol, value: any): Atom {
+ const child = node.children?.get(key);
+
+ // 如果没有子节点或者子节点是RNode类型,就创建一个新的Atom,否则返回已存在的子节点
+ // 注意:不能使用getNodeVal来判断之前节点的值,因为根数据已被修改,只能通过判断child的类型为
+ if (!child || isRNode(child)) {
+ const atom = new Atom(value, node, key);
+ if (!node.children) {
+ node.children = new Map();
+ }
+ node.children.set(key, atom);
+ return atom;
+ }
+
+ return child;
+}
+
+export function getOrCreateChildNode(value: unknown, parent: RNode, key: string | symbol): Atom | RNode {
+ let child: Atom | RNode;
+ // if (isPrimitive(value) || value === null || value === undefined) {
+ // child = getOrCreateChildAtom(parent, key, value);
+ // } else {
+ child = getOrCreateChildRNode(parent, key);
+ // }
+ return child;
+}
+
+export function getOrCreateChildProxy(value: unknown, parent: RNode, key: string | symbol): Atom | ProxyRNode {
+ let child: Atom | RNode;
+ // if (isPrimitive(value) || value === null || value === undefined) {
+ // child = getOrCreateChildAtom(parent, key, value);
+ // return child;
+ // } else {
+ child = getOrCreateChildRNode(parent, key);
+ if (!child.proxy) {
+ child.proxy = createProxy(child);
+ }
+ return child.proxy;
+ // }
+}
+
+// 最终响应式数据的使用
+export function trackReactiveData(reactive: Reactive) {
+ if (currentDependent !== null) {
+ bindReactiveWithContext(reactive, currentDependent);
+ }
+}
+
+export function getRNodeVal(node: Reactive): any {
+ let currentNode = node;
+ const keys: (string | symbol)[] = [];
+ while (currentNode.parentKey !== null && currentNode.parent !== null) {
+ keys.push(currentNode.parentKey);
+ currentNode = currentNode.parent;
+ }
+
+ let rawObj = node.root.$;
+ for (let i = keys.length - 1; i >= 0; i--) {
+ if (keys[i] !== undefined && rawObj) {
+ rawObj = rawObj[keys[i]];
+ }
+ }
+
+ return rawObj;
+}
+
+export function setRNodeVal(rNode: RNode, value: unknown, trigger = false, isArrayModified = false): void {
+ if (rNode.root.readOnly) {
+ return;
+ }
+
+ const { parent, parentKey } = rNode;
+ const isRoot = parent === null;
+ let prevValue: unknown;
+ let newValue: unknown;
+
+ if (isRoot) {
+ prevValue = rNode.root.$;
+ newValue = isFunction<(...prev: any) => any>(value) ? value(prevValue) : value;
+ rNode.root.$ = newValue;
+ } else {
+ const parentVal = getRNodeVal(parent!);
+ prevValue = parentVal[parentKey!];
+ newValue = isFunction<(...prev: any) => any>(value) ? value(prevValue) : value;
+ parentVal[parentKey!] = newValue;
+ }
+
+ if (trigger && !isSame(newValue, prevValue)) {
+ triggerRContexts(rNode, prevValue, newValue, isArrayModified);
+ }
+}
diff --git a/packages/inula/src/reactive/Reactive.ts b/packages/inula/src/reactive/Reactive.ts
new file mode 100644
index 00000000..6c558004
--- /dev/null
+++ b/packages/inula/src/reactive/Reactive.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 { createReactiveObj } from './RNode';
+import { ReactiveProxy } from './types';
+
+export function reactive(obj: T): ReactiveProxy {
+ return createReactiveObj(obj);
+}
diff --git a/packages/inula/src/reactive/Utils.ts b/packages/inula/src/reactive/Utils.ts
new file mode 100644
index 00000000..f2fd2057
--- /dev/null
+++ b/packages/inula/src/reactive/Utils.ts
@@ -0,0 +1,89 @@
+/*
+ * 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 {Atom} from './Atom';
+import {getRNodeVal, nodeSymbol} from './RNode';
+import {AtomNode, AtomNodeFn, ProxyRNode, ProxyRNodeFn, Reactive, ReactiveProxy, RNode} from './types';
+import {GET_R_NODE} from './proxy/RProxyHandler';
+
+export function isAtom(val: unknown): val is Atom {
+ return val instanceof Atom;
+}
+
+export function isRNode(val: unknown): val is RNode {
+ return typeof val === 'object' && val != null && val['type'] === nodeSymbol;
+}
+
+export function isReactiveProxy(val: unknown): val is RNode {
+ return typeof val === 'object' && val != null && val[GET_R_NODE] !== undefined;
+}
+
+export function isReactiveObj(val: unknown): val is ProxyRNodeFn | AtomNodeFn {
+ return isAtom(val) || isReactiveProxy(val);
+}
+
+export function isObject(obj: unknown): boolean {
+ const type = typeof obj;
+ return obj != null && (type === 'object' || type === 'function');
+}
+
+export function isPrimitive(obj: unknown): boolean {
+ const type = typeof obj;
+ return obj != null && type !== 'object' && type !== 'function';
+}
+
+export function isFunction any>(obj: unknown): obj is T {
+ return typeof obj === 'function';
+}
+
+/**
+ * 如果是函数就执行,如果是reactive就调用get()
+ * @param val 值/reactive对象/函数
+ * @return 返回真实值
+ */
+export function calculateReactive(val: any | (() => any)): any {
+ let ret = val;
+ if (typeof val === 'function') {
+ ret = val();
+ }
+
+ if (isReactiveObj(ret)) {
+ ret = ret.get();
+ }
+
+ return ret;
+}
+
+export function getRNode(rObj: ProxyRNode | AtomNode): RNode {
+ return isReactiveProxy(rObj) ? rObj[GET_R_NODE] : rObj;
+}
+
+export function getRNodeFromProxy(rObj: ProxyRNode): RNode {
+ return rObj[GET_R_NODE];
+}
+
+export function isPromise(obj: unknown): obj is Promise {
+ return obj instanceof Promise;
+}
+
+export function getValue(value: any): any {
+ if (isAtom(value)) {
+ return getRNodeVal(value as Reactive);
+ }
+ if (isReactiveProxy(value)) {
+ return getRNodeVal(value[GET_R_NODE]);
+ }
+ return value;
+}
diff --git a/packages/inula/src/reactive/Var.ts b/packages/inula/src/reactive/Var.ts
new file mode 100644
index 00000000..9d1e6b1a
--- /dev/null
+++ b/packages/inula/src/reactive/Var.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 const inJSX: { current: boolean } = {
+ current: false,
+};
+
+export const inComputation: { current: boolean } = {
+ current: false,
+};
diff --git a/packages/inula/src/reactive/Watch.ts b/packages/inula/src/reactive/Watch.ts
new file mode 100644
index 00000000..9841a246
--- /dev/null
+++ b/packages/inula/src/reactive/Watch.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { RContext } from './RContext';
+import { calculateReactive } from './Utils';
+import { Reactive } from './types';
+
+export function watch(fn: () => any | Reactive, callback?: () => void): any {
+ // 有两个参数,第一个是要监听数据/函数,第二个是回调
+ if (typeof callback === 'function') {
+ const effect = new RContext(() => {
+ callback();
+ });
+
+ const endEffect = effect.start();
+
+ calculateReactive(fn);
+
+ endEffect();
+ } else {
+ // 只有一个参数
+ const effect = new RContext(fn);
+
+ const endEffect = effect.start();
+
+ fn();
+
+ endEffect();
+ }
+}
diff --git a/packages/inula/src/reactive/components/Block.tsx b/packages/inula/src/reactive/components/Block.tsx
new file mode 100644
index 00000000..7396c51a
--- /dev/null
+++ b/packages/inula/src/reactive/components/Block.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 { JSXElement } from '../../renderer/Types';
+import { calculateReactive } from '../Utils';
+
+/**
+ * Block可以控制更新范围,若View的children函数中通过get()使用了响应式数据,当响应式数据变化时,children函数会被重新执行,不会影响父组件。
+ *
+ {() => {
+ const count = _rObj.count.get();
+ return <>
+ {count}
+ >;
+ }}
+
+ * @param children
+ */
+export function Block({ children }: { children: () => JSXElement }): any {
+ const result = calculateReactive(children);
+
+ return result;
+}
diff --git a/packages/inula/src/reactive/components/For.tsx b/packages/inula/src/reactive/components/For.tsx
new file mode 100644
index 00000000..d7253932
--- /dev/null
+++ b/packages/inula/src/reactive/components/For.tsx
@@ -0,0 +1,91 @@
+/*
+ * 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 { JSXElement } from '../../renderer/Types';
+import { createElement } from '../../external/JSXElement';
+import { useMemo } from '../../renderer/hooks/HookExternal';
+import { memo } from '../../renderer/components/Memo';
+import { shallowCompare } from '../../renderer/utils/compare';
+import { ArrayState } from '../types';
+import { GET_R_NODE } from '../proxy/RProxyHandler';
+
+/**
+ * For组件用于循环渲染数据
+ * @param each 包含要循环的数据的可观察对象
+ * @param children 渲染每个数据项的子组件
+ * @returns 返回JSX元素或null
+ */
+export function For({
+ each,
+ children,
+}: {
+ each: any;
+ children?: (value: any, index: number) => JSXElement;
+}): JSXElement[] | null {
+ // 获取可观察对象中的数据数组
+ const reactiveArr = each.get();
+
+ if (!reactiveArr || !reactiveArr.length) {
+ return null;
+ }
+
+ const rNode = each[GET_R_NODE];
+ const states = rNode.states !== undefined ? rNode.states : [];
+
+ let Item: Partial | null = null;
+
+ if (children) {
+ Item = useMemo(() => {
+ return memo(({ item, index }) => children(item, index), itemCompare);
+ }, []);
+ }
+
+ const ret: JSXElement[] = [];
+ const len = reactiveArr.length;
+ for (let i = 0; i < len; i++) {
+ const state = states[i];
+ const isFresh = state === ArrayState.Fresh;
+
+ // 创建并添加JSX元素
+ ret.push(createElement(Item, { isFresh, item: each[i], index: i }));
+ }
+
+ // 用完,重置
+ rNode.states = [];
+
+ // 合并多次的diffOperator
+ if (rNode.diffOperators) {
+ rNode.diffOperators.forEach(() => {
+
+ });
+ }
+
+ ret.diffOperator = rNode.diffOperator;
+ // 用完,删除
+ delete rNode.diffOperators;
+ delete rNode.diffOperator;
+
+ // 返回JSX元素数组
+ return ret;
+}
+
+// 如果属性中有isFresh,就优先判断isFresh
+function itemCompare(oldProps, newProps) {
+ if (newProps.isFresh !== undefined) {
+ return Boolean(newProps.isFresh);
+ } else {
+ return shallowCompare(oldProps, newProps);
+ }
+}
diff --git a/packages/inula/src/reactive/components/RText.ts b/packages/inula/src/reactive/components/RText.ts
new file mode 100644
index 00000000..93b07705
--- /dev/null
+++ b/packages/inula/src/reactive/components/RText.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 { memo } from '../../renderer/components/Memo';
+import { JSXElement } from '../../renderer/Types';
+import { calculateReactive } from '../Utils';
+
+export const RText = memo(
+ function ({ children }: { children: any }): JSXElement {
+ return calculateReactive(children);
+ },
+ () => true // 属性指针始终为true,不刷新
+);
diff --git a/packages/inula/src/reactive/components/Show.tsx b/packages/inula/src/reactive/components/Show.tsx
new file mode 100644
index 00000000..daff4b8a
--- /dev/null
+++ b/packages/inula/src/reactive/components/Show.tsx
@@ -0,0 +1,38 @@
+/*
+ * 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 { JSXElement } from '../../renderer/Types';
+import { calculateReactive } from '../Utils';
+
+export function Show({
+ if: rIf,
+ else: rElse,
+ children,
+}: {
+ if: any | (() => T);
+ else?: any;
+ children: JSXElement | (() => JSXElement);
+}): any {
+ const ifValue: any = calculateReactive(rIf);
+
+ let child: JSXElement | (() => JSXElement) | null = null;
+ if (ifValue) {
+ child = typeof children === 'function' ? children() : children;
+ } else {
+ child = typeof rElse === 'function' ? rElse() : rElse;
+ }
+
+ return child;
+}
diff --git a/packages/inula/src/reactive/components/Switch.ts b/packages/inula/src/reactive/components/Switch.ts
new file mode 100644
index 00000000..ee9a9117
--- /dev/null
+++ b/packages/inula/src/reactive/components/Switch.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { calculateReactive } from '../Utils';
+import { JSXElement, VNode } from '../../renderer/Types';
+import { Children } from '../../external/ChildrenUtil';
+
+export function Switch({
+ children,
+ default: df,
+}: {
+ children: JSXElement[] | Record JSXElement>;
+ default?: JSXElement;
+}): JSXElement | null {
+ const arr = Children.toArray(children);
+
+ let index = -1;
+ for (let i = 0; i < arr.length; i++) {
+ const showComp = arr[i];
+
+ const ifValue: any = calculateReactive((showComp as VNode).props.if);
+
+ if (ifValue) {
+ index = i;
+ break;
+ }
+ }
+
+ return index >= 0 ? arr[index] : df ?? null;
+}
diff --git a/packages/inula/src/reactive/proxy/RProxyHandler.ts b/packages/inula/src/reactive/proxy/RProxyHandler.ts
new file mode 100644
index 00000000..768baf5f
--- /dev/null
+++ b/packages/inula/src/reactive/proxy/RProxyHandler.ts
@@ -0,0 +1,146 @@
+/*
+ * 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 { isArray } from '../../inulax/CommonUtils';
+import { getOrCreateChildProxy, getRNodeVal, setRNodeVal, trackReactiveData } from '../RNode';
+import { RNode, ProxyRNode } from '../types';
+
+const GET = 'get';
+const SET = 'set';
+const READ = 'read';
+const DELETE = 'delete';
+const ONCHANGE = 'onChange';
+export const GET_R_NODE = '$$getRNode';
+const PROTOTYPE = 'prototype';
+
+// 数组的修改方法
+const MODIFY_ARR_FNS = new Set([
+ 'push',
+ 'pop',
+ 'splice',
+ 'shift',
+ 'unshift',
+ 'reverse',
+ 'sort',
+ 'fill',
+ 'from',
+ 'copyWithin',
+]);
+
+// 数组的遍历方法
+const LOOP_ARR_FNS = new Set(['forEach', 'map', 'every', 'some', 'filter', 'join']);
+
+export function createProxy(proxyNode: RNode): ProxyRNode {
+ return new Proxy(proxyNode, {
+ get,
+ set,
+ });
+}
+
+const GetFns: Record any> = {
+ [GET]: getFn,
+ [READ]: readFn,
+ [DELETE]: deleteFn,
+ [ONCHANGE]: onChangeFn,
+};
+
+function get(rNode: RNode, key: string | symbol): any {
+ // 处理get,read,delete,onchange方法
+ const fn = GetFns[key];
+ if (fn && typeof fn === 'function') {
+ return () => fn(rNode);
+ }
+
+ // 调用set()方法
+ if (key === SET) {
+ return function (val: any) {
+ setRNodeVal(rNode, val, true, false);
+ };
+ }
+
+ if (key === GET_R_NODE) {
+ return rNode;
+ }
+
+ const rawObj = getRNodeVal(rNode);
+
+ // const value = rawObj !== undefined ? Reflect.get(rawObj, key) : rawObj;
+ const value = rawObj !== undefined ? rawObj[key] : rawObj;
+
+ // 对于prototype不做代理
+ if (key === PROTOTYPE) {
+ return value;
+ }
+
+ if (isArray(rawObj) && key === 'length') {
+ // 标记依赖
+ trackReactiveData(rNode);
+ return value;
+ }
+
+ // 处理数组的方法
+ if (typeof value === 'function') {
+ if (isArray(rawObj)) {
+ // 处理数组的修改方法
+ if (MODIFY_ARR_FNS.has(key)) {
+ return (...args: any[]) => {
+ // 调用数组方法的时候,前后是相同的引用,所以需要先浅拷贝数组,并在浅拷贝的数组上进行操作
+ const value = rawObj.slice();
+ const ret = value[key](...args);
+ // 调用了数组的修改方法,默认值有变化
+ setRNodeVal(rNode, value, true, true);
+
+ return ret;
+ };
+ } else if (LOOP_ARR_FNS.has(key)) { // 处理数组的遍历方法
+ // 标记被使用了
+ trackReactiveData(rNode);
+
+ return function (callBackFn: any, thisArg?: any) {
+ function cb(_: any, index: number, array: any[]) {
+ const idx = String(index);
+ const itemProxy = getOrCreateChildProxy(array[idx], rNode, idx);
+ return callBackFn(itemProxy, index, array);
+ }
+
+ return rawObj[key](cb, thisArg);
+ };
+ }
+ }
+ return value.bind(rawObj);
+ }
+
+ return getOrCreateChildProxy(value, rNode, key);
+}
+
+function set(proxyNode: any, key: string, value: any, receiver: any): boolean {
+ return true;
+}
+
+// get()调用的处理
+function getFn(node: RNode) {
+ trackReactiveData(node);
+ return readFn(node);
+}
+
+function readFn(node: RNode) {
+ return getRNodeVal(node);
+}
+
+// delete()调用的处理
+function deleteFn() {}
+
+// onChange()调用的处理
+function onChangeFn() {}
diff --git a/packages/inula/src/reactive/types.ts b/packages/inula/src/reactive/types.ts
new file mode 100644
index 00000000..e1c0abc5
--- /dev/null
+++ b/packages/inula/src/reactive/types.ts
@@ -0,0 +1,116 @@
+/*
+ * 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 '../renderer/Types';
+import { Atom } from './Atom';
+import { RContextSet } from './RContext';
+import { DiffOperator } from './DiffUtils';
+
+export enum ArrayState {
+ Fresh = 0,
+ NotFresh = 1,
+}
+
+export interface Root {
+ $?: T;
+
+ /**
+ * 下面属性computed使用
+ * @param {readOnly} 标识computed 是否处于写入状态
+ */
+ readOnly?: boolean;
+}
+
+export type PrimitiveType = string | number | boolean;
+
+export type ValueType = { [K in keyof T]: any } | Record | PrimitiveType;
+
+export interface BaseNodeFns {
+ /**
+ * 返回响应式对象的值,自动追踪依赖
+ */
+ get(): T;
+
+ /**
+ * 返回响应式对象的值,不追踪依赖
+ */
+ read(): T;
+}
+
+export interface ProxyRNodeFn extends BaseNodeFns {
+ set>(value: V | ((prev: T) => V));
+}
+
+export interface AtomNodeFn extends BaseNodeFns {
+ set(value: V | ((prev: T) => V));
+}
+
+type PropsRecursive = T[K] extends PrimitiveType
+ ? AtomNode
+ : T[K] extends any[]
+ ? any[] & ProxyRNodeFn
+ : T extends Record
+ ? RecurseType
+ : T[K];
+
+export type ProxyRNodeProps = {
+ [K in keyof T]: PropsRecursive>;
+};
+
+export type ComputedProps = {
+ [K in keyof T]: PropsRecursive>;
+};
+
+export type ProxyRNode = ProxyRNodeFn & ProxyRNodeProps;
+
+export type AtomNode = AtomNodeFn;
+
+export type Computed = BaseNodeFns & ComputedProps;
+
+export type ReactiveProxy = T extends PrimitiveType ? AtomNode : ProxyRNode;
+
+export interface BaseRNode {
+ // 标识Node类型 atomSymbol,nodeSymbol,computedSymbol
+ type: symbol;
+ root: Root;
+ children?: Map | Atom>;
+ usedRContexts?: RContextSet;
+ proxy?: any;
+
+ diffOperator?: DiffOperator;
+ diffOperators?: DiffOperator[];
+ states?: ArrayState[];
+}
+
+export interface RootRNode extends BaseRNode {
+ parentKey: null;
+ parent: null;
+}
+
+export interface ChildrenRNode extends BaseRNode {
+ parentKey: string | symbol;
+ parent: RNode;
+}
+
+export type RNode = RootRNode | ChildrenRNode;
+
+export type Reactive = RNode | Atom;
+
+export interface RContextParam {
+ vNode?: VNode;
+ reactive?: Reactive;
+}
+
+export type RContextCallback = (params: RContextParam, reactive: Reactive) => void;
diff --git a/packages/inula/src/renderer/TreeBuilder.ts b/packages/inula/src/renderer/TreeBuilder.ts
index dff56e07..4f9a2026 100644
--- a/packages/inula/src/renderer/TreeBuilder.ts
+++ b/packages/inula/src/renderer/TreeBuilder.ts
@@ -18,7 +18,7 @@ import type { VNode } from './Types';
import { callRenderQueueImmediate, pushRenderCallback } from './taskExecutor/RenderQueue';
import { updateVNode } from './vnode/VNodeCreator';
import { ContextProvider, DomComponent, DomPortal, TreeRoot } from './vnode/VNodeTags';
-import { FlagUtils, InitFlag, Interrupted } from './vnode/VNodeFlags';
+import { FlagUtils, InitFlag, Interrupted, Deletion } from './vnode/VNodeFlags';
import { captureVNode } from './render/BaseComponent';
import { checkLoopingUpdateLimit, submitToRender } from './submit/Submit';
import { runAsyncEffects } from './submit/HookEffectHandler';
@@ -81,13 +81,13 @@ function collectDirtyNodes(vNode: VNode, parent: VNode): void {
if (parent.dirtyNodes === null) {
parent.dirtyNodes = dirtyNodes;
} else {
- parent.dirtyNodes.push(...vNode.dirtyNodes!);
+ parent.dirtyNodes.push(...dirtyNodes);
dirtyNodes.length = 0;
}
vNode.dirtyNodes = null;
}
- if (FlagUtils.hasAnyFlag(vNode)) {
+ if (FlagUtils.hasAnyFlag(vNode) && vNode.flags !== Deletion) {
if (parent.dirtyNodes === null) {
parent.dirtyNodes = [vNode];
} else {
@@ -273,44 +273,44 @@ function buildVNodeTree(treeRoot: VNode) {
changeMode(InRender, true);
// 计算出开始节点
- const startVNode = calcStartUpdateVNode(treeRoot);
+ // const startVNode = calcStartUpdateVNode(treeRoot);
// 缓存起来
- setStartVNode(startVNode);
+ setStartVNode(treeRoot);
// 清空toUpdateNodes
treeRoot.toUpdateNodes?.clear();
- if (startVNode.tag !== TreeRoot) {
- // 不是根节点
- // 设置namespace,用于createElement
- let parent = startVNode.parent;
- while (parent !== null) {
- const tag = parent.tag;
- if (tag === DomComponent) {
- break;
- } else if (tag === TreeRoot || tag === DomPortal) {
- break;
- }
- parent = parent.parent;
- }
-
- // 当在componentWillUnmount中调用setState,parent可能是null,因为startVNode会被clear
- if (parent !== null) {
- resetNamespaceCtx(parent);
- setNamespaceCtx(parent, parent.realNode);
- }
-
- // 恢复父节点的context
- recoverTreeContext(startVNode);
- }
+ // if (startVNode.tag !== TreeRoot) {
+ // // 不是根节点
+ // // 设置namespace,用于createElement
+ // let parent = startVNode.parent;
+ // while (parent !== null) {
+ // const tag = parent.tag;
+ // if (tag === DomComponent) {
+ // break;
+ // } else if (tag === TreeRoot || tag === DomPortal) {
+ // break;
+ // }
+ // parent = parent.parent;
+ // }
+ //
+ // // 当在componentWillUnmount中调用setState,parent可能是null,因为startVNode会被clear
+ // if (parent !== null) {
+ // resetNamespaceCtx(parent);
+ // setNamespaceCtx(parent, parent.realNode);
+ // }
+ //
+ // // 恢复父节点的context
+ // recoverTreeContext(startVNode);
+ // }
// 重置环境变量,为重新进行深度遍历做准备
- resetProcessingVariables(startVNode);
+ resetProcessingVariables(treeRoot);
// devProps 用于插件手动更新props值
- if (startVNode.devProps !== undefined) {
- startVNode.props = startVNode.devProps;
- startVNode.devProps = undefined;
- }
+ // if (startVNode.devProps !== undefined) {
+ // startVNode.props = startVNode.devProps;
+ // startVNode.devProps = undefined;
+ // }
while (processing !== null) {
try {
@@ -327,11 +327,11 @@ function buildVNodeTree(treeRoot: VNode) {
handleError(treeRoot, thrownValue);
}
}
- if (startVNode.tag !== TreeRoot) {
- // 不是根节点
- // 恢复父节点的context
- resetTreeContext(startVNode);
- }
+ // if (startVNode.tag !== TreeRoot) {
+ // // 不是根节点
+ // // 恢复父节点的context
+ // resetTreeContext(startVNode);
+ // }
setProcessingClassVNode(null);
diff --git a/packages/inula/src/renderer/Types.ts b/packages/inula/src/renderer/Types.ts
index 9a80f6c6..2ebc7c74 100644
--- a/packages/inula/src/renderer/Types.ts
+++ b/packages/inula/src/renderer/Types.ts
@@ -14,6 +14,7 @@
*/
import { BELONG_CLASS_VNODE_KEY } from './vnode/VNode';
+import { VNodeTag } from './vnode/VNodeTags';
export { VNode } from './vnode/VNode';
@@ -82,3 +83,75 @@ export type Source = {
};
export type Callback = () => void;
+
+export type VNode = {
+ tag: VNodeTag,
+ key: string | null, // 唯一标识符
+ props: any, // 传给组件的props的值,类组件包含defaultProps,Lazy组件不包含
+ type: any,
+ realNode: any, // 如果是类,则存放实例;如果是div这种,则存放真实DOM;
+
+ // 关系结构
+ parent: VNode | null, // 父节点
+ child: VNode | null, // 子节点
+ next: VNode | null, // 兄弟节点
+ cIndex, // 节点在children数组中的位置
+ eIndex, // HorizonElement在jsx中的位置,例如:jsx中的null不会生成vNode,所以eIndex和cIndex不一致
+
+ ref: RefType | ((handle: any) => void) | null, // 包裹一个函数,submit阶段使用,比如将外部useRef生成的对象赋值到ref上
+ oldProps: any,
+
+ // 是否已经被从树上移除
+ isCleared,
+ changeList: any, // DOM的变更列表
+ effectList: any[] | null, // useEffect 的更新数组
+ updates: any[] | null, // TreeRoot和ClassComponent使用的更新数组
+ stateCallbacks: any[] | null, // 存放存在setState的第二个参数和HorizonDOM.render的第三个参数所在的node数组
+ isForceUpdate: boolean, // 是否使用强制更新
+ isSuspended, // 是否被suspense打断更新
+ state: any, // ClassComponent和TreeRoot的状态
+ hooks: Array> | null, // 保存hook
+ depContexts: Array> | null, // FunctionComponent和ClassComponent对context的依赖列表
+ isDepContextChange: boolean, // context是否变更
+ dirtyNodes: Array | null, // 需要改动的节点数组
+ shouldUpdate,
+ childShouldUpdate,
+ task: any,
+
+ // 使用这个变量来记录修改前的值,用于恢复。
+ context: any,
+ // 因为LazyComponent会修改tag和type属性,为了能识别,增加一个属性
+ isLazyComponent: boolean,
+
+ // 因为LazyComponent会修改type属性,为了在diff中判断是否可以复用,需要增加一个lazyType
+ lazyType: any,
+ flags,
+ clearChild: VNode | null,
+ // one tree相关属性
+ isCreated,
+ oldHooks: Array> | null, // 保存上一次执行的hook
+ oldState: any,
+ oldRef: RefType | ((handle: any) => void) | null,
+ oldChild: VNode | null,
+ promiseResolve: boolean, // suspense的promise是否resolve
+ devProps: any, // 用于dev插件临时保存更新props值
+ suspenseState: SuspenseState | null,
+
+ path: string, // 保存从根到本节点的路径
+
+ // 根节点数据
+ toUpdateNodes: Set | null, // 保存要更新的节点
+ delegatedEvents: Set,
+
+ belongClassVNode: VNode | null, // 记录JSXElement所属class vNode,处理ref的时候使用
+
+ // 状态管理器HorizonX使用
+ isStoreChange: boolean,
+ observers: Set | null, // 记录这个函数组件/类组件依赖哪些Observer
+ classComponentWillUnmount: Function | null, // HorizonX会在classComponentWillUnmount中清除对VNode的引入用
+ src: Source | null, // 节点所在代码位置
+
+ // reactive
+ attrRContexts: Set | null,
+ compRContext: any | null,
+}
diff --git a/packages/inula/src/renderer/diff/nodeDiffComparator.ts b/packages/inula/src/renderer/diff/nodeDiffComparator.ts
index 8de6f392..1216916c 100644
--- a/packages/inula/src/renderer/diff/nodeDiffComparator.ts
+++ b/packages/inula/src/renderer/diff/nodeDiffComparator.ts
@@ -23,11 +23,14 @@ import {
createFragmentVNode,
createPortalVNode,
createDomTextVNode,
+ createReactiveVNode,
} from '../vnode/VNodeCreator';
import { isSameType, getIteratorFn, isTextType, isIteratorType, isObjectType } from './DiffTools';
import { travelChildren } from '../vnode/VNodeUtils';
import { markVNodePath } from '../utils/vNodePath';
import { BELONG_CLASS_VNODE_KEY } from '../vnode/VNode';
+import { Operation } from '../../reactive/DiffUtils';
+import { isReactiveProxy, isReactiveObj, getRNodeFromProxy } from '../../reactive/Utils';
enum DiffCategory {
TEXT_NODE = 'TEXT_NODE',
@@ -101,6 +104,9 @@ function getNodeType(newChild: any): string | null {
return DiffCategory.TEXT_NODE;
}
if (isObjectType(newChild)) {
+ if (isReactiveObj(newChild)) {
+ return DiffCategory.REACTIVE_NODE;
+ }
if (Array.isArray(newChild) || isIteratorType(newChild)) {
return DiffCategory.ARR_NODE;
}
@@ -152,6 +158,14 @@ function getNewNode(parentNode: VNode, newChild: any, oldNode: VNode | null) {
}
break;
}
+ case DiffCategory.REACTIVE_NODE: {
+ if (oldNode === null || oldNode.tag !== ReactiveComponent) {
+ resultNode = createReactiveVNode(newChild);
+ } else {
+ resultNode = updateVNode(oldNode, newChild);
+ }
+ break;
+ }
case DiffCategory.OBJECT_NODE: {
if (newChild.vtype === TYPE_COMMON_ELEMENT) {
if (newChild.type === TYPE_FRAGMENT) {
@@ -564,7 +578,7 @@ function diffObjectNodeHandler(
}
let resultNode: VNode | null = null;
- let startDelVNode: VNode | null = firstChildVNode;
+ let startDelVNode: VNode | null = node;
if (newChild.vtype === TYPE_COMMON_ELEMENT) {
if (canReuseNode) {
// 可以复用
@@ -624,13 +638,91 @@ function diffObjectNodeHandler(
return null;
}
+// 响应式For组件专用
+function diffReactiveForNodeHandler(
+ parentNode: VNode,
+ firstChild: VNode | null,
+ newChildren: Array,
+): VNode | null {
+ let oldNode = firstChild;
+ let nextOldNode: VNode | null = null;
+ let resultingFirstChild: VNode | null = null;
+ let prevNewNode: VNode | null = null;
+
+ function appendNode(newNode: VNode) {
+ if (prevNewNode === null) {
+ resultingFirstChild = newNode;
+ newNode.cIndex = 0;
+ } else {
+ prevNewNode.next = newNode;
+ newNode.cIndex = prevNewNode.cIndex + 1;
+ }
+ markVNodePath(newNode);
+ prevNewNode = newNode;
+ }
+
+ // 特殊处理
+ // 如果新节点为空
+ if (newChildren.length === 0) {
+ if (firstChild) {
+ FlagUtils.markClear(parentNode);
+ parentNode.clearChild = firstChild;
+
+ return null;
+ }
+ }
+
+ let childIndex = 0;
+ const { opts } = newChildren.diffOperator;
+ for (let i = 0; i < opts.length; i++) {
+ const opt = opts[i];
+ if (oldNode !== null) {
+ // 先保存next,因为getNewNode会修改next
+ nextOldNode = oldNode.next;
+ }
+
+ switch (opt.action) {
+ case Operation.Nop: {
+ const newNode = getNewNode(parentNode, newChildren[childIndex], oldNode);
+ newNode.eIndex = childIndex;
+ appendNode(newNode);
+ // 使用了加1
+ childIndex++;
+ // 使用了oldNode,更新
+ oldNode = nextOldNode;
+ break;
+ }
+ case Operation.Insert: {
+ const newNode = getNewNode(parentNode, newChildren[childIndex], null);
+ FlagUtils.setAddition(newNode);
+ newNode.eIndex = childIndex;
+ appendNode(newNode);
+ // 使用了加1
+ childIndex++;
+ break;
+ }
+ case Operation.Delete: {
+ deleteVNode(parentNode, oldNode);
+ // 使用了oldNode,更新
+ oldNode = nextOldNode;
+ break;
+ }
+ }
+
+ }
+
+ return resultingFirstChild;
+}
+
// Diff算法的对外接口
export function createChildrenByDiff(
parentNode: VNode,
firstChild: VNode | null,
- newChild: any,
+ child: any,
isComparing: boolean
): VNode | null {
+ let newChild = isReactiveProxy(child) ? getRNodeFromProxy(child) : child;
+
const isFragment = isNoKeyFragment(newChild);
newChild = isFragment ? newChild.props.children : newChild;
@@ -649,7 +741,11 @@ export function createChildrenByDiff(
// 3. newChild是数组类型
if (Array.isArray(newChild)) {
- return diffArrayNodesHandler(parentNode, firstChild, newChild);
+ if (newChild.diffOperator) {
+ return diffReactiveForNodeHandler(parentNode, firstChild, newChild);
+ } else {
+ return diffArrayNodesHandler(parentNode, firstChild, newChild);
+ }
}
// 4. newChild是迭代器类型
diff --git a/packages/inula/src/renderer/hooks/HookExternal.ts b/packages/inula/src/renderer/hooks/HookExternal.ts
index 44e2dc04..67a887de 100644
--- a/packages/inula/src/renderer/hooks/HookExternal.ts
+++ b/packages/inula/src/renderer/hooks/HookExternal.ts
@@ -24,6 +24,11 @@ import { useReducerImpl } from './UseReducerHook';
import { useStateImpl } from './UseStateHook';
import { getNewContext } from '../components/context/Context';
import { getProcessingVNode } from '../GlobalVar';
+import { useAtomImpl } from './reactive/UseAtom';
+import { useComputedImpl } from './reactive/UseCompute';
+import { useReactiveImpl } from './reactive/UseReactive';
+import { AtomNode, PrimitiveType, Reactive } from '../../reactive/types';
+import { useWatchImpl } from './reactive/UseWatch';
import type { MutableRef, RefCallBack, RefObject } from './HookType';
import type {
@@ -115,3 +120,23 @@ export function useImperativeHandle(
// 兼容react-redux
export const useDebugValue = () => {};
+
+// reactive hooks
+export function useAtom(initialValue: T): [AtomNode, (value: T) => void] {
+ return useAtomImpl(initialValue);
+}
+
+export function useCompute(compute: () => T) {
+ return useComputedImpl(compute);
+}
+export function useComputed(compute: () => T) {
+ return useComputedImpl(compute);
+}
+
+export function useReactive(obj: T) {
+ return useReactiveImpl(obj);
+}
+
+export function useWatch(fn: () => any | Reactive, callback?: () => void) {
+ return useWatchImpl(fn, callback);
+}
diff --git a/packages/inula/src/renderer/hooks/UseWatch.ts b/packages/inula/src/renderer/hooks/UseWatch.ts
new file mode 100644
index 00000000..8ef9b885
--- /dev/null
+++ b/packages/inula/src/renderer/hooks/UseWatch.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 { watch } from '../../horizonx/proxy/watch';
+
+export function useWatchImpl(fn: () => void) {
+ watch(fn);
+}
diff --git a/packages/inula/src/renderer/hooks/reactive/UseAtom.ts b/packages/inula/src/renderer/hooks/reactive/UseAtom.ts
new file mode 100644
index 00000000..9c1a5b81
--- /dev/null
+++ b/packages/inula/src/renderer/hooks/reactive/UseAtom.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { getHookStage, HookStage } from '../HookStage';
+import { createHook, getCurrentHook, throwNotInFuncError } from '../BaseHook';
+import { disposeReactive } from '../../../reactive/RContext';
+import { useCallbackImpl } from '../UseCallbackHook';
+import { useEffectImpl } from '../UseEffectHook';
+import { Atom } from '../../../reactive/Atom';
+import { AtomNode, PrimitiveType } from '../../../reactive/types';
+
+export function useAtomImpl(initialValue: T): [AtomNode, (value: T) => void] {
+ const stage = getHookStage();
+ let atom: Atom;
+
+ switch (stage) {
+ case HookStage.Init:
+ atom = new Atom(initialValue);
+ createHook(atom);
+ break;
+ case HookStage.Update:
+ atom = getCurrentHook().state as unknown as Atom;
+ break;
+ default:
+ throwNotInFuncError();
+ }
+
+ const setAtom = useCallbackImpl((value: T) => {
+ atom.set(value);
+ }, []);
+
+ // 组件销毁时,清除effect
+ useEffectImpl(
+ () => () => {
+ disposeReactive(atom);
+ },
+ []
+ );
+
+ return [atom, setAtom];
+}
diff --git a/packages/inula/src/renderer/hooks/reactive/UseCompute.ts b/packages/inula/src/renderer/hooks/reactive/UseCompute.ts
new file mode 100644
index 00000000..c3dc9d39
--- /dev/null
+++ b/packages/inula/src/renderer/hooks/reactive/UseCompute.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { useMemoImpl } from '../UseMemoHook';
+import { Computed } from '../../../reactive/types';
+import { computed } from '../../../reactive/Computed';
+
+export function useComputedImpl(fn: () => T): Computed {
+ return useMemoImpl(() => {
+ return computed(fn);
+ }, []);
+}
diff --git a/packages/inula/src/renderer/hooks/reactive/UseReactive.ts b/packages/inula/src/renderer/hooks/reactive/UseReactive.ts
new file mode 100644
index 00000000..14176890
--- /dev/null
+++ b/packages/inula/src/renderer/hooks/reactive/UseReactive.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { reactive } from '../../../reactive/Reactive';
+import { getHookStage, HookStage } from '../HookStage';
+import { createHook, getCurrentHook, throwNotInFuncError } from '../BaseHook';
+import { useEffectImpl } from '../UseEffectHook';
+import { disposeReactive } from '../../../reactive/RContext';
+import { Reactive, ReactiveProxy } from '../../../reactive/types';
+
+export function useReactiveImpl(obj) {
+ const stage = getHookStage();
+ let reactiveObj: any;
+
+ switch (stage) {
+ case HookStage.Init:
+ reactiveObj = reactive(obj);
+
+ createHook(reactiveObj);
+ break;
+ case HookStage.Update:
+ reactiveObj = getCurrentHook().state as unknown as Reactive;
+ break;
+ default:
+ throwNotInFuncError();
+ }
+
+ // 组件销毁时,清除effect
+ useEffectImpl(
+ () => () => {
+ disposeReactive(reactiveObj);
+ },
+ []
+ );
+
+ return reactiveObj as ReactiveProxy;
+}
diff --git a/packages/inula/src/renderer/hooks/reactive/UseWatch.ts b/packages/inula/src/renderer/hooks/reactive/UseWatch.ts
new file mode 100644
index 00000000..0661a652
--- /dev/null
+++ b/packages/inula/src/renderer/hooks/reactive/UseWatch.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 { useEffectImpl } from '../UseEffectHook';
+import { Reactive } from '../../../reactive/types';
+import { watch } from '../../../reactive/Watch';
+import { useMemoImpl } from '../UseMemoHook';
+
+export function useWatchImpl(fn: () => any | Reactive, callback?: () => void): void {
+ useMemoImpl(() => {
+ watch(fn, callback);
+ }, []);
+
+ // 组件销毁时
+ useEffectImpl(() => () => {}, []);
+}
diff --git a/packages/inula/src/renderer/render/ClassComponent.ts b/packages/inula/src/renderer/render/ClassComponent.ts
index 2792425c..fc3a1620 100644
--- a/packages/inula/src/renderer/render/ClassComponent.ts
+++ b/packages/inula/src/renderer/render/ClassComponent.ts
@@ -34,6 +34,7 @@ import { processUpdates } from '../UpdateHandler';
import { setProcessingClassVNode } from '../GlobalVar';
import { onlyUpdateChildVNodes } from '../vnode/VNodeCreator';
import { createChildrenByDiff } from '../diff/nodeDiffComparator';
+import { createComponentDependent } from '../../reactive/RContextCreator';
const emptyContextObj = {};
// 获取当前节点的context
@@ -174,7 +175,7 @@ export function captureRender(processing: VNode): VNode | null {
// 不复用
if (shouldUpdate) {
- return createChildren(ctor, processing);
+ return createComponentDependent(() => createChildren(ctor, processing), processing);
} else {
return onlyUpdateChildVNodes(processing);
}
diff --git a/packages/inula/src/renderer/render/DomComponent.ts b/packages/inula/src/renderer/render/DomComponent.ts
index 65044cb2..8a2476b6 100644
--- a/packages/inula/src/renderer/render/DomComponent.ts
+++ b/packages/inula/src/renderer/render/DomComponent.ts
@@ -21,7 +21,7 @@ import { appendChildElement, newDom, initDomProps, getPropChangeList, isTextChil
import { FlagUtils } from '../vnode/VNodeFlags';
import { markRef } from './BaseComponent';
import { DomComponent, DomPortal, DomText } from '../vnode/VNodeTags';
-import { travelVNodeTree } from '../vnode/VNodeUtils';
+import { isDomVNode, travelVNodeTree } from '../vnode/VNodeUtils';
import { createChildrenByDiff } from '../diff/nodeDiffComparator';
function updateDom(processing: VNode, type: any, newProps: Props) {
@@ -74,7 +74,7 @@ export function bubbleRender(processing: VNode) {
travelVNodeTree(
vNode,
node => {
- if (node.tag === DomComponent || node.tag === DomText) {
+ if (isDomVNode(node)) {
appendChildElement(dom, node.realNode);
}
},
diff --git a/packages/inula/src/renderer/render/FunctionComponent.ts b/packages/inula/src/renderer/render/FunctionComponent.ts
index ed3cd4a4..8419f581 100644
--- a/packages/inula/src/renderer/render/FunctionComponent.ts
+++ b/packages/inula/src/renderer/render/FunctionComponent.ts
@@ -22,6 +22,7 @@ import { ForwardRef } from '../vnode/VNodeTags';
import { FlagUtils, Update } from '../vnode/VNodeFlags';
import { onlyUpdateChildVNodes } from '../vnode/VNodeCreator';
import { createChildrenByDiff } from '../diff/nodeDiffComparator';
+import { createComponentDependent } from '../../reactive/RContextCreator';
// 在useState, useReducer的时候,会触发state变化
let stateChange = false;
@@ -56,10 +57,14 @@ export function captureFunctionComponent(processing: VNode, funcComp: any, nextP
// 在执行exeFunctionHook前先设置stateChange为false
setStateChange(false);
- const newElements = runFunctionWithHooks(
- processing.tag === ForwardRef ? funcComp.render : funcComp,
- nextProps,
- processing.tag === ForwardRef ? processing.ref : undefined,
+ const newElements = createComponentDependent(
+ () =>
+ runFunctionWithHooks(
+ processing.tag === ForwardRef ? funcComp.render : funcComp,
+ nextProps,
+ processing.tag === ForwardRef ? processing.ref : undefined,
+ processing
+ ),
processing
);
diff --git a/packages/inula/src/renderer/render/ReactiveComponent.ts b/packages/inula/src/renderer/render/ReactiveComponent.ts
new file mode 100644
index 00000000..33956375
--- /dev/null
+++ b/packages/inula/src/renderer/render/ReactiveComponent.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 type { VNode } from '../Types';
+
+import { throwIfTrue } from '../utils/throwIfTrue';
+import { newTextDom } from '../../dom/DOMOperator';
+import { subscribeReactiveComponent } from '../../reactive/RContextCreator';
+import { getRNode, getValue, isPrimitive } from '../../reactive/Utils';
+
+export function captureRender(): VNode | null {
+ return null;
+}
+
+export function bubbleRender(processing: VNode) {
+ const text = processing.props;
+ const newText = getValue(text);
+
+ if (!processing.isCreated && processing.realNode != null) {
+ // 更新不需要处理
+ } else {
+ // 初始化
+ if (!isPrimitive(newText)) {
+ // 如果存在bug,可能出现这种情况
+ throwIfTrue(processing.realNode === null, 'The reactive obj value must be a primitive.');
+ }
+
+ // 获得对应节点
+ processing.realNode = newTextDom(newText, processing);
+
+ // 监听Reactive
+ subscribeReactiveComponent(processing.realNode, getRNode(text));
+ }
+}
diff --git a/packages/inula/src/renderer/render/index.ts b/packages/inula/src/renderer/render/index.ts
index 3422db55..9f1c4dc1 100644
--- a/packages/inula/src/renderer/render/index.ts
+++ b/packages/inula/src/renderer/render/index.ts
@@ -27,6 +27,7 @@ import * as DomTextRender from './DomText';
import * as LazyComponentRender from './LazyComponent';
import * as MemoComponentRender from './MemoComponent';
import * as SuspenseComponentRender from './SuspenseComponent';
+import * as ReactiveComponentRender from './ReactiveComponent';
import {
ClassComponent,
@@ -42,6 +43,7 @@ import {
LazyComponent,
MemoComponent,
SuspenseComponent,
+ ReactiveComponent,
} from '../vnode/VNodeTags';
export { BaseComponentRender };
@@ -60,4 +62,5 @@ export default {
[LazyComponent]: LazyComponentRender,
[MemoComponent]: MemoComponentRender,
[SuspenseComponent]: SuspenseComponentRender,
+ [ReactiveComponent]: ReactiveComponentRender,
};
diff --git a/packages/inula/src/renderer/submit/LifeCycleHandler.ts b/packages/inula/src/renderer/submit/LifeCycleHandler.ts
index 34d34e8d..8d85661b 100644
--- a/packages/inula/src/renderer/submit/LifeCycleHandler.ts
+++ b/packages/inula/src/renderer/submit/LifeCycleHandler.ts
@@ -50,9 +50,10 @@ import {
callUseLayoutEffectRemove,
} from './HookEffectHandler';
import { handleSubmitError } from '../ErrorHandler';
-import { travelVNodeTree, clearVNode, isDomVNode, getSiblingDom } from '../vnode/VNodeUtils';
+import { travelVNodeTree, clearVNode, isDomVNode, getSiblingDom, findDOMContainer, isDomContainer } from '../vnode/VNodeUtils';
import { shouldAutoFocus } from '../../dom/utils/Common';
import { BELONG_CLASS_VNODE_KEY } from '../vnode/VNode';
+import { cleanupRContext } from '../../reactive/RContext';
function callComponentWillUnmount(vNode: VNode, instance: any) {
try {
@@ -205,48 +206,119 @@ function unmountDomComponents(vNode: VNode): void {
// 这两个变量要一起更新
let currentParent;
- travelVNodeTree(
- vNode,
- node => {
- if (!currentParentIsValid) {
- let parent = node.parent;
- let tag;
- while (parent !== null) {
- tag = parent.tag;
- if (tag === DomComponent || tag === TreeRoot || tag === DomPortal) {
- currentParent = parent.realNode;
- break;
+ let node = vNode;
+
+ while (true) {
+ // handle
+ if (!currentParentIsValid) {
+ let parent = node.parent;
+ let tag;
+ while (parent !== null) {
+ tag = parent.tag;
+ if (tag === DomComponent || tag === TreeRoot || tag === DomPortal) {
+ currentParent = parent.realNode;
+ break;
+ }
+ parent = parent.parent;
+ }
+ currentParentIsValid = true;
+ }
+
+ if (node.tag === DomComponent || node.tag === DomText) {
+ let nd = node;
+
+ // 卸载vNode,递归遍历子vNode
+ outer: while (true) {
+ unmountVNode(nd);
+
+ // 找子节点
+ const childVNode = nd.child;
+ if (childVNode !== null && nd.tag !== DomPortal) {
+ childVNode.parent = nd;
+ nd = childVNode;
+ continue;
+ }
+
+ // 回到开始节点
+ if (nd === node) {
+ break;
+ }
+
+ // 找兄弟,没有就往上再找兄弟
+ while (nd.next === null) {
+ if (nd.parent === null || nd.parent === node) {
+ break outer;
}
- parent = parent.parent;
+ nd = nd.parent;
}
- currentParentIsValid = true;
+ // 找到兄弟
+ const siblingVNode = nd.next;
+ siblingVNode.parent = nd.parent;
+ nd = siblingVNode;
}
- if (node.tag === DomComponent || node.tag === DomText) {
- // 卸载vNode,递归遍历子vNode
- unmountNestedVNodes(node);
-
- // 在所有子项都卸载后,删除dom树中的节点
- removeChildDom(currentParent, node.realNode);
- } else if (node.tag === DomPortal) {
- if (node.child !== null) {
- currentParent = node.realNode;
- }
- } else {
- unmountVNode(node);
+ // 在所有子项都卸载后,删除dom树中的节点
+ removeChildDom(currentParent, node.realNode);
+ } else if (node.tag === DomPortal) {
+ if (node.child !== null) {
+ currentParent = node.realNode;
}
- },
- node =>
- // 如果是dom不用再遍历child
- node.tag === DomComponent || node.tag === DomText,
- vNode,
- node => {
+ } else {
+ unmountVNode(node);
+ }
+
+ // 找子节点
+ const childVNode = node.child;
+ if (childVNode !== null && node.tag !== DomComponent && node.tag !== DomText) {
+ childVNode.parent = node;
+ node = childVNode;
+ continue;
+ }
+
+ // 回到开始节点
+ if (node === vNode) {
+ return null;
+ }
+
+ // 找兄弟,没有就往上再找兄弟
+ while (node.next === null) {
+ if (node.parent === null || node.parent === vNode) {
+ return null;
+ }
+ node = node.parent;
+
if (node.tag === DomPortal) {
// 当离开portal,需要重新设置parent
currentParentIsValid = false;
}
}
- );
+ // 找到兄弟
+ const siblingVNode = node.next;
+ siblingVNode.parent = node.parent;
+ node = siblingVNode;
+ }
+}
+
+// dom节点上的attr值,如果是reactive对象,会自动创建attrEffect。所以在清除时,需要销毁attrEffect。
+function detachAttrRContexts(vNode: VNode) {
+ const attrRContexts = vNode.attrRContexts;
+ if (attrRContexts) {
+ for (const attrRContext of attrRContexts) {
+ cleanupRContext(attrRContext);
+ attrRContext.callback = null;
+ }
+
+ attrRContexts.clear();
+ }
+}
+
+// 函数组件和Class组件会自动创rContext,所以在清除时,需要销毁rContext
+function detachCompRContext(vNode: VNode) {
+ const rContext = vNode.compRContext;
+ if (rContext) {
+ cleanupRContext(rContext);
+ rContext.callback = null;
+ }
}
// 卸载一个vNode,不会递归
@@ -256,6 +328,8 @@ function unmountVNode(vNode: VNode): void {
case ForwardRef:
case MemoComponent: {
callEffectRemove(vNode);
+
+ detachCompRContext(vNode);
break;
}
case ClassComponent: {
@@ -273,10 +347,14 @@ function unmountVNode(vNode: VNode): void {
vNode.classComponentWillUnmount(vNode);
vNode.classComponentWillUnmount = null;
}
+
+ detachCompRContext(vNode);
break;
}
case DomComponent: {
detachRef(vNode);
+
+ detachAttrRContexts(vNode);
break;
}
case DomPortal: {
@@ -390,6 +468,64 @@ function submitClear(vNode: VNode): void {
vNode.clearChild = null;
}
+function submitClear2(vNode: VNode): void {
+ let domVNode;
+ if (isDomContainer(vNode)) {
+ domVNode = vNode;
+ }
+
+ // 1、拿到最近的父DOM
+ domVNode = findDOMContainer(vNode);
+ const dom = domVNode.realNode;
+
+ // 2、复制DOM节点
+ const cloneDom = dom.cloneNode(false);
+
+ // 3、复制cloneNode未能复制的属性
+ // 真实 dom 获取的keys只包含新增的属性
+ // 比如真实 dom 拿到的 keys 一般只有两个 horizon 自定义属性
+ // 但考虑到用户可能自定义其他属性,所以采用遍历赋值的方式
+ const customizeKeys = Object.keys(dom);
+ const keyLength = customizeKeys.length;
+ for (let i = 0; i < keyLength; i++) {
+ const key = customizeKeys[i];
+ // 测试代码 mock 实例的全部可遍历属性都会被Object.keys方法读取到
+ // children 属性被复制意味着复制了子节点,因此要排除
+ if (key !== 'children') {
+ cloneDom[key] = dom[key];
+ }
+ }
+
+ // 4、拿到最近的父DOM的父DOM
+ const parentDomVNode = findDOMContainer(domVNode);
+ const parentDom = parentDomVNode.realNode;
+
+ // 5、执行unmount
+ let clearChild = vNode.clearChild as VNode;
+ // 卸载 clearChild 和 它的兄弟节点
+ while (clearChild) {
+ // 卸载子vNode,递归遍历子vNode
+ unmountNestedVNodes(clearChild);
+ clearVNode(clearChild);
+ clearChild = clearChild.next as VNode;
+ }
+
+ // 6、在所有子项都卸载后,删除dom树中的节点
+ removeChildDom(parentDom, dom);
+
+ // 7、插入cloneDom
+ const realNodeNext = getSiblingDom(domVNode);
+ insertDom(parentDom, cloneDom, realNodeNext);
+
+ // 8、重置realNode
+ domVNode.realNode = cloneDom;
+ attachRef(domVNode);
+
+ // 9、清理
+ FlagUtils.removeFlag(vNode, Clear);
+ vNode.clearChild = null;
+}
+
function submitDeletion(vNode: VNode): void {
// 遍历所有子节点:删除dom节点,detach ref 和 调用componentWillUnmount()
unmountDomComponents(vNode);
@@ -440,6 +576,7 @@ export {
submitAddition,
submitDeletion,
submitClear,
+ submitClear2,
submitUpdate,
callAfterSubmitLifeCycles,
attachRef,
diff --git a/packages/inula/src/renderer/submit/Submit.ts b/packages/inula/src/renderer/submit/Submit.ts
index 53230f4e..700f0f3a 100644
--- a/packages/inula/src/renderer/submit/Submit.ts
+++ b/packages/inula/src/renderer/submit/Submit.ts
@@ -28,6 +28,7 @@ import {
submitUpdate,
detachRef,
submitClear,
+ submitClear2,
} from './LifeCycleHandler';
import { tryRenderFromRoot } from '../TreeBuilder';
import { InRender, copyExecuteMode, setExecuteMode, changeMode } from '../ExecuteMode';
@@ -98,6 +99,7 @@ function submit(dirtyNodes: Array) {
submitDeletion(node);
}
if (isClear) {
+ // submitClear2(node);
submitClear(node);
}
}
diff --git a/packages/inula/src/renderer/utils/vNodePath.ts b/packages/inula/src/renderer/utils/vNodePath.ts
index e2b5fd6f..7d8b4a93 100644
--- a/packages/inula/src/renderer/utils/vNodePath.ts
+++ b/packages/inula/src/renderer/utils/vNodePath.ts
@@ -22,7 +22,7 @@ const PATH_DELIMITER = ',';
* @param vNode
*/
export function markVNodePath(vNode: VNode) {
- vNode.path = `${vNode.parent!.path}${PATH_DELIMITER}${vNode.cIndex}`;
+ // vNode.path = `${vNode.parent!.path}${PATH_DELIMITER}${vNode.cIndex}`;
}
export function getPathArr(vNode: VNode) {
diff --git a/packages/inula/src/renderer/vnode/VNode.ts b/packages/inula/src/renderer/vnode/VNode.ts
index 1a1ba3ea..10c5978e 100644
--- a/packages/inula/src/renderer/vnode/VNode.ts
+++ b/packages/inula/src/renderer/vnode/VNode.ts
@@ -17,194 +17,152 @@
* 虚拟DOM结构体
*/
import {
- TreeRoot,
- FunctionComponent,
ClassComponent,
+ ContextConsumer,
+ ContextProvider,
+ DomComponent,
DomPortal,
DomText,
- ContextConsumer,
ForwardRef,
- SuspenseComponent,
- LazyComponent,
- DomComponent,
Fragment,
- ContextProvider,
- Profiler,
+ FunctionComponent,
+ LazyComponent,
MemoComponent,
+ Profiler,
+ SuspenseComponent,
+ TreeRoot,
} from './VNodeTags';
import type { VNodeTag } from './VNodeTags';
-import type { RefType, ContextType, SuspenseState, Source } from '../Types';
-import type { Hook } from '../hooks/HookType';
import { InitFlag } from './VNodeFlags';
-import { Observer } from '../../inulax/proxy/Observer';
+import { VNode } from '../Types';
export const BELONG_CLASS_VNODE_KEY = typeof Symbol === 'function' ? Symbol('belongClassVNode') : 'belongClassVNode';
-export class VNode {
- tag: VNodeTag;
- key: string | null; // 唯一标识符
- props: any; // 传给组件的props的值,类组件包含defaultProps,Lazy组件不包含
- type: any = null;
- realNode: any; // 如果是类,则存放实例;如果是div这种,则存放真实DOM;
+export function VirtualNode(tag: VNodeTag, props: any, key: null | string, realNode) {
+ this.tag = tag; // 对应组件的类型,比如ClassComponent等
+ // 唯一标识符
+ // if (isReactiveObj(this.key)) {
+ // this.key = getReactiveValue(this.key);
+ // subscribeKeyEffect(this, this.key);
+ // } else {
+ this.key = key;
+ // }
+
+ this.props = props; // 传给组件的props的值,类组件包含defaultProps,Lazy组件不包含
+ this.type = null;
+ this.realNode = null; // 如果是类,则存放实例;如果是div这种,则存放真实DOM;
// 关系结构
- parent: VNode | null = null; // 父节点
- child: VNode | null = null; // 子节点
- next: VNode | null = null; // 兄弟节点
- cIndex = 0; // 节点在children数组中的位置
- eIndex = 0; // InulaElement在jsx中的位置,例如:jsx中的null不会生成vNode,所以eIndex和cIndex不一致
+ this.parent = null; // 父节点
+ this.child = null; // 子节点
+ this.next = null; // 兄弟节点
+ this.cIndex = 0; // 节点在children数组中的位置
+ this.eIndex = 0; // HorizonElement在jsx中的位置,例如:jsx中的null不会生成vNode,所以eIndex和cIndex不一致
- ref: RefType | ((handle: any) => void) | null = null; // 包裹一个函数,submit阶段使用,比如将外部useRef生成的对象赋值到ref上
- oldProps: any = null;
+ this.ref = null; // 包裹一个函数,submit阶段使用,比如将外部useRef生成的对象赋值到ref上
+ this.oldProps = null;
- // 是否已经被从树上移除
- isCleared = false;
- changeList: any; // DOM的变更列表
- effectList: any[] | null; // useEffect 的更新数组
- updates: any[] | null; // TreeRoot和ClassComponent使用的更新数组
- stateCallbacks: any[] | null; // 存放存在setState的第二个参数和InulaDOM.render的第三个参数所在的node数组
- isForceUpdate: boolean; // 是否使用强制更新
- isSuspended = false; // 是否被suspense打断更新
- state: any; // ClassComponent和TreeRoot的状态
- hooks: Array> | null; // 保存hook
- depContexts: Array> | null; // FunctionComponent和ClassComponent对context的依赖列表
- isDepContextChange: boolean; // context是否变更
- dirtyNodes: Array | null = null; // 需要改动的节点数组
- shouldUpdate = false;
- childShouldUpdate = false;
- task: any;
+ this.dirtyNodes = null; // 需要改动的节点数组
+ this.shouldUpdate = false;
+ this.childShouldUpdate = false;
- // 使用这个变量来记录修改前的值,用于恢复。
- context: any;
- // 因为LazyComponent会修改tag和type属性,为了能识别,增加一个属性
- isLazyComponent: boolean;
+ this.flags = InitFlag;
+ this.clearChild = null;
+ this.isCreated = true;
+ this.oldRef = null;
+ this.oldChild = null;
- // 因为LazyComponent会修改type属性,为了在diff中判断是否可以复用,需要增加一个lazyType
- lazyType: any;
- flags = InitFlag;
- clearChild: VNode | null;
- // one tree相关属性
- isCreated = true;
- oldHooks: Array> | null; // 保存上一次执行的hook
- oldState: any;
- oldRef: RefType | ((handle: any) => void) | null = null;
- oldChild: VNode | null = null;
- promiseResolve: boolean; // suspense的promise是否resolve
- devProps: any; // 用于dev插件临时保存更新props值
- suspenseState: SuspenseState | null;
-
- path = ''; // 保存从根到本节点的路径
-
- // 根节点数据
- toUpdateNodes: Set | null; // 保存要更新的节点
- delegatedEvents: Set;
-
- // @ts-ignore
- [BELONG_CLASS_VNODE_KEY]: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用
-
- // 状态管理器InulaX使用
- isStoreChange: boolean;
- observers: Set | null = null; // 记录这个函数组件/类组件依赖哪些Observer
- classComponentWillUnmount: ((vNode: VNode) => any) | null; // InulaX会在classComponentWillUnmount中清除对VNode的引入用
- src: Source | null; // 节点所在代码位置
-
- constructor(tag: VNodeTag, props: any, key: null | string, realNode) {
- this.tag = tag; // 对应组件的类型,比如ClassComponent等
- this.key = key;
-
- this.props = props;
-
- switch (tag) {
- case TreeRoot:
- this.realNode = realNode;
- this.task = null;
- this.toUpdateNodes = new Set();
- this.delegatedEvents = new Set();
- this.updates = null;
- this.stateCallbacks = null;
- this.state = null;
- this.oldState = null;
- this.context = null;
- break;
- case FunctionComponent:
- this.realNode = null;
- this.effectList = null;
- this.hooks = null;
- this.depContexts = null;
- this.isDepContextChange = false;
- this.oldHooks = null;
- this.isStoreChange = false;
- this.observers = null;
- this.classComponentWillUnmount = null;
- this.src = null;
- break;
- case ClassComponent:
- this.realNode = null;
- this.updates = null;
- this.stateCallbacks = null;
- this.isForceUpdate = false;
- this.state = null;
- this.depContexts = null;
- this.isDepContextChange = false;
- this.oldState = null;
- this.context = null;
- this.isStoreChange = false;
- this.observers = null;
- this.classComponentWillUnmount = null;
- this.src = null;
- break;
- case DomPortal:
- this.realNode = null;
- this.context = null;
- this.delegatedEvents = new Set();
- this.src = null;
- break;
- case DomComponent:
- this.realNode = null;
- this.changeList = null;
- this.context = null;
- this.src = null;
- break;
- case DomText:
- this.realNode = null;
- break;
- case SuspenseComponent:
- this.realNode = null;
- this.suspenseState = {
- promiseSet: null,
- didCapture: false,
- promiseResolved: false,
- oldChildStatus: '',
- childStatus: '',
- };
- this.src = null;
- break;
- case ContextProvider:
- this.src = null;
- this.context = null;
- break;
- case MemoComponent:
- this.effectList = null;
- this.src = null;
- break;
- case LazyComponent:
- this.realNode = null;
- this.stateCallbacks = null;
- this.isLazyComponent = true;
- this.lazyType = null;
- this.updates = null;
- this.src = null;
- break;
- case Fragment:
- break;
- case ContextConsumer:
- break;
- case ForwardRef:
- break;
- case Profiler:
- break;
- default:
- break;
- }
+ switch (tag) {
+ case TreeRoot:
+ this.realNode = realNode;
+ this.task = null;
+ this.toUpdateNodes = new Set();
+ this.delegatedEvents = new Set();
+ this.updates = null;
+ this.stateCallbacks = null;
+ this.state = null;
+ this.oldState = null;
+ this.context = null;
+ break;
+ case FunctionComponent:
+ this.realNode = null;
+ this.effectList = null;
+ this.hooks = null;
+ this.depContexts = null;
+ this.isDepContextChange = false;
+ this.oldHooks = null;
+ this.isStoreChange = false;
+ this.observers = null;
+ this.classComponentWillUnmount = null;
+ this.src = null;
+ this.compRContext = null;
+ break;
+ case ClassComponent:
+ this.realNode = null;
+ this.updates = null;
+ this.stateCallbacks = null;
+ this.isForceUpdate = false;
+ this.state = null;
+ this.depContexts = null;
+ this.isDepContextChange = false;
+ this.oldState = null;
+ this.context = null;
+ this.isStoreChange = false;
+ this.observers = null;
+ this.classComponentWillUnmount = null;
+ this.src = null;
+ this.compRContext = null;
+ break;
+ case DomPortal:
+ this.realNode = null;
+ this.context = null;
+ this.delegatedEvents = new Set();
+ this.src = null;
+ break;
+ case DomComponent:
+ this.realNode = null;
+ this.changeList = null;
+ this.context = null;
+ this.src = null;
+ this.attrRContexts = null;
+ break;
+ case DomText:
+ this.realNode = null;
+ break;
+ case SuspenseComponent:
+ this.realNode = null;
+ this.suspenseState = {
+ promiseSet: null,
+ didCapture: false,
+ promiseResolved: false,
+ oldChildStatus: '',
+ childStatus: '',
+ };
+ this.src = null;
+ break;
+ case ContextProvider:
+ this.src = null;
+ this.context = null;
+ break;
+ case MemoComponent:
+ this.effectList = null;
+ this.src = null;
+ break;
+ case LazyComponent:
+ this.realNode = null;
+ this.stateCallbacks = null;
+ this.isLazyComponent = true;
+ this.lazyType = null;
+ this.updates = null;
+ this.src = null;
+ break;
+ case Fragment:
+ break;
+ case ContextConsumer:
+ break;
+ case ForwardRef:
+ break;
+ case Profiler:
+ break;
}
}
diff --git a/packages/inula/src/renderer/vnode/VNodeCreator.ts b/packages/inula/src/renderer/vnode/VNodeCreator.ts
index 142bac33..21b09cb2 100644
--- a/packages/inula/src/renderer/vnode/VNodeCreator.ts
+++ b/packages/inula/src/renderer/vnode/VNodeCreator.ts
@@ -29,6 +29,7 @@ import {
LazyComponent,
MemoComponent,
SuspenseComponent,
+ ReactiveComponent,
} from './VNodeTags';
import {
TYPE_CONTEXT,
@@ -41,8 +42,8 @@ import {
TYPE_STRICT_MODE,
TYPE_SUSPENSE,
} from '../../external/JSXElementType';
-import { VNode } from './VNode';
-import { JSXElement, Source } from '../Types';
+import { VirtualNode } from './VNode';
+import { JSXElement, Source, VNode } from '../Types';
import { markVNodePath } from '../utils/vNodePath';
const typeLazyMap = {
@@ -57,7 +58,7 @@ const typeMap = {
};
function newVirtualNode(tag: VNodeTag, key?: null | string, vNodeProps?: any, realNode?: any): VNode {
- return new VNode(tag, vNodeProps, key as null | string, realNode);
+ return new VirtualNode(tag, vNodeProps, key as null | string, realNode);
}
function isClassComponent(comp: Function) {
@@ -106,6 +107,12 @@ export function createFragmentVNode(fragmentKey, fragmentProps) {
return vNode;
}
+export function createReactiveVNode(content) {
+ const vNode = newVirtualNode(ReactiveComponent, null, content);
+ vNode.shouldUpdate = true;
+ return vNode;
+}
+
export function createDomTextVNode(content) {
const vNode = newVirtualNode(DomText, null, content);
vNode.shouldUpdate = true;
@@ -231,32 +238,32 @@ export function onlyUpdateChildVNodes(processing: VNode): VNode | null {
}
// 当跳过子树更新时,父节点path更新时,需要更新所有子树path
- if (processing.child && processing.path !== processing.child.path.slice(0, processing.path.length)) {
- // bfs更新子树path
- const queue: VNode[] = [];
-
- const putChildrenIntoQueue = (vNode: VNode) => {
- const child = vNode.child;
- if (child) {
- queue.push(child);
- let sibling = child.next;
- while (sibling) {
- queue.push(sibling);
- sibling = sibling.next;
- }
- }
- };
-
- putChildrenIntoQueue(processing);
-
- while (queue.length) {
- const vNode = queue.shift()!;
-
- markVNodePath(vNode);
-
- putChildrenIntoQueue(vNode);
- }
- }
+ // if (processing.child && processing.path !== processing.child.path.slice(0, processing.path.length)) {
+ // // bfs更新子树path
+ // const queue: VNode[] = [];
+ //
+ // const putChildrenIntoQueue = (vNode: VNode) => {
+ // const child = vNode.child;
+ // if (child) {
+ // queue.push(child);
+ // let sibling = child.next;
+ // while (sibling) {
+ // queue.push(sibling);
+ // sibling = sibling.next;
+ // }
+ // }
+ // };
+ //
+ // putChildrenIntoQueue(processing);
+ //
+ // while (queue.length) {
+ // const vNode = queue.shift()!;
+ //
+ // markVNodePath(vNode);
+ //
+ // putChildrenIntoQueue(vNode);
+ // }
+ // }
// 子树无需工作
return null;
}
diff --git a/packages/inula/src/renderer/vnode/VNodeTags.ts b/packages/inula/src/renderer/vnode/VNodeTags.ts
index dc358aef..c824ab75 100644
--- a/packages/inula/src/renderer/vnode/VNodeTags.ts
+++ b/packages/inula/src/renderer/vnode/VNodeTags.ts
@@ -33,3 +33,4 @@ export const SuspenseComponent = 'SuspenseComponent';
export const MemoComponent = 'MemoComponent';
export const LazyComponent = 'LazyComponent';
export const IncompleteClassComponent = 'IncompleteClassComponent';
+export const ReactiveComponent = 'ReactiveComponent';
diff --git a/packages/inula/src/renderer/vnode/VNodeUtils.ts b/packages/inula/src/renderer/vnode/VNodeUtils.ts
index 2d32beaf..25cb2753 100644
--- a/packages/inula/src/renderer/vnode/VNodeUtils.ts
+++ b/packages/inula/src/renderer/vnode/VNodeUtils.ts
@@ -19,7 +19,7 @@
import type { VNode } from '../Types';
-import { DomComponent, DomPortal, DomText, TreeRoot } from './VNodeTags';
+import { DomComponent, DomPortal, DomText, TreeRoot, ReactiveComponent } from './VNodeTags';
import { getNearestVNode } from '../../dom/DOMInternalKeys';
import { Addition, InitFlag } from './VNodeFlags';
import { BELONG_CLASS_VNODE_KEY } from './VNode';
@@ -129,15 +129,22 @@ export function clearVNode(vNode: VNode) {
const hook = window.__INULA_DEV_HOOK__;
hook.deleteVNode(vNode);
}
+
+ if (vNode.attrRContexts) {
+ vNode.attrRContexts = null;
+ }
+ if (vNode.compRContext) {
+ vNode.compRContext = null;
+ }
}
// 是dom类型的vNode
export function isDomVNode(node: VNode) {
- return node.tag === DomComponent || node.tag === DomText;
+ return node.tag === DomComponent || node.tag === DomText || node.tag === ReactiveComponent;
}
// 是容器类型的vNode
-function isDomContainer(vNode: VNode): boolean {
+export function isDomContainer(vNode: VNode): boolean {
return vNode.tag === DomComponent || vNode.tag === TreeRoot || vNode.tag === DomPortal;
}
@@ -277,3 +284,15 @@ export function findRoot(targetVNode, targetDom) {
}
return targetVNode;
}
+
+export function findDOMContainer(vNode: VNode): VNode {
+ let parent = vNode.parent;
+ while (parent !== null) {
+ if (isDomContainer(parent)) {
+ break;
+ }
+ parent = parent.parent;
+ }
+
+ return parent as VNode;
+}