feat: add useWatch

This commit is contained in:
chaoling 2024-04-18 15:55:20 +08:00
parent 05f0610c99
commit d7101ff5e3
21 changed files with 825 additions and 77 deletions

View File

@ -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 (
<div>
<Text id={'hasPerson'} text={`has new person: ${reactiveObj.persons.length}`} />
<button id={'addBtn'} onClick={addOnePerson}>
add person
</button>
<button id={'delBtn'} onClick={delOnePerson}>
delete person
</button>
</div>
);
}
render(<App />, 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 (
<div>
<Text id={'hasPerson'} text={`has new person: ${refObj.value.persons.length}`} />
<button id={'addBtn'} onClick={addOnePerson}>
add person
</button>
<button id={'delBtn'} onClick={delOnePerson}>
delete person
</button>
</div>
);
}
render(<App />, 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 (
<div>
<Text id={'hasPerson'} text={`has new person: ${refObj.value}`} />
<button id={'addBtn'} onClick={add}>
add person
</button>
<button id={'delBtn'} onClick={del}>
delete person
</button>
</div>
);
}
render(<App />, 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 (
<div>
<Text id={'text'} text={computedData.value} />
<button id={'setText'} onClick={setText}>
set text
</button>
</div>
);
}
render(<App />, 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 (
<div>
<Text id={'text'} text={counter.num} />
<button id={'updateCounter'} onClick={updateCounter}>
set text
</button>
</div>
);
}
render(<App />, container);
expect(container?.querySelector('#text')?.innerHTML).toBe('0');
act(() => {
triggerClickEvent(container, 'updateCounter');
});
expect(container?.querySelector('#text')?.innerHTML).toBe('1');
expect(fn).toHaveBeenCalledTimes(2);
});
});

View File

@ -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<number>)[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<number[]>([]);
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);
});
});

View File

@ -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);
});

View File

@ -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);
});
});

View File

@ -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,
};

View File

@ -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<T extends { length?: number; _type?: string; entries?: any; values?: any }>(
from: T,
to: T
): Mutation<T> {
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 {

View File

@ -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);
})
```

View File

@ -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);
}

View File

@ -65,7 +65,7 @@ export function createProxy(rawObj: any, listener?: CurrentListener, isDeepProxy
deepProxyMap.set(rawObj, isDeepProxy);
// 创建Proxy
let proxyObj;
let proxyObj: ProxyHandler<any>;
if (!isDeepProxy) {
proxyObj = createObjectProxy(
rawObj,

View File

@ -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<T extends any[]>(rawObj: T, listener: CurrentListener) {
export function createArrayProxy<T extends any[]>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
const listeners: Listeners = [];
function get(rawObj: T, key: KeyType, receiver: any) {
@ -31,5 +31,5 @@ export function createArrayProxy<T extends any[]>(rawObj: T, listener: CurrentLi
registerListener(rawObj, listener, listeners);
return new Proxy(rawObj, handler);
return new Proxy(rawObj as ObjectType, handler);
}

View File

@ -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<T extends ObjectType>(rawObj: T, key: KeyType) {
}
export function deleteProperty<T extends ObjectType | any[]>(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);

View File

@ -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<T extends Map<any, any>>(rawObj: T, listener: CurrentListener): Record<string, any> {
export function createMapProxy<T extends Map<any, any>>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
const listeners: Listeners = [];
// 场景let obj = {}; map.set(obj, val);
// 满足两个UT1、map.has(Array.from(map.keys())[0])为true; 2、map.has(obj)为true;
@ -198,5 +198,5 @@ export function createMapProxy<T extends Map<any, any>>(rawObj: T, listener: Cur
registerListener(rawObj, listener, listeners);
return new Proxy(rawObj, handler);
return new Proxy(rawObj as ObjectType, handler);
}

View File

@ -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<T extends ObjectType>(rawObj: T, listener: CurrentListener, singleLevel = false) {
export function createObjectProxy<T extends ObjectType>(
rawObj: T,
listener: CurrentListener,
singleLevel = false
): ProxyHandler<T> {
const listeners: Listeners = [];
function get(rawObj: T, key: KeyType, receiver: any): any {

View File

@ -109,5 +109,5 @@ export function createSetProxy<T extends Set<any>>(rawObj: T, listener: CurrentL
registerListener(rawObj, listener, listeners);
return new Proxy(rawObj, handler as any);
return new Proxy(rawObj, handler);
}

View File

@ -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<T extends WeakMap<any, any>>(rawObj: T, listener: CurrentListener) {
export function createWeakMapProxy<T extends WeakMap<any, any>>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
const listeners: Listeners = [];
const handler = {
@ -72,5 +72,5 @@ export function createWeakMapProxy<T extends WeakMap<any, any>>(rawObj: T, liste
registerListener(rawObj, listener, listeners);
return new Proxy(rawObj, handler as any);
return new Proxy(rawObj as ObjectType, handler as any);
}

View File

@ -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<T> = (oldValue?: T) => T;
@ -23,7 +27,16 @@ export function computed<T>(fn: ComputedFN<T>): ComputedImpl<T> {
return new ComputedImpl(fn);
}
export class ComputedImpl<T> {
export function useComputed<T>(fn: ComputedFN<T>): ComputedImpl<T> {
const objRef = useRef<null | ComputedImpl>(null);
if (objRef.current === null) {
objRef.current = new ComputedImpl(fn);
}
return objRef.current;
}
export class ComputedImpl<T = any> {
private _value: T;
private readonly fn: ComputedFN<T>;
private readonly rContext: RContext;
@ -50,11 +63,19 @@ export class ComputedImpl<T> {
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);
}
}

View File

@ -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<T extends ObjectType>(rawObj: T): ReactiveRet<T>;
export function reactive<T extends ObjectType>(rawObj: T) {
return createProxy(rawObj);
}
export function useReactive<T extends ObjectType>(rawObj: T): ReactiveRet<T>;
export function useReactive<T extends ObjectType>(rawObj: T) {
registerDestroyFunction();
const objRef = useRef(rawObj);
return createProxy(objRef.current);
}
export function toRaw<T>(observed: T): T {
const raw = observed && observed[KeyTypes.RAW_VALUE];
return raw ? toRaw(raw) : observed;

View File

@ -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<T = any>(): RefType<T | undefined>;
export function ref<T>(value: T): RefType<UnwrapRef<T>>;
@ -25,10 +31,24 @@ export function ref(value?: unknown) {
return createRef(value, false);
}
function createRef(rawValue: unknown, isShallow: boolean) {
export function useReference<T = any>(): RefType<T | undefined>;
export function useReference<T>(value: T): RefType<UnwrapRef<T>>;
export function useReference(value?: unknown) {
registerDestroyFunction();
const objRef = useRef<null | RefType>(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<T> {
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<T>(ref: MaybeRef<T>): ref is RefType<T>;
@ -72,7 +101,7 @@ export function isRef(ref: any): ref is RefType {
}
export function toReactive<T extends unknown>(value: T): T {
return isObject(value) ? reactive(value as Record<any, any>) : value;
return isObject(value) ? createProxy(value) : value;
}
export function unref<T>(ref: MaybeRef<T>): T {

View File

@ -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<T = any> = RefType<T> | ProxyHandler<T> | ComputedImpl<T> | (() => 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 () => {
stateVariable.removeListener(listener);
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 () => {
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 | RContext>(null);
if (objRef.current === null) {
objRef.current = watch(source, fn);
}
return objRef.current;
}

View File

@ -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<S extends Record<string, any>, A extends UserActions
const storeObj = {
id,
$s: proxyObj,
$state: proxyObj,
$a: $a as StoreActions<S, A>,
$c: $c as ComputedValues<S, C>,
$queue: $queue as QueuedStoreActions<S, A>,

View File

@ -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> = T & { watch?: WatchFn };
export type AddWatchProp<T> =
@ -67,3 +68,9 @@ export interface IObserver {
clearByVNode: (vNode: any) => void;
}
export type Mutation<T = any> = {
mutation: boolean;
from: T;
to: T;
};