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', () => { describe('test watchEffect', () => {
it('should run the passed function once (wrapped by a effect)', () => { it('should run the passed function once (wrapped by a effect)', () => {
const fnSpy = jest.fn(() => {}); const fnSpy = jest.fn();
watchEffect(fnSpy); watchEffect(fnSpy);
expect(fnSpy).toHaveBeenCalledTimes(1); 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, isPortal,
} from './external/InulaIs'; } from './external/InulaIs';
import { createStore, useStore, clearStore } from './inulax/store/StoreHandler'; import { createStore, useStore, clearStore } from './inulax/store/StoreHandler';
import { reactive, toRaw } from './inulax/reactive/Reactive'; import { reactive, useReactive, toRaw } from './inulax/reactive/Reactive';
import { ref, isRef, unref, shallowRef } from './inulax/reactive/Ref'; import { ref, useReference, isRef, unref, shallowRef } from './inulax/reactive/Ref';
import * as reduxAdapter from './inulax/adapters/redux'; import * as reduxAdapter from './inulax/adapters/redux';
import { watch, watchEffect } from './inulax/reactive/Watch'; import { watch, watchEffect, useWatch } from './inulax/reactive/Watch';
import { computed } from './inulax/reactive/Computed'; import { computed, useComputed } from './inulax/reactive/Computed';
import { act } from './external/TestUtil'; import { act } from './external/TestUtil';
import { import {
@ -127,14 +127,18 @@ const Inula = {
Suspense, Suspense,
// vue reactive api // vue reactive api
ref, ref,
useReference,
isRef, isRef,
unref, unref,
shallowRef, shallowRef,
reactive, reactive,
useReactive,
isReactive, isReactive,
isShallow, isShallow,
computed, computed,
useComputed,
watchEffect, watchEffect,
useWatch,
toRaw, toRaw,
}; };
@ -194,14 +198,18 @@ export {
Suspense, Suspense,
// vue reactive api // vue reactive api
ref, ref,
useReference,
isRef, isRef,
unref, unref,
shallowRef, shallowRef,
reactive, reactive,
useReactive,
isReactive, isReactive,
isShallow, isShallow,
computed, computed,
useComputed,
watchEffect, watchEffect,
useWatch,
toRaw, toRaw,
}; };

View File

@ -14,6 +14,7 @@
*/ */
import { KeyTypes, ReactiveFlags } from './Constants'; import { KeyTypes, ReactiveFlags } from './Constants';
import { Mutation } from './types/ProxyTypes';
export function isObject(obj: any): boolean { export function isObject(obj: any): boolean {
const type = typeof obj; const type = typeof obj;
@ -133,21 +134,24 @@ export function getDetailedType(val: any) {
return typeof val; 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)) { if (getDetailedType(from) !== getDetailedType(to)) {
return { mutation: true, from, to }; return { mutation: true, from, to };
} }
switch (getDetailedType(from)) { switch (getDetailedType(from)) {
case 'array': { case 'array': {
const len = Math.max(from.length, to.length); const len = Math.max(from.length ?? 0, to.length ?? 0);
const res: any[] = []; const res: any[] = [];
let found = false; let found = false;
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
if (from.length <= i) { if ((from.length ?? 0) <= i) {
res[i] = { mutation: true, to: to[i] }; res[i] = { mutation: true, to: to[i] };
found = true; found = true;
} else if (to.length <= i) { } else if ((to.length ?? 0) <= i) {
res[i] = { mutation: true, from: from[i] }; res[i] = { mutation: true, from: from[i] };
found = true; found = true;
} else { } else {

View File

@ -185,3 +185,58 @@ it('should no longer update when stopped', () => {
expect(dummy).toBe(1); 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 { KeyTypes } from '../Constants';
import { addRContext, RContextSet } from '../reactive/RContext'; import { addRContext, RContextSet } from '../reactive/RContext';
import { IObserver } from '../types/ProxyTypes'; import { IObserver, Listener, Mutation } from '../types/ProxyTypes';
/** /**
* Observer * Observer
@ -30,7 +30,7 @@ export class Observer implements IObserver {
keyVNodes = new Map(); keyVNodes = new Map();
listeners: ((mutation) => void)[] = []; listeners: Listener[] = [];
watchers = {}; 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); const vNodes = this.keyVNodes.get(key);
// NOTE: using Set directly can lead to deadlock // NOTE: using Set directly can lead to deadlock
const vNodeArray = Array.from(vNodes || []); const vNodeArray = Array.from(vNodes || []);
@ -115,11 +115,11 @@ export class Observer implements IObserver {
launchUpdateFromVNode(vNode); launchUpdateFromVNode(vNode);
} }
addListener(listener: (mutation) => void): void { addListener(listener: Listener): void {
this.listeners.push(listener); this.listeners.push(listener);
} }
removeListener(listener: (mutation) => void): void { removeListener(listener: Listener): void {
this.listeners = this.listeners.filter(item => item != listener); 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); deepProxyMap.set(rawObj, isDeepProxy);
// 创建Proxy // 创建Proxy
let proxyObj; let proxyObj: ProxyHandler<any>;
if (!isDeepProxy) { if (!isDeepProxy) {
proxyObj = createObjectProxy( proxyObj = createObjectProxy(
rawObj, rawObj,

View File

@ -15,9 +15,9 @@
import { registerListener } from './HandlerUtils'; import { registerListener } from './HandlerUtils';
import { baseSetFun, baseGetFun } from './BaseObjectHandler'; 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 = []; const listeners: Listeners = [];
function get(rawObj: T, key: KeyType, receiver: any) { 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); 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 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 = const hadKey =
isArr && isValidIntegerKey(key) ? Number(key) < rawObj.length : Object.prototype.hasOwnProperty.call(rawObj, key); 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); const observer = getObserver(rawObj);
if (!isSame(newValue, oldValue)) { if (!isSame(newValue, oldValue)) {
const mutation = isPanelActive() ? resolveMutation(oldJSON, rawObj) : resolveMutation(null, rawObj); const mutation = resolveMutation(oldObj, rawObj);
// 触发属性变化 // 触发属性变化
observer.setProp(key, mutation, oldValue, newValue); 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) { 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 observer = getObserver(rawObj);
const oldValue = rawObj[key]; const oldValue = rawObj[key];
const newValue = undefined; const newValue = undefined;
const ret = Reflect.deleteProperty(rawObj, key); const ret = Reflect.deleteProperty(rawObj, key);
const mutation = isPanelActive() ? resolveMutation(oldObject, rawObj) : resolveMutation(null, rawObj); const mutation = resolveMutation(oldObj, rawObj);
if (!isSame(newValue, oldValue)) { if (!isSame(newValue, oldValue)) {
observer.setProp(key, mutation, oldValue, newValue); observer.setProp(key, mutation, oldValue, newValue);

View File

@ -19,11 +19,11 @@ import { resolveMutation } from '../../CommonUtils';
import { KeyTypes } from '../../Constants'; import { KeyTypes } from '../../Constants';
import { getValOrProxy, registerListener } from './HandlerUtils'; import { getValOrProxy, registerListener } from './HandlerUtils';
import { baseDeleteFun, baseHasFun, baseForEach, baseGetFun, baseClearFun } from './BaseCollectionHandler'; 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'; 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 = []; const listeners: Listeners = [];
// 场景let obj = {}; map.set(obj, val); // 场景let obj = {}; map.set(obj, val);
// 满足两个UT1、map.has(Array.from(map.keys())[0])为true; 2、map.has(obj)为true; // 满足两个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); 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 { baseSetFun, baseGetFun, has, deleteProperty, ownKeys } from './BaseObjectHandler';
import { CurrentListener, KeyType, Listeners, ObjectType } from '../../types/ProxyTypes'; 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 = []; const listeners: Listeners = [];
function get(rawObj: T, key: KeyType, receiver: any): any { 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); 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 { resolveMutation } from '../../CommonUtils';
import { isPanelActive } from '../../devtools'; import { isPanelActive } from '../../devtools';
import { getValOrProxy, registerListener } from './HandlerUtils'; import { getValOrProxy, registerListener } from './HandlerUtils';
import { CurrentListener, Listeners } from '../../types/ProxyTypes'; import { CurrentListener, Listeners, ObjectType } from '../../types/ProxyTypes';
import { baseDeleteFun, baseGetFun } from './BaseCollectionHandler'; 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 listeners: Listeners = [];
const handler = { const handler = {
@ -72,5 +72,5 @@ export function createWeakMapProxy<T extends WeakMap<any, any>>(rawObj: T, liste
registerListener(rawObj, listener, listeners); 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 { RContext } from './RContext';
import { Observer } from '../proxy/Observer'; import { Observer } from '../proxy/Observer';
import { isSame } from '../CommonUtils'; 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; export type ComputedFN<T> = (oldValue?: T) => T;
@ -23,7 +27,16 @@ export function computed<T>(fn: ComputedFN<T>): ComputedImpl<T> {
return new ComputedImpl(fn); 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 _value: T;
private readonly fn: ComputedFN<T>; private readonly fn: ComputedFN<T>;
private readonly rContext: RContext; private readonly rContext: RContext;
@ -50,11 +63,19 @@ export class ComputedImpl<T> {
this._value = this.fn(oldValue); this._value = this.fn(oldValue);
if (!isSame(oldValue, this._value)) { if (!isSame(oldValue, this._value)) {
this.observer.setProp('value', {}); this.observer.setProp('value', { mutation: true, from: oldValue, to: this._value });
} }
} }
stop() { stop() {
this.rContext.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 { KeyTypes } from '../Constants';
import { ReactiveRet } from '../types/ReactiveTypes'; import { ReactiveRet } from '../types/ReactiveTypes';
import { ObjectType } from '../types/ProxyTypes'; 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): ReactiveRet<T>;
export function reactive<T extends ObjectType>(rawObj: T) { export function reactive<T extends ObjectType>(rawObj: T) {
return createProxy(rawObj); 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 { export function toRaw<T>(observed: T): T {
const raw = observed && observed[KeyTypes.RAW_VALUE]; const raw = observed && observed[KeyTypes.RAW_VALUE];
return raw ? toRaw(raw) : observed; return raw ? toRaw(raw) : observed;

View File

@ -16,8 +16,14 @@
import { isObject, isSame, isShallow } from '../CommonUtils'; import { isObject, isSame, isShallow } from '../CommonUtils';
import { reactive, toRaw } from './Reactive'; import { reactive, toRaw } from './Reactive';
import { Observer } from '../proxy/Observer'; import { Observer } from '../proxy/Observer';
import { OBSERVER_KEY } from '../Constants'; import { KeyTypes, OBSERVER_KEY } from '../Constants';
import { MaybeRef, RefType, UnwrapRef } from '../types/ReactiveTypes'; 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 = any>(): RefType<T | undefined>;
export function ref<T>(value: T): RefType<UnwrapRef<T>>; export function ref<T>(value: T): RefType<UnwrapRef<T>>;
@ -25,10 +31,24 @@ export function ref(value?: unknown) {
return createRef(value, false); 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)) { if (isRef(rawValue)) {
return rawValue; return rawValue;
} }
return new RefImpl(rawValue, isShallow); return new RefImpl(rawValue, isShallow);
} }
@ -55,15 +75,24 @@ class RefImpl<T> {
const useDirectValue = this._isShallow || isShallow(newVal); const useDirectValue = this._isShallow || isShallow(newVal);
newVal = useDirectValue ? newVal : toRaw(newVal); newVal = useDirectValue ? newVal : toRaw(newVal);
if (!isSame(newVal, this._rawValue)) { if (!isSame(newVal, this._rawValue)) {
const mutation = { mutation: true, from: this._rawValue, to: newVal };
this._rawValue = newVal; this._rawValue = newVal;
this._value = useDirectValue ? newVal : toReactive(newVal); this._value = useDirectValue ? newVal : toReactive(newVal);
this.observer.setProp('value', {}); this.observer.setProp('value', mutation);
} }
} }
get [OBSERVER_KEY]() { get [OBSERVER_KEY]() {
return this.observer; 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>; 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 { 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 { export function unref<T>(ref: MaybeRef<T>): T {

View File

@ -14,13 +14,70 @@
*/ */
import { RContext } from './RContext'; 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) { export type WatchSource<T = any> = RefType<T> | ProxyHandler<T> | ComputedImpl<T> | (() => T);
listener = listener.bind(null, stateVariable);
stateVariable.addListener(listener); 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 () => { 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 | 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销毁时的清理动作 // 注册VNode销毁时的清理动作
function registerDestroyFunction() { export function registerDestroyFunction() {
const processingVNode = getProcessingVNode(); const processingVNode = getProcessingVNode();
// 获取不到当前运行的VNode说明不在组件中运行属于非法场景 // 获取不到当前运行的VNode说明不在组件中运行属于非法场景
@ -174,7 +174,6 @@ export function createStore<S extends Record<string, any>, A extends UserActions
const storeObj = { const storeObj = {
id, id,
$s: proxyObj, $s: proxyObj,
$state: proxyObj,
$a: $a as StoreActions<S, A>, $a: $a as StoreActions<S, A>,
$c: $c as ComputedValues<S, C>, $c: $c as ComputedValues<S, C>,
$queue: $queue as QueuedStoreActions<S, A>, $queue: $queue as QueuedStoreActions<S, A>,

View File

@ -29,6 +29,7 @@ export type Listeners = Listener[];
export type CurrentListener = { current: Listener }; export type CurrentListener = { current: Listener };
export type WatchHandler = (key?: KeyType, oldValue?: any, newValue?: any, mutation?: any) => void; export type WatchHandler = (key?: KeyType, oldValue?: any, newValue?: any, mutation?: any) => void;
export type WatchFn = (prop: KeyType, handler?: WatchHandler) => void; export type WatchFn = (prop: KeyType, handler?: WatchHandler) => void;
export type WatchCallback = (val: any, prevVal: any) => void;
type WatchProp<T> = T & { watch?: WatchFn }; type WatchProp<T> = T & { watch?: WatchFn };
export type AddWatchProp<T> = export type AddWatchProp<T> =
@ -67,3 +68,9 @@ export interface IObserver {
clearByVNode: (vNode: any) => void; clearByVNode: (vNode: any) => void;
} }
export type Mutation<T = any> = {
mutation: boolean;
from: T;
to: T;
};