feat: add useWatch
This commit is contained in:
parent
05f0610c99
commit
d7101ff5e3
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
```
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
// 满足两个UT:1、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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 () => {
|
||||
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 | RContext>(null);
|
||||
if (objRef.current === null) {
|
||||
objRef.current = watch(source, fn);
|
||||
}
|
||||
|
||||
return objRef.current;
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue