diff --git a/packages/inula/__tests__/HorizonXTest/ReactiveTest/render.test.tsx b/packages/inula/__tests__/HorizonXTest/ReactiveTest/render.test.tsx
new file mode 100644
index 00000000..5d82a087
--- /dev/null
+++ b/packages/inula/__tests__/HorizonXTest/ReactiveTest/render.test.tsx
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2024 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 { render, act, useReactive, useReference, useComputed, useWatch } from '../../../src';
+import { Text, triggerClickEvent } from '../../jest/commonComponents';
+import * as Inula from '../../../src';
+
+describe('test reactive in FunctionComponent', () => {
+ const { unmountComponentAtNode } = Inula;
+ let container: HTMLElement | null = null;
+ beforeEach(() => {
+ // 创建一个 DOM 元素作为渲染目标
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ // 退出时进行清理
+ unmountComponentAtNode(container);
+ container?.remove();
+ container = null;
+ });
+
+ it('should support useReactive in FunctionComponent', () => {
+ const fn = jest.fn();
+
+ function App(props) {
+ fn();
+
+ const reactiveObj = useReactive({
+ persons: [
+ { name: 'p1', age: 1 },
+ { name: 'p2', age: 2 },
+ ],
+ });
+
+ const newPerson = { name: 'p3', age: 3 };
+ const addOnePerson = function () {
+ reactiveObj.persons.push(newPerson);
+ };
+ const delOnePerson = function () {
+ reactiveObj.persons.pop();
+ };
+
+ return (
+
+
+
+
+
+ );
+ }
+
+ render(, container);
+
+ expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
+ act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3');
+
+ act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
+
+ expect(fn).toHaveBeenCalledTimes(3);
+ });
+
+ it('should support ref object in FunctionComponent', () => {
+ const fn = jest.fn();
+ function App(props) {
+ fn();
+ const refObj = useReference({
+ persons: [
+ { name: 'p1', age: 1 },
+ { name: 'p2', age: 2 },
+ ],
+ });
+
+ const newPerson = { name: 'p3', age: 3 };
+ const addOnePerson = function () {
+ refObj.value.persons.push(newPerson);
+ };
+ const delOnePerson = function () {
+ refObj.value.persons.pop();
+ };
+
+ return (
+
+
+
+
+
+ );
+ }
+
+ render(, container);
+
+ expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
+ act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3');
+
+ act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
+
+ expect(fn).toHaveBeenCalledTimes(3);
+ });
+
+ it('should support ref primitive in FunctionComponent', () => {
+ const fn = jest.fn();
+ function App(props) {
+ fn();
+ const refObj = useReference(2);
+
+ const add = function () {
+ refObj.value++;
+ };
+ const del = function () {
+ refObj.value--;
+ };
+
+ return (
+
+
+
+
+
+ );
+ }
+
+ render(, container);
+
+ expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
+ // 在Array中增加一个对象
+ act(() => {
+ triggerClickEvent(container, 'addBtn');
+ });
+ expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3');
+
+ act(() => {
+ triggerClickEvent(container, 'delBtn');
+ });
+ expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
+
+ expect(fn).toHaveBeenCalledTimes(3);
+ });
+
+ it('should support useComputed in FunctionComponent', () => {
+ const fn = jest.fn();
+ function App(props) {
+ const data = useReactive<{ bar?: string }>({});
+ const computedData = useComputed(() => {
+ fn();
+ return data.bar;
+ });
+
+ const setText = function () {
+ data.bar = 'bar';
+ };
+
+ return (
+
+
+
+
+ );
+ }
+
+ render(, container);
+
+ expect(container?.querySelector('#text')?.innerHTML).toBe('');
+ act(() => {
+ triggerClickEvent(container, 'setText');
+ });
+ expect(container?.querySelector('#text')?.innerHTML).toBe('bar');
+
+ expect(fn).toHaveBeenCalledTimes(2);
+ });
+
+ it('should support useWatch in FunctionComponent', () => {
+ const fn = jest.fn();
+ function App(props) {
+ let dummy;
+ const counter = useReactive({ num: 0 });
+ useWatch(() => {
+ fn();
+ dummy = counter.num;
+ });
+
+ const updateCounter = function () {
+ counter.num++;
+ };
+
+ return (
+
+
+
+
+ );
+ }
+
+ render(, container);
+
+ expect(container?.querySelector('#text')?.innerHTML).toBe('0');
+ act(() => {
+ triggerClickEvent(container, 'updateCounter');
+ });
+ expect(container?.querySelector('#text')?.innerHTML).toBe('1');
+
+ expect(fn).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/packages/inula/__tests__/HorizonXTest/ReactiveTest/watch.test.tsx b/packages/inula/__tests__/HorizonXTest/ReactiveTest/watch.test.tsx
new file mode 100644
index 00000000..e9084017
--- /dev/null
+++ b/packages/inula/__tests__/HorizonXTest/ReactiveTest/watch.test.tsx
@@ -0,0 +1,336 @@
+/*
+ * Copyright (c) 2024 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 { ref, reactive, watch, computed } from '../../../src';
+
+describe('test watch', () => {
+ it('should watch effect', async () => {
+ const state = reactive({ count: 0 });
+ let dummy;
+ watch(() => {
+ dummy = state.count;
+ });
+ expect(dummy).toBe(0);
+
+ state.count++;
+ expect(dummy).toBe(1);
+ });
+
+ it('should watching single source: getter', async () => {
+ const state = reactive({ count: 0 });
+ let dummy;
+ watch(
+ () => state.count,
+ (count, prevCount) => {
+ dummy = [count, prevCount];
+ // assert types
+ count + 1;
+ if (prevCount) {
+ prevCount + 1;
+ }
+ }
+ );
+ state.count++;
+ expect(dummy).toMatchObject([1, 0]);
+ });
+
+ it('should watching single source: ref', async () => {
+ const count = ref(0);
+ let dummy;
+ const spy = jest.fn();
+ watch(count, (count, prevCount) => {
+ spy();
+ dummy = [count, prevCount];
+ });
+ count.value++;
+ expect(dummy).toMatchObject([1, 0]);
+ expect(spy).toBeCalledTimes(1);
+ });
+
+ it('watching single source: array', async () => {
+ const array = reactive([]);
+ const spy = jest.fn((val, prevVal) => {
+ let a = 1;
+ });
+ watch(array, spy);
+ array.push(1);
+
+ // push会触发两次spy(一次是push,一次是length)
+ expect(spy).toBeCalledTimes(2);
+ });
+
+ it('should not fire if watched getter result did not change', async () => {
+ const spy = jest.fn();
+ const n = ref(0);
+ watch(() => n.value % 2, spy);
+
+ n.value++;
+ expect(spy).toBeCalledTimes(1);
+
+ n.value += 2;
+ // should not be called again because getter result did not change
+ expect(spy).toBeCalledTimes(1);
+ });
+
+ it('watching single source: computed ref', async () => {
+ const count = ref(0);
+ const plus = computed(() => count.value + 1);
+ let dummy;
+ watch(plus, (count, prevCount) => {
+ dummy = [count, prevCount];
+ // assert types
+ count + 1;
+ if (prevCount) {
+ prevCount + 1;
+ }
+ });
+ count.value++;
+ expect(dummy).toMatchObject([2, 1]);
+ });
+
+ it('watching primitive with deep: true', async () => {
+ const count = ref(0);
+ let dummy;
+ watch(count, (c, prevCount) => {
+ dummy = [c, prevCount];
+ });
+ count.value++;
+ expect(dummy).toMatchObject([1, 0]);
+ });
+
+ it('directly watching reactive object (with automatic deep: true)', async () => {
+ const src = reactive({
+ count: 0,
+ });
+ let dummy;
+ watch(src, ({ count }) => {
+ dummy = count;
+ });
+ src.count++;
+ expect(dummy).toBe(1);
+ });
+
+ it('directly watching reactive object with explicit deep: true', async () => {
+ const src = reactive({
+ state: {
+ count: 0,
+ },
+ });
+ let dummy;
+ watch(src, ({ state }) => {
+ dummy = state?.count;
+ });
+
+ // nested should not trigger
+ src.state.count++;
+ expect(dummy).toBe(1);
+
+ // root level should trigger
+ src.state = { count: 2 };
+ expect(dummy).toBe(2);
+ });
+
+ it('watching multiple sources', async () => {
+ const spy = jest.fn();
+ const state = reactive({ count: 1 });
+ const count = ref(1);
+ const plus = computed(() => count.value + 1);
+
+ let dummy;
+ watch([() => state.count, count, plus], (vals, oldVals) => {
+ spy();
+ dummy = [vals, oldVals];
+ // assert types
+ vals.concat(1);
+ oldVals.concat(1);
+ });
+
+ state.count++;
+ expect(dummy).toMatchObject([
+ [2, 1, 2],
+ [1, 1, 2],
+ ]);
+ expect(spy).toBeCalledTimes(1);
+
+ count.value++;
+ // count触发一次,plus触发一次
+ expect(spy).toBeCalledTimes(3);
+ });
+
+ it('watching multiple sources: readonly array', async () => {
+ const state = reactive({ count: 1 });
+ const status = ref(false);
+
+ let dummy;
+ watch([() => state.count, status] as const, (vals, oldVals) => {
+ dummy = [vals, oldVals];
+ const [count] = vals;
+ const [, oldStatus] = oldVals;
+ // assert types
+ count + 1;
+ oldStatus === true;
+ });
+
+ state.count++;
+ expect(dummy).toMatchObject([
+ [2, false],
+ [1, false],
+ ]);
+ status.value = true;
+ expect(dummy).toMatchObject([
+ [2, true],
+ [2, false],
+ ]);
+ });
+
+ it('watching multiple sources: reactive object (with automatic deep: true)', async () => {
+ const src = reactive({ count: 0 });
+ let dummy;
+ watch([src], ([state]) => {
+ dummy = state;
+ // assert types
+ state.count === 1;
+ });
+ src.count++;
+ expect(dummy).toMatchObject({ count: 1 });
+ });
+
+ it('stopping the watcher (effect)', async () => {
+ const state = reactive({ count: 0 });
+ let dummy;
+ const stop = watch(() => {
+ dummy = state.count;
+ });
+ expect(dummy).toBe(0);
+
+ stop();
+ state.count++;
+ // should not update
+ expect(dummy).toBe(0);
+ });
+
+ it('stopping the watcher (with source)', async () => {
+ const state = reactive({ count: 0 });
+ let dummy;
+ const stop = watch(
+ () => state.count,
+ count => {
+ dummy = count;
+ }
+ );
+
+ state.count++;
+ expect(dummy).toBe(1);
+
+ stop();
+ state.count++;
+ // should not update
+ expect(dummy).toBe(1);
+ });
+
+ it('deep watch effect', async () => {
+ const state = reactive({
+ nested: {
+ count: 0,
+ },
+ array: [1, 2, 3],
+ map: new Map([
+ ['a', 1],
+ ['b', 2],
+ ]),
+ set: new Set([1, 2, 3]),
+ });
+
+ let dummy;
+ watch(() => {
+ dummy = [state.nested.count, state.array[0], state.map.get('a'), state.set.has(1)];
+ });
+
+ state.nested.count++;
+ expect(dummy).toEqual([1, 1, 1, true]);
+
+ // nested array mutation
+ state.array[0] = 2;
+ expect(dummy).toEqual([1, 2, 1, true]);
+
+ // nested map mutation
+ state.map.set('a', 2);
+ expect(dummy).toEqual([1, 2, 2, true]);
+
+ // nested set mutation
+ state.set.delete(1);
+ expect(dummy).toEqual([1, 2, 2, false]);
+ });
+
+ it('watching deep ref', async () => {
+ const count = ref(0);
+ const double = computed(() => count.value * 2);
+ const state = reactive([count, double]);
+
+ let dummy;
+ watch(() => {
+ dummy = [state[0].value, state[1].value];
+ });
+
+ count.value++;
+ expect(dummy).toEqual([1, 2]);
+ });
+
+ it('warn and not respect deep option when using effect', async () => {
+ const arr = ref([1, [2]]);
+ const spy = jest.fn();
+ watch(() => {
+ spy();
+ return arr;
+ });
+ expect(spy).toHaveBeenCalledTimes(1);
+ (arr.value[1] as Array)[0] = 3;
+ expect(spy).toHaveBeenCalledTimes(1);
+ // expect(`"deep" option is only respected`).toHaveBeenWarned()
+ });
+
+ test('watchEffect should not recursively trigger itself', async () => {
+ const spy = jest.fn();
+ const price = ref(10);
+ const history = ref([]);
+ watch(() => {
+ history.value.push(price.value);
+ spy();
+ });
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ test('computed refs should not trigger watch if value has no change', async () => {
+ const spy = jest.fn();
+ const source = ref(0);
+ const price = computed(() => source.value === 0);
+ watch(price, spy);
+ source.value++;
+ source.value++;
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('watching multiple sources: computed', async () => {
+ let count = 0;
+ const value = ref('1');
+ const plus = computed(() => !!value.value);
+ watch([plus], () => {
+ count++;
+ });
+ value.value = '2';
+ expect(plus.value).toBe(true);
+ expect(count).toBe(0);
+ });
+});
diff --git a/packages/inula/__tests__/HorizonXTest/ReactiveTest/watchEffect.test.tsx b/packages/inula/__tests__/HorizonXTest/ReactiveTest/watchEffect.test.tsx
index 4616009b..1ed81541 100644
--- a/packages/inula/__tests__/HorizonXTest/ReactiveTest/watchEffect.test.tsx
+++ b/packages/inula/__tests__/HorizonXTest/ReactiveTest/watchEffect.test.tsx
@@ -21,7 +21,7 @@ function stop(stopHandle: () => void) {
describe('test watchEffect', () => {
it('should run the passed function once (wrapped by a effect)', () => {
- const fnSpy = jest.fn(() => {});
+ const fnSpy = jest.fn();
watchEffect(fnSpy);
expect(fnSpy).toHaveBeenCalledTimes(1);
});
diff --git a/packages/inula/__tests__/HorizonXTest/reactive/reactive.test.tsx b/packages/inula/__tests__/HorizonXTest/reactive/reactive.test.tsx
deleted file mode 100644
index 7d61dbb8..00000000
--- a/packages/inula/__tests__/HorizonXTest/reactive/reactive.test.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (c) 2024 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, watchEffect } from '../../../src/index';
-
-describe('reactive', () => {
- it('should trigger when delete array item', () => {
- const fn = jest.fn();
- const arr = reactive([1, 2, 3]);
- watchEffect(() => {
- fn();
- arr.length;
- });
- expect(arr.length).toBe(3);
-
- // 用shift删除数组元素,代码:
- arr.shift();
-
- // Even if the array element is deleted, the array length will not change
- expect(arr.length).toBe(2);
- expect(fn).toHaveBeenCalledTimes(2);
- });
-});
diff --git a/packages/inula/src/index.ts b/packages/inula/src/index.ts
index a802bc60..54fc09b5 100644
--- a/packages/inula/src/index.ts
+++ b/packages/inula/src/index.ts
@@ -56,11 +56,11 @@ import {
isPortal,
} from './external/InulaIs';
import { createStore, useStore, clearStore } from './inulax/store/StoreHandler';
-import { reactive, toRaw } from './inulax/reactive/Reactive';
-import { ref, isRef, unref, shallowRef } from './inulax/reactive/Ref';
+import { reactive, useReactive, toRaw } from './inulax/reactive/Reactive';
+import { ref, useReference, isRef, unref, shallowRef } from './inulax/reactive/Ref';
import * as reduxAdapter from './inulax/adapters/redux';
-import { watch, watchEffect } from './inulax/reactive/Watch';
-import { computed } from './inulax/reactive/Computed';
+import { watch, watchEffect, useWatch } from './inulax/reactive/Watch';
+import { computed, useComputed } from './inulax/reactive/Computed';
import { act } from './external/TestUtil';
import {
@@ -127,14 +127,18 @@ const Inula = {
Suspense,
// vue reactive api
ref,
+ useReference,
isRef,
unref,
shallowRef,
reactive,
+ useReactive,
isReactive,
isShallow,
computed,
+ useComputed,
watchEffect,
+ useWatch,
toRaw,
};
@@ -194,14 +198,18 @@ export {
Suspense,
// vue reactive api
ref,
+ useReference,
isRef,
unref,
shallowRef,
reactive,
+ useReactive,
isReactive,
isShallow,
computed,
+ useComputed,
watchEffect,
+ useWatch,
toRaw,
};
diff --git a/packages/inula/src/inulax/CommonUtils.ts b/packages/inula/src/inulax/CommonUtils.ts
index 64a0fe96..956e6ed9 100644
--- a/packages/inula/src/inulax/CommonUtils.ts
+++ b/packages/inula/src/inulax/CommonUtils.ts
@@ -14,6 +14,7 @@
*/
import { KeyTypes, ReactiveFlags } from './Constants';
+import { Mutation } from './types/ProxyTypes';
export function isObject(obj: any): boolean {
const type = typeof obj;
@@ -133,21 +134,24 @@ export function getDetailedType(val: any) {
return typeof val;
}
-export function resolveMutation(from, to) {
+export function resolveMutation(
+ from: T,
+ to: T
+): Mutation {
if (getDetailedType(from) !== getDetailedType(to)) {
return { mutation: true, from, to };
}
switch (getDetailedType(from)) {
case 'array': {
- const len = Math.max(from.length, to.length);
+ const len = Math.max(from.length ?? 0, to.length ?? 0);
const res: any[] = [];
let found = false;
for (let i = 0; i < len; i++) {
- if (from.length <= i) {
+ if ((from.length ?? 0) <= i) {
res[i] = { mutation: true, to: to[i] };
found = true;
- } else if (to.length <= i) {
+ } else if ((to.length ?? 0) <= i) {
res[i] = { mutation: true, from: from[i] };
found = true;
} else {
diff --git a/packages/inula/src/inulax/docs/reactive_feature.md b/packages/inula/src/inulax/docs/reactive_feature.md
index bfff3176..20bd99ea 100644
--- a/packages/inula/src/inulax/docs/reactive_feature.md
+++ b/packages/inula/src/inulax/docs/reactive_feature.md
@@ -185,3 +185,58 @@ it('should no longer update when stopped', () => {
expect(dummy).toBe(1);
});
```
+
+watch接口差异:
+1、不支持deep,可以只传一个函数,那样会自动跟踪。
+```js
+it('deep', async () => {
+ const state = reactive({
+ nested: {
+ count: ref(0),
+ },
+ array: [1, 2, 3],
+ map: new Map([
+ ['a', 1],
+ ['b', 2],
+ ]),
+ set: new Set([1, 2, 3]),
+ });
+
+ let dummy;
+ watch(
+ () => state,
+ state => {
+ dummy = [
+ state.nested.count,
+ state.array[0],
+ state.map.get('a'),
+ state.set.has(1),
+ ]
+ }
+ )
+
+ state.nested.count++;
+ expect(dummy).toEqual(undefined);
+
+ // 改成:
+ watch(
+ () => {
+ dummy = [
+ state.nested.count,
+ state.array[0],
+ state.map.get('a'),
+ state.set.has(1),
+ ]
+ }
+ )
+});
+```
+2、不支持immediate。
+```js
+it('immediate', async () => {
+ const count = ref(0);
+ const cb = vi.fn();
+ watch(count, cb, { immediate: true });
+ expect(cb).toHaveBeenCalledTimes(0);
+ })
+```
diff --git a/packages/inula/src/inulax/proxy/Observer.ts b/packages/inula/src/inulax/proxy/Observer.ts
index 894dcd1a..40af77f5 100644
--- a/packages/inula/src/inulax/proxy/Observer.ts
+++ b/packages/inula/src/inulax/proxy/Observer.ts
@@ -20,7 +20,7 @@ import { devtools } from '../devtools';
import { KeyTypes } from '../Constants';
import { addRContext, RContextSet } from '../reactive/RContext';
-import { IObserver } from '../types/ProxyTypes';
+import { IObserver, Listener, Mutation } from '../types/ProxyTypes';
/**
* 一个对象(对象、数组、集合)对应一个Observer
@@ -30,7 +30,7 @@ export class Observer implements IObserver {
keyVNodes = new Map();
- listeners: ((mutation) => void)[] = [];
+ listeners: Listener[] = [];
watchers = {};
@@ -71,7 +71,7 @@ export class Observer implements IObserver {
}
// 对象的属性被赋值时调用
- setProp(key: string | symbol, mutation: any, oldValue?: any, newValue?: any): void {
+ setProp(key: string | symbol, mutation: Mutation, oldValue?: any, newValue?: any): void {
const vNodes = this.keyVNodes.get(key);
// NOTE: using Set directly can lead to deadlock
const vNodeArray = Array.from(vNodes || []);
@@ -115,11 +115,11 @@ export class Observer implements IObserver {
launchUpdateFromVNode(vNode);
}
- addListener(listener: (mutation) => void): void {
+ addListener(listener: Listener): void {
this.listeners.push(listener);
}
- removeListener(listener: (mutation) => void): void {
+ removeListener(listener: Listener): void {
this.listeners = this.listeners.filter(item => item != listener);
}
diff --git a/packages/inula/src/inulax/proxy/ProxyHandler.ts b/packages/inula/src/inulax/proxy/ProxyHandler.ts
index c5a84cda..5c0b2913 100644
--- a/packages/inula/src/inulax/proxy/ProxyHandler.ts
+++ b/packages/inula/src/inulax/proxy/ProxyHandler.ts
@@ -65,7 +65,7 @@ export function createProxy(rawObj: any, listener?: CurrentListener, isDeepProxy
deepProxyMap.set(rawObj, isDeepProxy);
// 创建Proxy
- let proxyObj;
+ let proxyObj: ProxyHandler;
if (!isDeepProxy) {
proxyObj = createObjectProxy(
rawObj,
diff --git a/packages/inula/src/inulax/proxy/handlers/ArrayProxyHandler.ts b/packages/inula/src/inulax/proxy/handlers/ArrayProxyHandler.ts
index fa14c917..4beee71c 100644
--- a/packages/inula/src/inulax/proxy/handlers/ArrayProxyHandler.ts
+++ b/packages/inula/src/inulax/proxy/handlers/ArrayProxyHandler.ts
@@ -15,9 +15,9 @@
import { registerListener } from './HandlerUtils';
import { baseSetFun, baseGetFun } from './BaseObjectHandler';
-import { CurrentListener, Listeners } from '../../types/ProxyTypes';
+import { CurrentListener, Listeners, ObjectType } from '../../types/ProxyTypes';
-export function createArrayProxy(rawObj: T, listener: CurrentListener) {
+export function createArrayProxy(rawObj: T, listener: CurrentListener): ProxyHandler {
const listeners: Listeners = [];
function get(rawObj: T, key: KeyType, receiver: any) {
@@ -31,5 +31,5 @@ export function createArrayProxy(rawObj: T, listener: CurrentLi
registerListener(rawObj, listener, listeners);
- return new Proxy(rawObj, handler);
+ return new Proxy(rawObj as ObjectType, handler);
}
diff --git a/packages/inula/src/inulax/proxy/handlers/BaseObjectHandler.ts b/packages/inula/src/inulax/proxy/handlers/BaseObjectHandler.ts
index d5a7eb92..f01bb729 100644
--- a/packages/inula/src/inulax/proxy/handlers/BaseObjectHandler.ts
+++ b/packages/inula/src/inulax/proxy/handlers/BaseObjectHandler.ts
@@ -34,7 +34,7 @@ export function baseSetFun(rawObj: any[], key: string, value: any, receiver: any
}
const oldLength = isArr ? rawObj.length : 0;
- const oldJSON = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
+ const oldObj = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
const hadKey =
isArr && isValidIntegerKey(key) ? Number(key) < rawObj.length : Object.prototype.hasOwnProperty.call(rawObj, key);
@@ -45,7 +45,7 @@ export function baseSetFun(rawObj: any[], key: string, value: any, receiver: any
const observer = getObserver(rawObj);
if (!isSame(newValue, oldValue)) {
- const mutation = isPanelActive() ? resolveMutation(oldJSON, rawObj) : resolveMutation(null, rawObj);
+ const mutation = resolveMutation(oldObj, rawObj);
// 触发属性变化
observer.setProp(key, mutation, oldValue, newValue);
@@ -145,14 +145,14 @@ export function has(rawObj: T, key: KeyType) {
}
export function deleteProperty(rawObj: T, key: KeyType) {
- const oldObject = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
+ const oldObj = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
const observer = getObserver(rawObj);
const oldValue = rawObj[key];
const newValue = undefined;
const ret = Reflect.deleteProperty(rawObj, key);
- const mutation = isPanelActive() ? resolveMutation(oldObject, rawObj) : resolveMutation(null, rawObj);
+ const mutation = resolveMutation(oldObj, rawObj);
if (!isSame(newValue, oldValue)) {
observer.setProp(key, mutation, oldValue, newValue);
diff --git a/packages/inula/src/inulax/proxy/handlers/MapProxy.ts b/packages/inula/src/inulax/proxy/handlers/MapProxy.ts
index 1dd62606..a2af5c93 100644
--- a/packages/inula/src/inulax/proxy/handlers/MapProxy.ts
+++ b/packages/inula/src/inulax/proxy/handlers/MapProxy.ts
@@ -19,11 +19,11 @@ import { resolveMutation } from '../../CommonUtils';
import { KeyTypes } from '../../Constants';
import { getValOrProxy, registerListener } from './HandlerUtils';
import { baseDeleteFun, baseHasFun, baseForEach, baseGetFun, baseClearFun } from './BaseCollectionHandler';
-import { CurrentListener, Listeners } from '../../types/ProxyTypes';
+import { CurrentListener, Listeners, ObjectType } from '../../types/ProxyTypes';
type IteratorTypes = 'keys' | 'values' | 'entries';
-export function createMapProxy>(rawObj: T, listener: CurrentListener): Record {
+export function createMapProxy>(rawObj: T, listener: CurrentListener): ProxyHandler {
const listeners: Listeners = [];
// 场景:let obj = {}; map.set(obj, val);
// 满足两个UT:1、map.has(Array.from(map.keys())[0])为true; 2、map.has(obj)为true;
@@ -198,5 +198,5 @@ export function createMapProxy>(rawObj: T, listener: Cur
registerListener(rawObj, listener, listeners);
- return new Proxy(rawObj, handler);
+ return new Proxy(rawObj as ObjectType, handler);
}
diff --git a/packages/inula/src/inulax/proxy/handlers/ObjectProxyHandler.ts b/packages/inula/src/inulax/proxy/handlers/ObjectProxyHandler.ts
index ae2c77f0..dfb8eef5 100644
--- a/packages/inula/src/inulax/proxy/handlers/ObjectProxyHandler.ts
+++ b/packages/inula/src/inulax/proxy/handlers/ObjectProxyHandler.ts
@@ -17,7 +17,11 @@ import { registerListener } from './HandlerUtils';
import { baseSetFun, baseGetFun, has, deleteProperty, ownKeys } from './BaseObjectHandler';
import { CurrentListener, KeyType, Listeners, ObjectType } from '../../types/ProxyTypes';
-export function createObjectProxy(rawObj: T, listener: CurrentListener, singleLevel = false) {
+export function createObjectProxy(
+ rawObj: T,
+ listener: CurrentListener,
+ singleLevel = false
+): ProxyHandler {
const listeners: Listeners = [];
function get(rawObj: T, key: KeyType, receiver: any): any {
diff --git a/packages/inula/src/inulax/proxy/handlers/SetProxy.ts b/packages/inula/src/inulax/proxy/handlers/SetProxy.ts
index 3e368db0..331e9031 100644
--- a/packages/inula/src/inulax/proxy/handlers/SetProxy.ts
+++ b/packages/inula/src/inulax/proxy/handlers/SetProxy.ts
@@ -109,5 +109,5 @@ export function createSetProxy>(rawObj: T, listener: CurrentL
registerListener(rawObj, listener, listeners);
- return new Proxy(rawObj, handler as any);
+ return new Proxy(rawObj, handler);
}
diff --git a/packages/inula/src/inulax/proxy/handlers/WeakMapProxy.ts b/packages/inula/src/inulax/proxy/handlers/WeakMapProxy.ts
index 0ad8fefa..bbc22501 100644
--- a/packages/inula/src/inulax/proxy/handlers/WeakMapProxy.ts
+++ b/packages/inula/src/inulax/proxy/handlers/WeakMapProxy.ts
@@ -18,10 +18,10 @@ import { isSame } from '../../CommonUtils';
import { resolveMutation } from '../../CommonUtils';
import { isPanelActive } from '../../devtools';
import { getValOrProxy, registerListener } from './HandlerUtils';
-import { CurrentListener, Listeners } from '../../types/ProxyTypes';
+import { CurrentListener, Listeners, ObjectType } from '../../types/ProxyTypes';
import { baseDeleteFun, baseGetFun } from './BaseCollectionHandler';
-export function createWeakMapProxy>(rawObj: T, listener: CurrentListener) {
+export function createWeakMapProxy>(rawObj: T, listener: CurrentListener): ProxyHandler {
const listeners: Listeners = [];
const handler = {
@@ -72,5 +72,5 @@ export function createWeakMapProxy>(rawObj: T, liste
registerListener(rawObj, listener, listeners);
- return new Proxy(rawObj, handler as any);
+ return new Proxy(rawObj as ObjectType, handler as any);
}
diff --git a/packages/inula/src/inulax/reactive/Computed.ts b/packages/inula/src/inulax/reactive/Computed.ts
index 75241a96..7d6e719c 100644
--- a/packages/inula/src/inulax/reactive/Computed.ts
+++ b/packages/inula/src/inulax/reactive/Computed.ts
@@ -16,6 +16,10 @@
import { RContext } from './RContext';
import { Observer } from '../proxy/Observer';
import { isSame } from '../CommonUtils';
+import { useRef } from '../../renderer/hooks/HookExternal';
+import { RefType } from '../types/ReactiveTypes';
+import { KeyTypes } from '../Constants';
+import { Listener } from '../types/ProxyTypes';
export type ComputedFN = (oldValue?: T) => T;
@@ -23,7 +27,16 @@ export function computed(fn: ComputedFN): ComputedImpl {
return new ComputedImpl(fn);
}
-export class ComputedImpl {
+export function useComputed(fn: ComputedFN): ComputedImpl {
+ const objRef = useRef(null);
+ if (objRef.current === null) {
+ objRef.current = new ComputedImpl(fn);
+ }
+
+ return objRef.current;
+}
+
+export class ComputedImpl {
private _value: T;
private readonly fn: ComputedFN;
private readonly rContext: RContext;
@@ -50,11 +63,19 @@ export class ComputedImpl {
this._value = this.fn(oldValue);
if (!isSame(oldValue, this._value)) {
- this.observer.setProp('value', {});
+ this.observer.setProp('value', { mutation: true, from: oldValue, to: this._value });
}
}
stop() {
this.rContext.stop();
}
+
+ [KeyTypes.ADD_LISTENER](listener: Listener) {
+ this.observer.addListener(listener);
+ }
+
+ [KeyTypes.REMOVE_LISTENER](listener: Listener) {
+ this.observer.removeListener(listener);
+ }
}
diff --git a/packages/inula/src/inulax/reactive/Reactive.ts b/packages/inula/src/inulax/reactive/Reactive.ts
index 544a334d..f24ad62c 100644
--- a/packages/inula/src/inulax/reactive/Reactive.ts
+++ b/packages/inula/src/inulax/reactive/Reactive.ts
@@ -17,12 +17,21 @@ import { createProxy } from '../proxy/ProxyHandler';
import { KeyTypes } from '../Constants';
import { ReactiveRet } from '../types/ReactiveTypes';
import { ObjectType } from '../types/ProxyTypes';
+import { registerDestroyFunction } from '../store/StoreHandler';
+import { useRef } from '../../renderer/hooks/HookExternal';
export function reactive(rawObj: T): ReactiveRet;
export function reactive(rawObj: T) {
return createProxy(rawObj);
}
+export function useReactive(rawObj: T): ReactiveRet;
+export function useReactive(rawObj: T) {
+ registerDestroyFunction();
+ const objRef = useRef(rawObj);
+ return createProxy(objRef.current);
+}
+
export function toRaw(observed: T): T {
const raw = observed && observed[KeyTypes.RAW_VALUE];
return raw ? toRaw(raw) : observed;
diff --git a/packages/inula/src/inulax/reactive/Ref.ts b/packages/inula/src/inulax/reactive/Ref.ts
index e22f62d7..00164fdd 100644
--- a/packages/inula/src/inulax/reactive/Ref.ts
+++ b/packages/inula/src/inulax/reactive/Ref.ts
@@ -16,8 +16,14 @@
import { isObject, isSame, isShallow } from '../CommonUtils';
import { reactive, toRaw } from './Reactive';
import { Observer } from '../proxy/Observer';
-import { OBSERVER_KEY } from '../Constants';
+import { KeyTypes, OBSERVER_KEY } from '../Constants';
import { MaybeRef, RefType, UnwrapRef } from '../types/ReactiveTypes';
+import { registerDestroyFunction } from '../store/StoreHandler';
+import { getProcessingVNode } from '../../renderer/GlobalVar';
+import { FunctionComponent } from '../../renderer/vnode/VNodeTags';
+import { useRef } from '../../renderer/hooks/HookExternal';
+import { createProxy } from '../proxy/ProxyHandler';
+import { Listener } from '../types/ProxyTypes';
export function ref(): RefType;
export function ref(value: T): RefType>;
@@ -25,10 +31,24 @@ export function ref(value?: unknown) {
return createRef(value, false);
}
-function createRef(rawValue: unknown, isShallow: boolean) {
+export function useReference(): RefType;
+export function useReference(value: T): RefType>;
+export function useReference(value?: unknown) {
+ registerDestroyFunction();
+
+ const objRef = useRef(null);
+ if (objRef.current === null) {
+ objRef.current = createRef(value, false);
+ }
+
+ return objRef.current;
+}
+
+function createRef(rawValue: unknown, isShallow: boolean): RefType {
if (isRef(rawValue)) {
return rawValue;
}
+
return new RefImpl(rawValue, isShallow);
}
@@ -55,15 +75,24 @@ class RefImpl {
const useDirectValue = this._isShallow || isShallow(newVal);
newVal = useDirectValue ? newVal : toRaw(newVal);
if (!isSame(newVal, this._rawValue)) {
+ const mutation = { mutation: true, from: this._rawValue, to: newVal };
this._rawValue = newVal;
this._value = useDirectValue ? newVal : toReactive(newVal);
- this.observer.setProp('value', {});
+ this.observer.setProp('value', mutation);
}
}
get [OBSERVER_KEY]() {
return this.observer;
}
+
+ [KeyTypes.ADD_LISTENER](listener: Listener) {
+ this.observer.addListener(listener);
+ }
+
+ [KeyTypes.REMOVE_LISTENER](listener: Listener) {
+ this.observer.removeListener(listener);
+ }
}
export function isRef(ref: MaybeRef): ref is RefType;
@@ -72,7 +101,7 @@ export function isRef(ref: any): ref is RefType {
}
export function toReactive(value: T): T {
- return isObject(value) ? reactive(value as Record) : value;
+ return isObject(value) ? createProxy(value) : value;
}
export function unref(ref: MaybeRef): T {
diff --git a/packages/inula/src/inulax/reactive/Watch.ts b/packages/inula/src/inulax/reactive/Watch.ts
index 86f1db09..5d861855 100644
--- a/packages/inula/src/inulax/reactive/Watch.ts
+++ b/packages/inula/src/inulax/reactive/Watch.ts
@@ -14,13 +14,70 @@
*/
import { RContext } from './RContext';
+import { useRef } from '../../renderer/hooks/HookExternal';
+import { RefType } from '../types/ReactiveTypes';
+import { WatchCallback } from '../types/ProxyTypes';
+import { computed, ComputedImpl } from './Computed';
+import { isRef } from './Ref';
+import { isArray, isReactive } from '../CommonUtils';
+import { toRaw } from './Reactive';
-export function watch(stateVariable: any, listener: (state: any) => void) {
- listener = listener.bind(null, stateVariable);
- stateVariable.addListener(listener);
+export type WatchSource = RefType | ProxyHandler | ComputedImpl | (() => T);
+
+export function watch(source: WatchSource | WatchSource[], fn: WatchCallback) {
+ if (isRef(source) || isReactive(source)) {
+ return doWatch(source, fn);
+ } else if (isArray(source)) {
+ const stops = (source as any[]).map((s, index) => {
+ return watch(s, (val, prevVal) => {
+ const vals = getSourcesValue(source);
+ const prevVals = getSourcesValue(source);
+ prevVals[index] = prevVal;
+ fn(vals, prevVals);
+ });
+ });
+
+ return () => {
+ stops.forEach(stop => stop());
+ };
+ } else if (typeof source === 'function') {
+ if (fn) {
+ return doWatch(computed(source), fn);
+ } else {
+ // no cb -> simple effect
+ const rContext = new RContext(source);
+
+ rContext.run();
+
+ return () => {
+ rContext.stop();
+ };
+ }
+ }
+}
+
+function getSourcesValue(sources: WatchSource[]) {
+ return sources.map(source => {
+ if (isRef(source)) {
+ return source.value;
+ } else if (isReactive(source)) {
+ return toRaw(source);
+ } else if (typeof source === 'function') {
+ return source();
+ }
+ });
+}
+
+function doWatch(source: WatchSource, listener: WatchCallback) {
+ let cb = (source: WatchSource, change) => {
+ const { mutation } = change;
+ listener(mutation.to, mutation.from);
+ };
+ cb = cb.bind(null, source);
+ source.addListener(cb);
return () => {
- stateVariable.removeListener(listener);
+ source.removeListener(cb);
};
}
@@ -35,3 +92,12 @@ export function watchEffect(fn: () => void): any {
};
}
}
+
+export function useWatch(source: WatchSource | WatchSource[], fn: WatchCallback): any {
+ const objRef = useRef(null);
+ if (objRef.current === null) {
+ objRef.current = watch(source, fn);
+ }
+
+ return objRef.current;
+}
diff --git a/packages/inula/src/inulax/store/StoreHandler.ts b/packages/inula/src/inulax/store/StoreHandler.ts
index a9b9c7d4..2b30ecde 100644
--- a/packages/inula/src/inulax/store/StoreHandler.ts
+++ b/packages/inula/src/inulax/store/StoreHandler.ts
@@ -100,7 +100,7 @@ export function clearVNodeObservers(vNode: VNode) {
}
// 注册VNode销毁时的清理动作
-function registerDestroyFunction() {
+export function registerDestroyFunction() {
const processingVNode = getProcessingVNode();
// 获取不到当前运行的VNode,说明不在组件中运行,属于非法场景
@@ -174,7 +174,6 @@ export function createStore, A extends UserActions
const storeObj = {
id,
$s: proxyObj,
- $state: proxyObj,
$a: $a as StoreActions,
$c: $c as ComputedValues,
$queue: $queue as QueuedStoreActions,
diff --git a/packages/inula/src/inulax/types/ProxyTypes.ts b/packages/inula/src/inulax/types/ProxyTypes.ts
index 751f14e2..d66593d1 100644
--- a/packages/inula/src/inulax/types/ProxyTypes.ts
+++ b/packages/inula/src/inulax/types/ProxyTypes.ts
@@ -29,6 +29,7 @@ export type Listeners = Listener[];
export type CurrentListener = { current: Listener };
export type WatchHandler = (key?: KeyType, oldValue?: any, newValue?: any, mutation?: any) => void;
export type WatchFn = (prop: KeyType, handler?: WatchHandler) => void;
+export type WatchCallback = (val: any, prevVal: any) => void;
type WatchProp = T & { watch?: WatchFn };
export type AddWatchProp =
@@ -67,3 +68,9 @@ export interface IObserver {
clearByVNode: (vNode: any) => void;
}
+
+export type Mutation = {
+ mutation: boolean;
+ from: T;
+ to: T;
+};