Compare commits
5 Commits
Author | SHA1 | Date |
---|---|---|
|
d780a17448 | |
|
8e7c6b3a72 | |
|
b725b0d98c | |
|
d7101ff5e3 | |
|
05f0610c99 |
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
printWidth: 120, // 一行120字符数,如果超过会进行换行
|
||||
tabWidth: 2, // tab等2个空格
|
||||
useTabs: false, // 用空格缩进行
|
||||
semi: true, // 行尾使用分号
|
||||
singleQuote: true, // 字符串使用单引号
|
||||
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
|
||||
jsxSingleQuote: false, // 在JSX中使用双引号
|
||||
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
|
||||
bracketSpacing: true, // 对象的括号间增加空格
|
||||
bracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
|
||||
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
|
||||
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
|
||||
endOfLine: 'lf', // 仅限换行(\n)
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
接口差异:
|
||||
|
||||
1、$patch不需要,可以直接赋值
|
||||
```ts
|
||||
actions: {
|
||||
setFoo(foo) {
|
||||
// not support
|
||||
this.$patch({ nested: { foo } });
|
||||
// support
|
||||
this.nested = { foo };
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
2、watch写法不同
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "@inula/pinia-adapter",
|
||||
"version": "0.0.1",
|
||||
"description": "pinia adapter",
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"test": "vitest --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.23.9",
|
||||
"@babel/core": "^7.23.9",
|
||||
"@babel/plugin-syntax-typescript": "^7.23.3",
|
||||
"openinula": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@vitest/ui": "^0.34.5",
|
||||
"jsdom": "^24.0.0",
|
||||
"vitest": "^0.34.5"
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
* Copyright (c) 2020 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.
|
||||
|
@ -13,11 +13,4 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
export function watch(stateVariable: any, listener: (state: any) => void) {
|
||||
listener = listener.bind(null, stateVariable);
|
||||
stateVariable.addListener(listener);
|
||||
|
||||
return () => {
|
||||
stateVariable.removeListener(listener);
|
||||
};
|
||||
}
|
||||
export * from './pinia';
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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 { createStore, watch as wt, StoreObj } from 'openinula';
|
||||
import { ref } from './ref';
|
||||
|
||||
const storeMap = new Map();
|
||||
|
||||
interface StoreDefinition {
|
||||
id: string;
|
||||
state: () => Record<string, any>;
|
||||
actions?: Record<string, Function>;
|
||||
getters?: Record<string, Function>;
|
||||
}
|
||||
|
||||
type StoreCallback = () => Record<string, any>;
|
||||
|
||||
export function defineStore(id: string | StoreDefinition, cbOrDef?: StoreCallback | StoreDefinition): Function {
|
||||
if (!cbOrDef && typeof id === 'object') {
|
||||
cbOrDef = id as StoreDefinition;
|
||||
id = cbOrDef.id;
|
||||
}
|
||||
|
||||
if (typeof cbOrDef === 'object') {
|
||||
return defineOptionsStore(id as string, cbOrDef as StoreDefinition);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const data = (cbOrDef as StoreCallback)();
|
||||
|
||||
if (storeMap.has(cbOrDef)) {
|
||||
return storeMap.get(cbOrDef)();
|
||||
}
|
||||
|
||||
const entries = Object.entries(data);
|
||||
|
||||
const state = Object.fromEntries(entries.filter(([key, val]) => val.isRef).map(([key, val]) => [key, val.value]));
|
||||
const computed = Object.fromEntries(
|
||||
entries.filter(([key, val]) => val.isComputed).map(([key, val]) => [key, val.raw])
|
||||
);
|
||||
const actions = Object.fromEntries(entries.filter(([key, val]) => !val.isRef && !val.isComputed));
|
||||
|
||||
const useStore = createStore({
|
||||
id: id as string,
|
||||
state,
|
||||
computed,
|
||||
actions: enhanceActions(actions),
|
||||
});
|
||||
|
||||
Object.entries(data)
|
||||
.filter(([key, val]) => val.isRef)
|
||||
.forEach(([key, val]) =>
|
||||
val.watch(newVal => {
|
||||
useStore().$s[key] = newVal;
|
||||
})
|
||||
);
|
||||
|
||||
storeMap.set(cbOrDef, useStore);
|
||||
|
||||
return useStore();
|
||||
};
|
||||
}
|
||||
|
||||
function enhanceActions(actions) {
|
||||
if (!actions) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(actions).map(([key, value]) => {
|
||||
return [
|
||||
key,
|
||||
function (store, ...args) {
|
||||
return value.bind(this)(...args);
|
||||
},
|
||||
];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function defineOptionsStore<S, A, C>(
|
||||
id: string,
|
||||
{ state, actions, getters }: StoreDefinition
|
||||
): () => StoreObj<any, any, any> {
|
||||
if (typeof state === 'function') {
|
||||
state = state();
|
||||
}
|
||||
let useStore = null;
|
||||
|
||||
return () => {
|
||||
if (!useStore) {
|
||||
useStore = createStore({
|
||||
id,
|
||||
state,
|
||||
actions: enhanceActions(actions),
|
||||
computed: getters,
|
||||
});
|
||||
}
|
||||
|
||||
return useStore();
|
||||
};
|
||||
}
|
||||
|
||||
export function mapStores(...stores) {
|
||||
const result = {};
|
||||
|
||||
stores.forEach(store => {
|
||||
const expandedStore = store();
|
||||
result[`${expandedStore.id}Store`] = () => expandedStore;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function storeToRefs(store) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(store.$s).map(([key, value]) => {
|
||||
return [key, ref(value)];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function createPinia() {
|
||||
const result = {
|
||||
install: app => {}, // do something?
|
||||
use: plugin => result,
|
||||
state: {}, //
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
export const watch = wt;
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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 { useState, useRef } from 'openinula';
|
||||
export function refBase(initialValue, update, state) {
|
||||
let listeners = new Set();
|
||||
|
||||
return new Proxy(
|
||||
{ value: initialValue },
|
||||
{
|
||||
get: (target, name) => {
|
||||
if (name === 'value' || name === 'current') {
|
||||
return state.current;
|
||||
}
|
||||
if (name === Symbol.toPrimitive) {
|
||||
return () => state.current;
|
||||
}
|
||||
if (name === 'isRef') return true;
|
||||
if (name === 'watch')
|
||||
return cb => {
|
||||
listeners.add(cb);
|
||||
};
|
||||
if (name === 'raw') return initialValue;
|
||||
},
|
||||
set: (target, name, value) => {
|
||||
if (name === 'value') {
|
||||
if (state.current === value) return true;
|
||||
state.current = value;
|
||||
update();
|
||||
Array.from(listeners.values()).forEach(listener => {
|
||||
listener(value);
|
||||
});
|
||||
return true;
|
||||
} else if (name === 'current') {
|
||||
if (state.current === value) return true;
|
||||
state.current = value;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function ref(initialValue) {
|
||||
let [b, r] = useState(false);
|
||||
let state = useRef(initialValue);
|
||||
|
||||
return refBase(initialValue, () => r(!b), state);
|
||||
}
|
||||
|
||||
export function ref(initialValue) {
|
||||
let [b, r] = useState(false);
|
||||
let state = useRef(initialValue);
|
||||
|
||||
return refBase(initialValue, () => r(!b), state);
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck For the compiled code.
|
||||
|
||||
import { describe, it, vi, expect } from 'vitest';
|
||||
import { defineStore } from '../src/pinia';
|
||||
|
||||
describe('pinia state', () => {
|
||||
const useStore = () => {
|
||||
return defineStore({
|
||||
id: 'main',
|
||||
state: () => ({
|
||||
a: true,
|
||||
nested: {
|
||||
foo: 'foo',
|
||||
a: { b: 'string' },
|
||||
},
|
||||
}),
|
||||
getters: {
|
||||
nonA(): boolean {
|
||||
return !this.a;
|
||||
},
|
||||
otherComputed() {
|
||||
return this.nonA;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async getNonA() {
|
||||
return this.nonA;
|
||||
},
|
||||
simple() {
|
||||
this.toggle();
|
||||
return 'simple';
|
||||
},
|
||||
|
||||
toggle() {
|
||||
return (this.a = !this.a);
|
||||
},
|
||||
|
||||
setFoo(foo: string) {
|
||||
this.nested = { foo };
|
||||
},
|
||||
|
||||
combined() {
|
||||
this.toggle();
|
||||
this.setFoo('bar');
|
||||
},
|
||||
|
||||
throws() {
|
||||
throw new Error('fail');
|
||||
},
|
||||
|
||||
async rejects() {
|
||||
throw 'fail';
|
||||
},
|
||||
},
|
||||
})();
|
||||
};
|
||||
|
||||
const useB = defineStore({
|
||||
id: 'B',
|
||||
state: () => ({ b: 'b' }),
|
||||
});
|
||||
|
||||
const useA = defineStore({
|
||||
id: 'A',
|
||||
state: () => ({ a: 'a' }),
|
||||
actions: {
|
||||
swap() {
|
||||
const bStore = useB();
|
||||
const b = bStore.$state.b;
|
||||
bStore.$state.b = this.$state.a;
|
||||
this.$state.a = b;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it('can use the store as this', () => {
|
||||
const store = useStore();
|
||||
expect(store.$state.a).toBe(true);
|
||||
store.toggle();
|
||||
expect(store.$state.a).toBe(false);
|
||||
});
|
||||
|
||||
it('store is forced as the context', () => {
|
||||
const store = useStore();
|
||||
expect(store.$state.a).toBe(true);
|
||||
expect(() => {
|
||||
store.toggle.call(null);
|
||||
}).not.toThrow();
|
||||
expect(store.$state.a).toBe(false);
|
||||
});
|
||||
|
||||
it('can call other actions', () => {
|
||||
const store = useStore();
|
||||
expect(store.$state.a).toBe(true);
|
||||
expect(store.$state.nested.foo).toBe('foo');
|
||||
store.combined();
|
||||
expect(store.$state.a).toBe(false);
|
||||
expect(store.$state.nested.foo).toBe('bar');
|
||||
});
|
||||
|
||||
it('throws errors', () => {
|
||||
const store = useStore();
|
||||
expect(() => store.throws()).toThrowError('fail');
|
||||
});
|
||||
|
||||
it('throws async errors', async () => {
|
||||
const store = useStore();
|
||||
expect.assertions(1);
|
||||
await expect(store.rejects()).rejects.toBe('fail');
|
||||
});
|
||||
|
||||
it('can catch async errors', async () => {
|
||||
const store = useStore();
|
||||
expect.assertions(3);
|
||||
const spy = vi.fn();
|
||||
await expect(store.rejects().catch(spy)).resolves.toBe(undefined);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith('fail');
|
||||
});
|
||||
|
||||
it('can destructure actions', () => {
|
||||
const store = useStore();
|
||||
const { simple } = store;
|
||||
expect(simple()).toBe('simple');
|
||||
// works with the wrong this
|
||||
expect({ simple }.simple()).toBe('simple');
|
||||
// special this check
|
||||
expect({ $id: 'o', simple }.simple()).toBe('simple');
|
||||
// override the function like devtools do
|
||||
expect(
|
||||
{
|
||||
$id: store.$id,
|
||||
simple,
|
||||
// otherwise it would fail
|
||||
toggle() {},
|
||||
}.simple()
|
||||
).toBe('simple');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck For the compiled code.
|
||||
|
||||
import { describe, it, vi, expect } from 'vitest';
|
||||
import { defineStore, createPinia } from '../src/pinia';
|
||||
import { ref } from '../src/ref';
|
||||
|
||||
function expectType<T>(_value: T): void {}
|
||||
|
||||
describe('pinia getters', () => {
|
||||
const useStore = defineStore({
|
||||
id: 'main',
|
||||
state: () => ({
|
||||
name: 'Eduardo',
|
||||
}),
|
||||
getters: {
|
||||
upperCaseName(store) {
|
||||
return store.name.toUpperCase();
|
||||
},
|
||||
doubleName(): string {
|
||||
return this.upperCaseName;
|
||||
},
|
||||
composed(): string {
|
||||
return this.upperCaseName + ': ok';
|
||||
},
|
||||
arrowUpper: state => {
|
||||
// @ts-expect-error
|
||||
state.nope;
|
||||
state.name.toUpperCase();
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
o() {
|
||||
// @ts-expect-error it should type getters
|
||||
this.arrowUpper.toUpperCase();
|
||||
this.o().toUpperCase();
|
||||
return 'a string';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const useB = defineStore({
|
||||
id: 'B',
|
||||
state: () => ({ b: 'b' }),
|
||||
});
|
||||
|
||||
const useA = defineStore({
|
||||
id: 'A',
|
||||
state: () => ({ a: 'a' }),
|
||||
getters: {
|
||||
fromB(): string {
|
||||
const bStore = useB();
|
||||
return this.a + ' ' + bStore.b;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it('adds getters to the store', () => {
|
||||
const store = useStore();
|
||||
expect(store.upperCaseName).toBe('EDUARDO');
|
||||
|
||||
// @ts-expect-error
|
||||
store.nope;
|
||||
|
||||
store.name = 'Ed';
|
||||
expect(store.upperCaseName).toBe('ED');
|
||||
});
|
||||
|
||||
it('updates the value', () => {
|
||||
const store = useStore();
|
||||
store.name = 'Ed';
|
||||
expect(store.upperCaseName).toBe('ED');
|
||||
});
|
||||
|
||||
it('can use other getters', () => {
|
||||
const store = useStore();
|
||||
expect(store.composed).toBe('EDUARDO: ok');
|
||||
store.name = 'Ed';
|
||||
expect(store.composed).toBe('ED: ok');
|
||||
});
|
||||
|
||||
describe('cross used stores', () => {
|
||||
const useA = defineStore('a', () => {
|
||||
const n = ref(0);
|
||||
const double = computed(() => n.value * 2);
|
||||
const sum = computed(() => n.value + B.n);
|
||||
|
||||
function increment() {
|
||||
return n.value++;
|
||||
}
|
||||
|
||||
function incrementB() {
|
||||
return B.increment();
|
||||
}
|
||||
|
||||
return { n, double, sum, increment, incrementB };
|
||||
});
|
||||
|
||||
const useB = defineStore('b', () => {
|
||||
const n = ref(0);
|
||||
const double = computed(() => n.value * 2);
|
||||
|
||||
function increment() {
|
||||
return n.value++;
|
||||
}
|
||||
|
||||
function incrementA() {
|
||||
return A.increment();
|
||||
}
|
||||
|
||||
return { n, double, incrementA, increment };
|
||||
});
|
||||
|
||||
const A = useA();
|
||||
const B = useB();
|
||||
|
||||
it('keeps getters reactive', () => {
|
||||
const a = useA();
|
||||
const b = useB();
|
||||
|
||||
expectType<() => number>(a.increment);
|
||||
expectType<() => number>(b.increment);
|
||||
expectType<() => number>(a.incrementB);
|
||||
expectType<() => number>(b.incrementA);
|
||||
|
||||
expect(a.double).toBe(0);
|
||||
b.incrementA();
|
||||
expect(a.double).toBe(2);
|
||||
a.increment();
|
||||
expect(a.double).toBe(4);
|
||||
|
||||
expect(b.double).toBe(0);
|
||||
a.incrementB();
|
||||
expect(b.double).toBe(2);
|
||||
b.increment();
|
||||
expect(b.double).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck For the compiled code.
|
||||
|
||||
import { beforeEach, describe, it, vi, expect } from 'vitest';
|
||||
import { createPinia, defineStore, computed } from '../src/pinia';
|
||||
|
||||
describe('pinia state', () => {
|
||||
beforeEach(() => {});
|
||||
|
||||
const useStore = defineStore('main', {
|
||||
state: () => ({
|
||||
name: 'Eduardo',
|
||||
counter: 0,
|
||||
nested: { n: 0 },
|
||||
}),
|
||||
});
|
||||
|
||||
it('can directly access state at the store level', () => {
|
||||
const store = useStore();
|
||||
expect(store.name).toBe('Eduardo');
|
||||
store.name = 'Ed';
|
||||
expect(store.name).toBe('Ed');
|
||||
});
|
||||
|
||||
it.skip('state is reactive', () => {
|
||||
const store = useStore();
|
||||
const upperCased = computed(() => store.name.toUpperCase());
|
||||
expect(upperCased.value).toBe('EDUARDO');
|
||||
store.name = 'Ed';
|
||||
expect(upperCased.value).toBe('ED');
|
||||
});
|
||||
|
||||
it('can be nested set on store.$s, not $state', () => {
|
||||
const store = useStore();
|
||||
|
||||
store.$s.nested.n = 3;
|
||||
|
||||
expect(store.nested.n).toBe(3);
|
||||
expect(store.$s.nested.n).toBe(3);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck For the compiled code.
|
||||
|
||||
import { beforeEach, describe, it, vi, expect } from 'vitest';
|
||||
import { createPinia, defineStore, computed } from '../src/pinia';
|
||||
|
||||
describe('pinia state', () => {
|
||||
beforeEach(() => {});
|
||||
|
||||
const useStore = defineStore({
|
||||
id: 'main',
|
||||
state: () => ({
|
||||
a: true,
|
||||
nested: {
|
||||
foo: 'foo',
|
||||
a: { b: 'string' },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
it.skip('sets the initial state', () => {
|
||||
const store = useStore();
|
||||
store.$s;
|
||||
expect(store.$s).toEqual({
|
||||
a: true,
|
||||
nested: {
|
||||
foo: 'foo',
|
||||
a: { b: 'string' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('reuses a store', () => {
|
||||
const useStore = defineStore({ id: 'main' });
|
||||
expect(useStore()).toBe(useStore());
|
||||
});
|
||||
|
||||
it('can replace its state', () => {
|
||||
const store = useStore();
|
||||
const spy = vi.fn();
|
||||
store.$s.watch('a', () => {
|
||||
spy();
|
||||
});
|
||||
|
||||
expect(store.a).toBe(true);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
||||
store.a = false;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
export default {
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
## 适配Vue
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
presets: ['@babel/preset-typescript', ['@babel/preset-env', { targets: { node: 'current' } }]],
|
||||
plugins: [
|
||||
'@babel/plugin-syntax-jsx',
|
||||
[
|
||||
'@babel/plugin-transform-react-jsx',
|
||||
{
|
||||
runtime: 'automatic',
|
||||
importSource: 'openinula',
|
||||
},
|
||||
],
|
||||
['@babel/plugin-proposal-class-properties', { loose: true }],
|
||||
['@babel/plugin-proposal-private-methods', { loose: true }],
|
||||
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
|
||||
'@babel/plugin-transform-object-assign',
|
||||
'@babel/plugin-transform-object-super',
|
||||
['@babel/plugin-proposal-object-rest-spread', { loose: true, useBuiltIns: true }],
|
||||
['@babel/plugin-transform-template-literals', { loose: true }],
|
||||
'@babel/plugin-transform-arrow-functions',
|
||||
'@babel/plugin-transform-literals',
|
||||
'@babel/plugin-transform-for-of',
|
||||
'@babel/plugin-transform-block-scoped-functions',
|
||||
'@babel/plugin-transform-classes',
|
||||
'@babel/plugin-transform-shorthand-properties',
|
||||
'@babel/plugin-transform-computed-properties',
|
||||
'@babel/plugin-transform-parameters',
|
||||
['@babel/plugin-transform-spread', { loose: true, useBuiltIns: true }],
|
||||
['@babel/plugin-transform-block-scoping', { throwIfClosureRequired: false }],
|
||||
['@babel/plugin-transform-destructuring', { loose: true, useBuiltIns: true }],
|
||||
'@babel/plugin-transform-runtime',
|
||||
'@babel/plugin-proposal-nullish-coalescing-operator',
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
],
|
||||
};
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"name": "@inula/vue-adapter",
|
||||
"version": "0.0.1",
|
||||
"description": "vue adapter",
|
||||
"main": "./build/cjs/vue-adapter.js",
|
||||
"module": "./build/esm/vue-adapter.js",
|
||||
"types": "build/@types/index.d.ts",
|
||||
"files": [
|
||||
"/build",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest --ui",
|
||||
"build": "rollup -c ./scripts/rollup.config.js && npm run build-types",
|
||||
"build-types": "tsc -p tsconfig.build.json && rollup -c ./scripts/build-types.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"openinula": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.21.3",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.16.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.16.7",
|
||||
"@babel/plugin-syntax-jsx": "7.16.7",
|
||||
"@babel/plugin-transform-arrow-functions": "7.16.7",
|
||||
"@babel/plugin-transform-block-scoped-functions": "7.16.7",
|
||||
"@babel/plugin-transform-block-scoping": "7.16.7",
|
||||
"@babel/plugin-transform-classes": "7.16.7",
|
||||
"@babel/plugin-transform-computed-properties": "7.16.7",
|
||||
"@babel/plugin-transform-destructuring": "7.16.7",
|
||||
"@babel/plugin-transform-for-of": "7.16.7",
|
||||
"@babel/plugin-transform-literals": "7.16.7",
|
||||
"@babel/plugin-transform-object-assign": "7.16.7",
|
||||
"@babel/plugin-transform-object-super": "7.16.7",
|
||||
"@babel/plugin-transform-parameters": "7.16.7",
|
||||
"@babel/plugin-transform-react-jsx": "7.16.7",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.16.7",
|
||||
"@babel/plugin-transform-runtime": "7.16.7",
|
||||
"@babel/plugin-transform-shorthand-properties": "7.16.7",
|
||||
"@babel/plugin-transform-spread": "7.16.7",
|
||||
"@babel/plugin-transform-template-literals": "7.16.7",
|
||||
"@babel/preset-env": "7.16.7",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@rollup/plugin-babel": "^6.0.3",
|
||||
"@rollup/plugin-node-resolve": "^15.1.0",
|
||||
"prettier": "2.8.8",
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-dts": "^6.0.1",
|
||||
"rollup-plugin-terser": "^5.1.3",
|
||||
"typescript": "4.9.3",
|
||||
"@vitest/ui": "^0.34.5",
|
||||
"jsdom": "^24.0.0",
|
||||
"vitest": "^0.34.5",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openinula": ">=0.1.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
|
||||
function deleteFolder(filePath) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
if (fs.lstatSync(filePath).isDirectory()) {
|
||||
const files = fs.readdirSync(filePath);
|
||||
files.forEach(file => {
|
||||
const nextFilePath = path.join(filePath, file);
|
||||
const states = fs.lstatSync(nextFilePath);
|
||||
if (states.isDirectory()) {
|
||||
deleteFolder(nextFilePath);
|
||||
} else {
|
||||
fs.unlinkSync(nextFilePath);
|
||||
}
|
||||
});
|
||||
fs.rmdirSync(filePath);
|
||||
} else if (fs.lstatSync(filePath).isFile()) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除非空文件夹
|
||||
* @param folders {string[]}
|
||||
* @returns {{buildEnd(): void, name: string}}
|
||||
*/
|
||||
export function cleanUp(folders) {
|
||||
return {
|
||||
name: 'clean-up',
|
||||
buildEnd() {
|
||||
folders.forEach(f => deleteFolder(f));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildTypeConfig() {
|
||||
return {
|
||||
input: ['./build/@types/index.d.ts'],
|
||||
output: {
|
||||
file: './build/@types/index.d.ts',
|
||||
},
|
||||
plugins: [dts(), cleanUp(['./build/@types/'])],
|
||||
};
|
||||
}
|
||||
|
||||
export default [buildTypeConfig()];
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import babel from '@rollup/plugin-babel';
|
||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
const outDir = path.join(rootDir, 'build');
|
||||
|
||||
const extensions = ['.js', '.ts', '.tsx'];
|
||||
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
|
||||
const getConfig = mode => {
|
||||
const prod = mode.startsWith('prod');
|
||||
const outputList = [
|
||||
{
|
||||
file: path.join(outDir, `cjs/vue-adapter.${prod ? 'min.' : ''}js`),
|
||||
sourcemap: 'true',
|
||||
format: 'cjs',
|
||||
},
|
||||
{
|
||||
file: path.join(outDir, `umd/vue-adapter.${prod ? 'min.' : ''}js`),
|
||||
name: 'VueAdapter',
|
||||
sourcemap: 'true',
|
||||
format: 'umd',
|
||||
},
|
||||
];
|
||||
if (!prod) {
|
||||
outputList.push({
|
||||
file: path.join(outDir, 'esm/vue-adapter.js'),
|
||||
sourcemap: 'true',
|
||||
format: 'esm',
|
||||
});
|
||||
}
|
||||
return {
|
||||
input: path.join(rootDir, '/src/index.ts'),
|
||||
output: outputList,
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
extensions,
|
||||
modulesOnly: true,
|
||||
}),
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
configFile: path.join(rootDir, '/babel.config.js'),
|
||||
babelHelpers: 'runtime',
|
||||
extensions,
|
||||
}),
|
||||
prod && terser(),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export default [getConfig('dev'), getConfig('prod')];
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './lifecycle';
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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 { useEffect, useLayoutEffect, useRef } from 'openinula';
|
||||
|
||||
// 用于存储组件是否已挂载的状态
|
||||
const useIsMounted = () => {
|
||||
const isMounted = useRef(false);
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
return isMounted.current;
|
||||
};
|
||||
|
||||
export const onBeforeMount = (fn: () => void) => {
|
||||
const isMounted = useIsMounted();
|
||||
if (!isMounted) {
|
||||
fn?.();
|
||||
}
|
||||
};
|
||||
|
||||
export function onMounted(fn: () => void) {
|
||||
useEffect(() => {
|
||||
fn?.();
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function onBeforeUpdated(fn: () => void) {
|
||||
useEffect(() => {
|
||||
fn?.();
|
||||
});
|
||||
}
|
||||
|
||||
export function onUpdated(fn: () => void) {
|
||||
useEffect(() => {
|
||||
fn?.();
|
||||
});
|
||||
}
|
||||
|
||||
export const onBeforeUnmount = (fn: () => void) => {
|
||||
useLayoutEffect(() => {
|
||||
return () => {
|
||||
fn?.();
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export function onUnmounted(fn: () => void) {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
fn?.();
|
||||
};
|
||||
}, []);
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck For the compiled code.
|
||||
|
||||
import { describe, it, vi, expect } from 'vitest';
|
||||
import { render, act, useState } from 'openinula';
|
||||
import { onBeforeUnmount, onUnmounted, onMounted, onBeforeMount, onUpdated } from '../src';
|
||||
|
||||
describe('lifecycle', () => {
|
||||
it('should call the onBeforeMount', () => {
|
||||
const fn = vi.fn(() => {
|
||||
expect(document.querySelector('span')).toBeNull();
|
||||
});
|
||||
|
||||
const Comp = () => {
|
||||
const [toggle, setToggle] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggle ? <Child /> : null}
|
||||
<button onClick={() => setToggle(false)}>Unmount</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Child = () => {
|
||||
onBeforeMount(fn);
|
||||
return <span />;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
render(<Comp />, container);
|
||||
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
container.querySelector('button').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call the onMounted', () => {
|
||||
const fn = vi.fn(() => {
|
||||
// 断言在组件卸载之后,子组件不存在于 DOM 中
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
});
|
||||
|
||||
const Comp = () => {
|
||||
const [toggle, setToggle] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggle ? <Child /> : null}
|
||||
<button onClick={() => setToggle(false)}>Unmount</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Child = () => {
|
||||
onMounted(fn);
|
||||
return <span />;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
render(<Comp />, container);
|
||||
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
container.querySelector('button').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call the onUnmounted after the component unmounts', () => {
|
||||
const fn = vi.fn(() => {
|
||||
// 断言在组件卸载之后,子组件不存在于 DOM 中
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
});
|
||||
|
||||
const Comp = () => {
|
||||
const [toggle, setToggle] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggle ? <Child /> : null}
|
||||
<button onClick={() => setToggle(false)}>Unmount</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Child = () => {
|
||||
onUnmounted(fn);
|
||||
return <span />;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
render(<Comp />, container);
|
||||
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
container.querySelector('button').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call the onBeforeUnmount before the component unmounts', () => {
|
||||
const fn = vi.fn(() => {
|
||||
// 断言在组件卸载之前,子组件仍然存在于 DOM 中
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
});
|
||||
|
||||
const Comp = () => {
|
||||
const [toggle, setToggle] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggle ? <Child /> : null}
|
||||
<button onClick={() => setToggle(false)}>Unmount</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Child = () => {
|
||||
onBeforeUnmount(fn);
|
||||
return <span />;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
render(<Comp />, container);
|
||||
|
||||
expect(document.querySelector('span')).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
container.querySelector('button').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(document.querySelector('span')).toBeNull();
|
||||
});
|
||||
|
||||
it('should call the onUpdated/onBeforeUpdated', () => {
|
||||
const fn = vi.fn(() => {
|
||||
expect(document.querySelector('span').outerHTML).toBe('<span>0</span>');
|
||||
});
|
||||
|
||||
const Comp = () => {
|
||||
const [toggle, setToggle] = useState(true);
|
||||
|
||||
onUpdated(fn);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>{toggle ? 1 : 0}</span>
|
||||
<button onClick={() => setToggle(false)}>Unmount</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
render(<Comp />, container);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(0);
|
||||
expect(document.querySelector('span').outerHTML).toBe('<span>1</span>');
|
||||
|
||||
container.querySelector('button').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"files": [
|
||||
"src/index.ts"
|
||||
],
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationDir": "./build/@types"
|
||||
},
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./build",
|
||||
"incremental": false,
|
||||
"sourceMap": true,
|
||||
"allowJs": true, // allowJs=true => tsc compile js as module, no type check
|
||||
"checkJs": false, // Disable ts error checking in js
|
||||
"strict": true, // js-ts mixed setting
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": false, // 等大部分js代码改成ts之后再启用.
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitAny": false,
|
||||
"noImplicitThis": true,
|
||||
"module": "CommonJS",
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"target": "es5",
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"allowUnreachableCode": true,
|
||||
"alwaysStrict": true,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"experimentalDecorators": true,
|
||||
"downlevelIteration": true,
|
||||
"types": ["jest"], // 赋值为空数组使@types/node不会起作用
|
||||
"lib": ["dom", "esnext", "ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020"],
|
||||
"baseUrl": ".",
|
||||
"rootDir": "./src",
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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 react from '@vitejs/plugin-react';
|
||||
|
||||
let alias = {
|
||||
react: 'openinula', // 新增
|
||||
'react-dom': 'openinula', // 新增
|
||||
'react/jsx-dev-runtime': 'openinula/jsx-dev-runtime',
|
||||
};
|
||||
|
||||
export default {
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
},
|
||||
resolve: {
|
||||
alias,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* 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 { vueReactive } from '../../../src';
|
||||
const { ref, reactive, watchEffect, computed } = vueReactive;
|
||||
|
||||
describe('test computed', () => {
|
||||
it('should correctly update the computed value', () => {
|
||||
const data = reactive<{ bar?: string }>({});
|
||||
const computedData = computed(() => {
|
||||
return data.bar;
|
||||
});
|
||||
expect(computedData.value).toBe(undefined);
|
||||
data.bar = 'test';
|
||||
expect(computedData.value).toBe('test');
|
||||
});
|
||||
|
||||
it('should validate the effect trigger', () => {
|
||||
const data = reactive<{ key?: number }>({});
|
||||
const computedData = computed(() => {
|
||||
return data.key;
|
||||
});
|
||||
let result;
|
||||
watchEffect(() => {
|
||||
result = computedData.value;
|
||||
});
|
||||
expect(result).toBe(undefined);
|
||||
data.key = 2;
|
||||
expect(result).toBe(2);
|
||||
});
|
||||
|
||||
it('should validate the computation chain', () => {
|
||||
const data = reactive({ bar: 0 });
|
||||
const c1 = computed(() => data.bar);
|
||||
const c2 = computed(() => c1.value + 2);
|
||||
expect(c2.value).toBe(2);
|
||||
expect(c1.value).toBe(0);
|
||||
data.bar += 2;
|
||||
expect(c2.value).toBe(4);
|
||||
expect(c1.value).toBe(2);
|
||||
});
|
||||
|
||||
it('should validate the computation sequence', () => {
|
||||
const data = reactive({ key: 0 });
|
||||
const getter1 = jest.fn(() => data.key);
|
||||
const getter2 = jest.fn(() => {
|
||||
return c1.value + 3;
|
||||
});
|
||||
const c1 = computed(getter1);
|
||||
const c2 = computed(getter2);
|
||||
|
||||
let result;
|
||||
watchEffect(() => {
|
||||
result = c2.value;
|
||||
});
|
||||
expect(result).toBe(3);
|
||||
expect(getter1).toHaveBeenCalledTimes(1);
|
||||
expect(getter2).toHaveBeenCalledTimes(1);
|
||||
data.key += 2;
|
||||
expect(result).toBe(5);
|
||||
|
||||
expect(getter1).toHaveBeenCalledTimes(2);
|
||||
expect(getter2).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should validate the computation process', () => {
|
||||
const reactiveData = reactive({ key: 0 });
|
||||
const getterFunc1 = jest.fn(() => reactiveData.key);
|
||||
const getterFunc2 = jest.fn(function () {
|
||||
return computedValue1.value + 2;
|
||||
});
|
||||
const computedValue1 = computed(getterFunc1);
|
||||
const computedValue2 = computed(getterFunc2);
|
||||
|
||||
let computedResult;
|
||||
watchEffect(() => {
|
||||
computedResult = computedValue1.value + computedValue2.value;
|
||||
});
|
||||
expect(computedResult).toBe(2);
|
||||
|
||||
expect(getterFunc1).toHaveBeenCalledTimes(1);
|
||||
expect(getterFunc2).toHaveBeenCalledTimes(1);
|
||||
reactiveData.key++;
|
||||
expect(computedResult).toBe(4);
|
||||
|
||||
expect(getterFunc1).toHaveBeenCalledTimes(2);
|
||||
expect(getterFunc2).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should validate the computation halt', function () {
|
||||
const reactiveObj = reactive<{ key1?: number }>({});
|
||||
const computedObj = computed(() => {
|
||||
return reactiveObj.key1;
|
||||
});
|
||||
let resultValue;
|
||||
watchEffect(() => {
|
||||
resultValue = computedObj.value;
|
||||
});
|
||||
expect(resultValue).toBe(undefined);
|
||||
reactiveObj.key1 = 3;
|
||||
expect(resultValue).toBe(3);
|
||||
computedObj.stop();
|
||||
reactiveObj.key1 = 4;
|
||||
expect(resultValue).toBe(3);
|
||||
});
|
||||
|
||||
it('should validate the computation changes', () => {
|
||||
const numRef = ref(0);
|
||||
const increment = computed(() => numRef.value + 2);
|
||||
const testFn = jest.fn(() => {
|
||||
numRef.value;
|
||||
increment.value;
|
||||
});
|
||||
watchEffect(testFn);
|
||||
numRef.value += 3;
|
||||
// should call testFn 3 times, 1 for init, 1 for numRef, 1 for increment
|
||||
expect(testFn).toBeCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should validate the computation stop', () => {
|
||||
const reactiveObj = reactive<{ key1?: number }>({ key1: 1 });
|
||||
const computedObj = computed(() => reactiveObj.key1);
|
||||
computedObj.stop();
|
||||
expect(computedObj.value).toBe(1);
|
||||
});
|
||||
|
||||
it('should validate data changes in a non-lazy manner', () => {
|
||||
const spyFunction = jest.fn();
|
||||
|
||||
const refData = ref<null | { num: number }>({
|
||||
num: 3,
|
||||
});
|
||||
const computedData1 = computed(() => {
|
||||
return refData.value;
|
||||
});
|
||||
const computedData2 = computed(() => {
|
||||
spyFunction();
|
||||
return computedData1.value?.num;
|
||||
});
|
||||
const computedData3 = computed(() => {
|
||||
if (computedData1.value) {
|
||||
return computedData2.value;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
computedData3.value;
|
||||
refData.value!.num = 4;
|
||||
refData.value = null;
|
||||
computedData3.value;
|
||||
expect(spyFunction).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should validate the computation of item status', () => {
|
||||
let statusMessage: string | undefined;
|
||||
|
||||
const itemList = ref<number[]>();
|
||||
const isNotEmpty = computed(() => {
|
||||
return !!itemList.value;
|
||||
});
|
||||
const status = computed(() => {
|
||||
if (isNotEmpty.value) {
|
||||
return 'Items are available';
|
||||
} else {
|
||||
return 'No items available';
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
statusMessage = status.value;
|
||||
});
|
||||
|
||||
itemList.value = [4, 5, 6];
|
||||
itemList.value = [7, 8, 9];
|
||||
itemList.value = undefined;
|
||||
|
||||
expect(statusMessage).toBe('No items available');
|
||||
});
|
||||
|
||||
it('chained computed dirty reallocation after trigger computed getter', () => {
|
||||
let _msg: string | undefined;
|
||||
|
||||
const items = ref<number[]>();
|
||||
const isLoaded = computed(() => {
|
||||
return !!items.value;
|
||||
});
|
||||
const msg = computed(() => {
|
||||
if (isLoaded.value) {
|
||||
return 'The items are loaded';
|
||||
} else {
|
||||
return 'The items are not loaded';
|
||||
}
|
||||
});
|
||||
|
||||
_msg = msg.value;
|
||||
items.value = [1, 2, 3];
|
||||
isLoaded.value; // <- trigger computed getter
|
||||
_msg = msg.value;
|
||||
items.value = undefined;
|
||||
_msg = msg.value;
|
||||
|
||||
expect(_msg).toBe('The items are not loaded');
|
||||
});
|
||||
|
||||
it('should trigger by the second computed that maybe dirty', () => {
|
||||
const cSpy = jest.fn();
|
||||
|
||||
const src1 = ref(0);
|
||||
const src2 = ref(0);
|
||||
const c1 = computed(() => src1.value);
|
||||
const c2 = computed(() => (src1.value % 2) + src2.value);
|
||||
const c3 = computed(() => {
|
||||
cSpy();
|
||||
c1.value;
|
||||
c2.value;
|
||||
});
|
||||
|
||||
c3.value;
|
||||
src1.value = 2;
|
||||
c3.value;
|
||||
expect(cSpy).toHaveBeenCalledTimes(2);
|
||||
src2.value = 1;
|
||||
c3.value;
|
||||
expect(cSpy).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should trigger the second effect', () => {
|
||||
const fnSpy = jest.fn();
|
||||
const v = ref(1);
|
||||
const c = computed(() => v.value);
|
||||
|
||||
watchEffect(() => {
|
||||
c.value;
|
||||
});
|
||||
watchEffect(() => {
|
||||
c.value;
|
||||
fnSpy();
|
||||
});
|
||||
|
||||
expect(fnSpy).toBeCalledTimes(1);
|
||||
v.value = 2;
|
||||
expect(fnSpy).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* 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 { vueReactive } from '../../../src';
|
||||
const { reactive, isReactive, toRaw, ref, isRef, computed, watchEffect } = vueReactive;
|
||||
|
||||
describe('test reactive', () => {
|
||||
it('should validate the reactivity of an object', () => {
|
||||
const original = { key1: 10 };
|
||||
const observed = reactive(original);
|
||||
expect(observed).not.toBe(original);
|
||||
expect(isReactive(observed)).toBe(true);
|
||||
expect(isReactive(original)).toBe(false);
|
||||
expect(observed.key1).toBe(10);
|
||||
expect('key1' in observed).toBe(true);
|
||||
expect(Object.keys(observed)).toEqual(['key1']);
|
||||
});
|
||||
|
||||
it('should validate the prototype reactivity', () => {
|
||||
const obj = {};
|
||||
const reactiveObj = reactive(obj);
|
||||
expect(isReactive(reactiveObj)).toBe(true);
|
||||
|
||||
const otherObj = { data: ['b'] };
|
||||
expect(isReactive(otherObj)).toBe(false);
|
||||
const reactiveOther = reactive(otherObj);
|
||||
expect(isReactive(reactiveOther)).toBe(true);
|
||||
expect(reactiveOther.data[0]).toBe('b');
|
||||
});
|
||||
|
||||
it('should validate nested reactivity', () => {
|
||||
const original = {
|
||||
nested: {
|
||||
key2: 10,
|
||||
},
|
||||
array: [{ key3: 20 }],
|
||||
};
|
||||
const observed = reactive(original);
|
||||
expect(isReactive(observed.nested)).toBe(true);
|
||||
expect(isReactive(observed.array)).toBe(true);
|
||||
expect(isReactive(observed.array[0])).toBe(true);
|
||||
});
|
||||
|
||||
it('should observe subtypes of IterableCollections (MyMap, MySet)', () => {
|
||||
class MyMap extends Map {}
|
||||
|
||||
const myMap = reactive(new MyMap());
|
||||
|
||||
expect(myMap).toBeInstanceOf(Map);
|
||||
expect(isReactive(myMap)).toBe(true);
|
||||
|
||||
myMap.set('newKey', {});
|
||||
expect(isReactive(myMap.get('newKey'))).toBe(true);
|
||||
|
||||
class MySet extends Set {}
|
||||
|
||||
const mySet = reactive(new MySet());
|
||||
|
||||
expect(mySet).toBeInstanceOf(Set);
|
||||
expect(isReactive(mySet)).toBe(true);
|
||||
|
||||
let testValue;
|
||||
watchEffect(() => (testValue = mySet.has('newValue')));
|
||||
expect(testValue).toBe(false);
|
||||
mySet.add('newValue');
|
||||
expect(testValue).toBe(true);
|
||||
mySet.delete('newValue');
|
||||
expect(testValue).toBe(false);
|
||||
});
|
||||
|
||||
it('should observe subtypes of WeakCollections (CustomWeakMap, CustomWeakSet)', () => {
|
||||
class CustomWeakMap extends WeakMap {}
|
||||
|
||||
const wmap = reactive(new CustomWeakMap());
|
||||
|
||||
expect(wmap).toBeInstanceOf(WeakMap);
|
||||
expect(isReactive(wmap)).toBe(true);
|
||||
|
||||
const customKey = {};
|
||||
wmap.set(customKey, {});
|
||||
expect(isReactive(wmap.get(customKey))).toBe(true);
|
||||
|
||||
class CustomWeakSet extends WeakSet {}
|
||||
|
||||
const wset = reactive(new CustomWeakSet());
|
||||
|
||||
expect(wset).toBeInstanceOf(WeakSet);
|
||||
expect(isReactive(wset)).toBe(true);
|
||||
|
||||
let testValue;
|
||||
watchEffect(() => (testValue = wset.has(customKey)));
|
||||
expect(testValue).toBe(false);
|
||||
wset.add(customKey);
|
||||
expect(testValue).toBe(true);
|
||||
wset.delete(customKey);
|
||||
expect(testValue).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate that changes in the observed value are reflected in the original (Object)', () => {
|
||||
const original: any = { baz: 5 };
|
||||
const observed = reactive(original);
|
||||
|
||||
observed.qux = 7;
|
||||
expect(observed.qux).toBe(7);
|
||||
expect(original.qux).toBe(7);
|
||||
|
||||
delete observed.baz;
|
||||
expect('baz' in observed).toBe(false);
|
||||
expect('baz' in original).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate that changes in the original value are reflected in the observed value (Object)', () => {
|
||||
const initialData: any = { key1: 2 };
|
||||
const reactiveData = reactive(initialData);
|
||||
|
||||
initialData.key2 = 3;
|
||||
expect(initialData.key2).toBe(3);
|
||||
expect(reactiveData.key2).toBe(3);
|
||||
|
||||
delete initialData.key1;
|
||||
expect('key1' in initialData).toBe(false);
|
||||
expect('key1' in reactiveData).toBe(false);
|
||||
});
|
||||
|
||||
it('should verify that assigning an unobserved value to a property results in a reactive wrap', () => {
|
||||
const reactiveObj = reactive<{ key?: object }>({});
|
||||
const rawObj = {};
|
||||
reactiveObj.key = rawObj;
|
||||
expect(reactiveObj.key).not.toBe(rawObj);
|
||||
expect(isReactive(reactiveObj.key)).toBe(true);
|
||||
});
|
||||
|
||||
it('should affirm that reactivity checks on an already reactive object yield the same Proxy', () => {
|
||||
const initialData = { key: 3 };
|
||||
const reactiveData1 = reactive(initialData);
|
||||
const reactiveData2 = reactive(reactiveData1);
|
||||
expect(reactiveData2).toBe(reactiveData1);
|
||||
});
|
||||
|
||||
it('should confirm that multiple observations of the same value return identical Proxies', () => {
|
||||
const initialData = { key: 2 };
|
||||
const reactiveData1 = reactive(initialData);
|
||||
const reactiveData2 = reactive(initialData);
|
||||
expect(reactiveData2).toBe(reactiveData1);
|
||||
});
|
||||
|
||||
it('should ensure original object remains unaffected by Proxies', () => {
|
||||
const initialObject: any = { key: 3 };
|
||||
const secondaryObject = { key2: 4 };
|
||||
const reactiveObject1 = reactive(initialObject);
|
||||
const reactiveObject2 = reactive(secondaryObject);
|
||||
reactiveObject1.key2 = reactiveObject2;
|
||||
expect(reactiveObject1.key2).toBe(reactiveObject2);
|
||||
});
|
||||
|
||||
it('should ensure that mutations on objects using reactive as prototype do not trigger', () => {
|
||||
const reactiveObject = reactive({ key: 1 });
|
||||
const originalObject = Object.create(reactiveObject);
|
||||
let testValue;
|
||||
watchEffect(() => (testValue = originalObject.key));
|
||||
expect(testValue).toBe(1);
|
||||
reactiveObject.key = 3;
|
||||
expect(testValue).toBe(3);
|
||||
});
|
||||
|
||||
it('should validate the identity of the original object after toRaw operation', () => {
|
||||
const initialObject = { key: 2 };
|
||||
const reactiveObject = reactive(initialObject);
|
||||
expect(toRaw(reactiveObject)).toBe(initialObject);
|
||||
expect(toRaw(initialObject)).toBe(initialObject);
|
||||
});
|
||||
|
||||
it('should validate the non-mutability of original object when wrapped by user Proxy', () => {
|
||||
const initialObject = {};
|
||||
const reactiveObject = reactive(initialObject);
|
||||
const proxyObject = new Proxy(reactiveObject, {});
|
||||
const rawObject = toRaw(proxyObject);
|
||||
expect(rawObject).toBe(initialObject);
|
||||
});
|
||||
|
||||
it('should confirm the non-unwrapping of Ref<T>', () => {
|
||||
const alphaRef = reactive(ref(2));
|
||||
const betaRef = reactive(ref({ key: 2 }));
|
||||
|
||||
expect(isRef(alphaRef)).toBe(true);
|
||||
expect(isRef(betaRef)).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate the property reassignment from one ref to another', () => {
|
||||
const alpha = ref(2);
|
||||
const beta = ref(3);
|
||||
const observedObject = reactive({ key: alpha });
|
||||
const computedValue = computed(() => observedObject.key);
|
||||
expect(computedValue.value).toBe(2);
|
||||
|
||||
// @ts-expect-error
|
||||
observedObject.key = beta;
|
||||
expect(computedValue.value).toBe(3);
|
||||
|
||||
beta.value += 2;
|
||||
expect(computedValue.value).toBe(5);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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 { vueReactive, RefType } from '../../../src';
|
||||
|
||||
const { ref, isRef, isReactive, reactive, watchEffect, unref, shallowRef, isShallow, computed } = vueReactive;
|
||||
|
||||
describe('test ref', () => {
|
||||
it('should validate the value holding capability', () => {
|
||||
const testRef = ref(3);
|
||||
expect(testRef.value).toBe(3);
|
||||
testRef.value = 4;
|
||||
expect(testRef.value).toBe(4);
|
||||
});
|
||||
|
||||
it('should maintain reactivity', () => {
|
||||
const testRef = ref(3);
|
||||
let testVar;
|
||||
const testFn = jest.fn(() => {
|
||||
testVar = testRef.value;
|
||||
});
|
||||
watchEffect(testFn);
|
||||
expect(testFn).toHaveBeenCalledTimes(1);
|
||||
expect(testVar).toBe(3);
|
||||
testRef.value = 4;
|
||||
expect(testFn).toHaveBeenCalledTimes(2);
|
||||
expect(testVar).toBe(4);
|
||||
testRef.value = 4;
|
||||
expect(testFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should verify the reactivity of nested properties', () => {
|
||||
const testRef = ref({
|
||||
num: 3,
|
||||
});
|
||||
let testValue;
|
||||
watchEffect(() => {
|
||||
testValue = testRef.value.num;
|
||||
});
|
||||
expect(testValue).toBe(3);
|
||||
testRef.value.num = 4;
|
||||
expect(testValue).toBe(4);
|
||||
});
|
||||
|
||||
it('should function correctly without an initial value', () => {
|
||||
const testRef = ref();
|
||||
let testValue;
|
||||
watchEffect(() => {
|
||||
testValue = testRef.value;
|
||||
});
|
||||
expect(testValue).toBe(undefined);
|
||||
testRef.value = 3;
|
||||
expect(testValue).toBe(3);
|
||||
});
|
||||
|
||||
it('should operate as a standard property when nested within a reactive structure', () => {
|
||||
const initialRef = ref(2);
|
||||
const reactiveObj = reactive({
|
||||
initialRef,
|
||||
nested: {
|
||||
innerRef: initialRef,
|
||||
},
|
||||
});
|
||||
|
||||
let first: number;
|
||||
let second: number;
|
||||
|
||||
watchEffect(() => {
|
||||
first = reactiveObj.initialRef;
|
||||
second = reactiveObj.nested.innerRef;
|
||||
});
|
||||
|
||||
const validateDummies = (val: number) => [first, second].forEach(dummy => expect(dummy).toBe(val));
|
||||
|
||||
validateDummies(2);
|
||||
initialRef.value += 2;
|
||||
validateDummies(4);
|
||||
reactiveObj.initialRef += 2;
|
||||
validateDummies(6);
|
||||
reactiveObj.nested.innerRef += 2;
|
||||
validateDummies(8);
|
||||
});
|
||||
|
||||
it('should confirm nested ref types', () => {
|
||||
const primaryRef = ref(2);
|
||||
const secondaryRef = ref(primaryRef);
|
||||
|
||||
expect(typeof (secondaryRef.value + 3)).toBe('number');
|
||||
});
|
||||
|
||||
it('should validate nested values in ref types', () => {
|
||||
const data = {
|
||||
key: ref(2),
|
||||
};
|
||||
|
||||
const refData = ref(data);
|
||||
|
||||
expect(typeof (refData.value.key + 3)).toBe('number');
|
||||
});
|
||||
|
||||
it('should validate ref types within array structures', () => {
|
||||
const arrayData = ref([2, ref(4)]).value;
|
||||
expect(isRef(arrayData[0])).toBe(false);
|
||||
expect(isRef(arrayData[1])).toBe(true);
|
||||
expect((arrayData[1] as RefType).value).toBe(4);
|
||||
});
|
||||
|
||||
it('should preserve tuple data types', () => {
|
||||
const tupleData: [number, string, { a: number }, () => number, RefType<number>] = [
|
||||
0,
|
||||
'1',
|
||||
{ a: 1 },
|
||||
() => 0,
|
||||
ref(0),
|
||||
];
|
||||
const refTuple = ref(tupleData);
|
||||
|
||||
refTuple.value[0] += 1;
|
||||
expect(refTuple.value[0]).toEqual(1);
|
||||
refTuple.value[1] = refTuple.value[1].concat('1');
|
||||
expect(refTuple.value[1]).toEqual('11');
|
||||
refTuple.value[2].a += 1;
|
||||
expect(refTuple.value[2].a).toEqual(2);
|
||||
expect(refTuple.value[3]()).toEqual(0);
|
||||
refTuple.value[4].value += 1;
|
||||
expect(refTuple.value[4].value).toEqual(1);
|
||||
});
|
||||
|
||||
it('should correctly unref values', () => {
|
||||
expect(unref(1)).toEqual(1);
|
||||
expect(unref(ref(1))).toEqual(1);
|
||||
});
|
||||
|
||||
it('should verify the reactivity of a shallowRef', () => {
|
||||
const shallowReference = shallowRef({ key: 1 });
|
||||
expect(isReactive(shallowReference.value)).toBe(false);
|
||||
|
||||
let result;
|
||||
watchEffect(() => {
|
||||
result = shallowReference.value.key;
|
||||
});
|
||||
|
||||
expect(result).toBe(1);
|
||||
|
||||
shallowReference.value = { key: 2 };
|
||||
expect(isReactive(shallowReference.value)).toBe(false);
|
||||
expect(result).toBe(2);
|
||||
});
|
||||
|
||||
it('should be isShallow', () => {
|
||||
const shallowReference = shallowRef({ key: 1 });
|
||||
expect(isShallow(shallowReference)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when isRef is called with a ref', () => {
|
||||
const testRef = ref(1);
|
||||
expect(isRef(testRef)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when isRef is called with a computed ref', () => {
|
||||
const computedRef = computed(() => 1);
|
||||
expect(isRef(computedRef)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when isRef is called with non-ref values', () => {
|
||||
expect(isRef(0)).toBe(false);
|
||||
expect(isRef(1)).toBe(false);
|
||||
const obj = { value: 0 };
|
||||
expect(isRef(obj)).toBe(false);
|
||||
});
|
||||
|
||||
it('should ref not react when assigned the same proxy', () => {
|
||||
const reactiveObj = reactive({ num: 0 });
|
||||
|
||||
const refInstance = ref(reactiveObj);
|
||||
const watchFn1 = jest.fn(() => refInstance.value);
|
||||
|
||||
watchEffect(watchFn1);
|
||||
|
||||
refInstance.value = reactiveObj;
|
||||
expect(watchFn1).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should shallowRef not react when assigned the same proxy', () => {
|
||||
const reactiveObj = reactive({ num: 0 });
|
||||
|
||||
const shallowRefInstance = shallowRef(reactiveObj);
|
||||
const watchFn2 = jest.fn(() => shallowRefInstance.value);
|
||||
|
||||
watchEffect(watchFn2);
|
||||
|
||||
shallowRefInstance.value = reactiveObj;
|
||||
expect(watchFn2).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
* 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, vueReactive, RefType, unmountComponentAtNode } from '../../../src';
|
||||
import { Text, triggerClickEvent } from '../../jest/commonComponents';
|
||||
import * as Inula from '../../../src';
|
||||
|
||||
const { useReactive, useReference, useComputed, useWatch } = vueReactive;
|
||||
|
||||
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,338 @@
|
|||
/*
|
||||
* 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 { vueReactive } from '../../../src';
|
||||
|
||||
const { ref, reactive, watch, computed } = vueReactive;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,613 @@
|
|||
/*
|
||||
* 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 { vueReactive } from '../../../src';
|
||||
|
||||
const { reactive, toRaw, watchEffect } = vueReactive;
|
||||
|
||||
function stop(stopHandle: () => void) {
|
||||
stopHandle();
|
||||
}
|
||||
|
||||
describe('test watchEffect', () => {
|
||||
it('should run the passed function once (wrapped by a effect)', () => {
|
||||
const fnSpy = jest.fn();
|
||||
watchEffect(fnSpy);
|
||||
expect(fnSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should observe basic properties', () => {
|
||||
let dummy;
|
||||
const counter = reactive({ num: 0 });
|
||||
watchEffect(() => {
|
||||
dummy = counter.num;
|
||||
});
|
||||
|
||||
expect(dummy).toBe(0);
|
||||
counter.num = 7;
|
||||
expect(dummy).toBe(7);
|
||||
});
|
||||
|
||||
it('should observe multiple properties', () => {
|
||||
let dummy;
|
||||
const counter = reactive({ num1: 0, num2: 0 });
|
||||
watchEffect(() => (dummy = counter.num1 + counter.num1 + counter.num2));
|
||||
|
||||
expect(dummy).toBe(0);
|
||||
counter.num1 = counter.num2 = 7;
|
||||
expect(dummy).toBe(21);
|
||||
});
|
||||
|
||||
it('should handle multiple effects', () => {
|
||||
let dummy1, dummy2;
|
||||
const counter = reactive({ num: 0 });
|
||||
watchEffect(() => (dummy1 = counter.num));
|
||||
watchEffect(() => (dummy2 = counter.num));
|
||||
|
||||
expect(dummy1).toBe(0);
|
||||
expect(dummy2).toBe(0);
|
||||
counter.num++;
|
||||
expect(dummy1).toBe(1);
|
||||
expect(dummy2).toBe(1);
|
||||
});
|
||||
|
||||
it('should observe nested properties', () => {
|
||||
let dummy;
|
||||
const counter = reactive({ nested: { num: 0 } });
|
||||
watchEffect(() => (dummy = counter.nested.num));
|
||||
|
||||
expect(dummy).toBe(0);
|
||||
counter.nested.num = 8;
|
||||
expect(dummy).toBe(8);
|
||||
});
|
||||
|
||||
it('should observe delete operations', () => {
|
||||
let dummy;
|
||||
const obj = reactive<{
|
||||
prop?: string;
|
||||
}>({ prop: 'value' });
|
||||
watchEffect(() => (dummy = obj.prop));
|
||||
|
||||
expect(dummy).toBe('value');
|
||||
delete obj.prop;
|
||||
expect(dummy).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should observe has operations', () => {
|
||||
let dummy;
|
||||
const obj = reactive<{ prop?: string | number }>({ prop: 'value' });
|
||||
watchEffect(() => {
|
||||
dummy = 'prop' in obj;
|
||||
});
|
||||
|
||||
expect(dummy).toBe(true);
|
||||
delete obj.prop;
|
||||
expect(dummy).toBe(false);
|
||||
obj.prop = 12;
|
||||
expect(dummy).toBe(true);
|
||||
});
|
||||
|
||||
it('should observe properties on the prototype chain', () => {
|
||||
let dummy;
|
||||
const counter = reactive<{ num?: number }>({ num: 0 });
|
||||
const parentCounter = reactive({ num: 2 });
|
||||
Object.setPrototypeOf(counter, parentCounter);
|
||||
watchEffect(() => (dummy = counter.num));
|
||||
|
||||
expect(dummy).toBe(0);
|
||||
delete counter.num;
|
||||
expect(dummy).toBe(2);
|
||||
parentCounter.num = 4;
|
||||
expect(dummy).toBe(4);
|
||||
counter.num = 3;
|
||||
expect(dummy).toBe(3);
|
||||
});
|
||||
|
||||
it('should observe has operations on the prototype chain', () => {
|
||||
let dummy;
|
||||
const counter = reactive<{ num?: number }>({ num: 0 });
|
||||
const parentCounter = reactive<{ num?: number }>({ num: 2 });
|
||||
Object.setPrototypeOf(counter, parentCounter);
|
||||
watchEffect(() => (dummy = 'num' in counter));
|
||||
|
||||
expect(dummy).toBe(true);
|
||||
delete counter.num;
|
||||
expect(dummy).toBe(true);
|
||||
delete parentCounter.num;
|
||||
expect(dummy).toBe(false);
|
||||
counter.num = 3;
|
||||
expect(dummy).toBe(true);
|
||||
});
|
||||
|
||||
it('should observe inherited property accessors', () => {
|
||||
let dummy, parentDummy, hiddenValue: any;
|
||||
const obj = reactive<{ prop?: number }>({});
|
||||
const parent = reactive({
|
||||
set prop(value) {
|
||||
hiddenValue = value;
|
||||
},
|
||||
get prop() {
|
||||
return hiddenValue;
|
||||
},
|
||||
});
|
||||
Object.setPrototypeOf(obj, parent);
|
||||
watchEffect(() => (dummy = obj.prop));
|
||||
watchEffect(() => (parentDummy = parent.prop));
|
||||
|
||||
expect(dummy).toBe(undefined);
|
||||
expect(parentDummy).toBe(undefined);
|
||||
obj.prop = 4;
|
||||
expect(dummy).toBe(4);
|
||||
// this doesn't work, should it?
|
||||
// expect(parentDummy).toBe(4)
|
||||
parent.prop = 2;
|
||||
expect(dummy).toBe(2);
|
||||
expect(parentDummy).toBe(2);
|
||||
});
|
||||
|
||||
it('should observe function call chains', () => {
|
||||
let dummy;
|
||||
const counter = reactive({ num: 0 });
|
||||
watchEffect(() => (dummy = getNum()));
|
||||
|
||||
function getNum() {
|
||||
return counter.num;
|
||||
}
|
||||
|
||||
expect(dummy).toBe(0);
|
||||
counter.num = 2;
|
||||
expect(dummy).toBe(2);
|
||||
});
|
||||
|
||||
it('should observe iteration', () => {
|
||||
let dummy;
|
||||
const list = reactive(['Hello']);
|
||||
watchEffect(() => (dummy = list.join(' ')));
|
||||
|
||||
expect(dummy).toBe('Hello');
|
||||
list.push('World!');
|
||||
expect(dummy).toBe('Hello World!');
|
||||
list.shift();
|
||||
expect(dummy).toBe('World!');
|
||||
});
|
||||
|
||||
it('should observe implicit array length changes', () => {
|
||||
let dummy;
|
||||
const list = reactive(['Hello']);
|
||||
watchEffect(() => (dummy = list.join(' ')));
|
||||
|
||||
expect(dummy).toBe('Hello');
|
||||
list[1] = 'World!';
|
||||
expect(dummy).toBe('Hello World!');
|
||||
list[3] = 'Hello!';
|
||||
expect(dummy).toBe('Hello World! Hello!');
|
||||
});
|
||||
|
||||
it('should observe sparse array mutations', () => {
|
||||
let dummy;
|
||||
const list = reactive<string[]>([]);
|
||||
list[1] = 'World!';
|
||||
watchEffect(() => (dummy = list.join(' ')));
|
||||
|
||||
expect(dummy).toBe(' World!');
|
||||
list[0] = 'Hello';
|
||||
expect(dummy).toBe('Hello World!');
|
||||
list.pop();
|
||||
expect(dummy).toBe('Hello');
|
||||
});
|
||||
|
||||
it('should observe enumeration', () => {
|
||||
let dummy = 0;
|
||||
const numbers = reactive<Record<string, number>>({ num1: 3 });
|
||||
watchEffect(() => {
|
||||
dummy = 0;
|
||||
for (const key in numbers) {
|
||||
dummy += numbers[key];
|
||||
}
|
||||
});
|
||||
|
||||
expect(dummy).toBe(3);
|
||||
numbers.num2 = 4;
|
||||
expect(dummy).toBe(7);
|
||||
delete numbers.num1;
|
||||
expect(dummy).toBe(4);
|
||||
});
|
||||
|
||||
it('should observe symbol keyed properties', () => {
|
||||
const key = Symbol('symbol keyed prop');
|
||||
let dummy, hasDummy;
|
||||
const obj = reactive<{ [key]?: string }>({ [key]: 'value' });
|
||||
watchEffect(() => (dummy = obj[key]));
|
||||
watchEffect(() => (hasDummy = key in obj));
|
||||
|
||||
expect(dummy).toBe('value');
|
||||
expect(hasDummy).toBe(true);
|
||||
obj[key] = 'newValue';
|
||||
expect(dummy).toBe('newValue');
|
||||
delete obj[key];
|
||||
expect(dummy).toBe(undefined);
|
||||
expect(hasDummy).toBe(false);
|
||||
});
|
||||
|
||||
it('should not observe well-known symbol keyed properties', () => {
|
||||
const key = Symbol.isConcatSpreadable;
|
||||
let dummy;
|
||||
const array: any = reactive([]);
|
||||
watchEffect(() => (dummy = array[key]));
|
||||
|
||||
expect(array[key]).toBe(undefined);
|
||||
expect(dummy).toBe(undefined);
|
||||
array[key] = true;
|
||||
expect(array[key]).toBe(true);
|
||||
expect(dummy).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should observe function valued properties', () => {
|
||||
const oldFunc = () => {};
|
||||
const newFunc = () => {};
|
||||
|
||||
let dummy;
|
||||
const obj = reactive({ func: oldFunc });
|
||||
watchEffect(() => {
|
||||
dummy = obj.func;
|
||||
});
|
||||
|
||||
expect(dummy).toBe(oldFunc);
|
||||
obj.func = newFunc;
|
||||
expect(dummy).toBe(newFunc);
|
||||
});
|
||||
|
||||
it('should observe chained getters relying on this', () => {
|
||||
const obj = reactive({
|
||||
a: 1,
|
||||
get b() {
|
||||
return this.a;
|
||||
},
|
||||
});
|
||||
|
||||
let dummy;
|
||||
watchEffect(() => (dummy = obj.b));
|
||||
expect(dummy).toBe(1);
|
||||
obj.a++;
|
||||
expect(dummy).toBe(2);
|
||||
});
|
||||
|
||||
it('should observe methods relying on this', () => {
|
||||
const obj = reactive({
|
||||
a: 1,
|
||||
b() {
|
||||
return this.a;
|
||||
},
|
||||
});
|
||||
|
||||
let dummy;
|
||||
watchEffect(() => (dummy = obj.b()));
|
||||
expect(dummy).toBe(1);
|
||||
obj.a++;
|
||||
expect(dummy).toBe(2);
|
||||
});
|
||||
|
||||
it('should not observe set operations without a value change', () => {
|
||||
let hasDummy, getDummy;
|
||||
const obj = reactive({ prop: 'value' });
|
||||
|
||||
const getSpy = jest.fn(() => (getDummy = obj.prop));
|
||||
const hasSpy = jest.fn(() => (hasDummy = 'prop' in obj));
|
||||
watchEffect(getSpy);
|
||||
watchEffect(hasSpy);
|
||||
|
||||
expect(getDummy).toBe('value');
|
||||
expect(hasDummy).toBe(true);
|
||||
obj.prop = 'value';
|
||||
expect(getSpy).toHaveBeenCalledTimes(1);
|
||||
expect(hasSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getDummy).toBe('value');
|
||||
expect(hasDummy).toBe(true);
|
||||
});
|
||||
|
||||
it('should not observe raw mutations', () => {
|
||||
let dummy;
|
||||
const obj = reactive<{ prop?: string }>({});
|
||||
watchEffect(() => (dummy = toRaw(obj).prop));
|
||||
|
||||
expect(dummy).toBe(undefined);
|
||||
obj.prop = 'value';
|
||||
expect(dummy).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should not be triggered by raw mutations', () => {
|
||||
let dummy;
|
||||
const obj = reactive<{ prop?: string }>({});
|
||||
watchEffect(() => (dummy = obj.prop));
|
||||
|
||||
expect(dummy).toBe(undefined);
|
||||
toRaw(obj).prop = 'value';
|
||||
expect(dummy).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should not be triggered by inherited raw setters', () => {
|
||||
let dummy, parentDummy, hiddenValue: any;
|
||||
const obj = reactive<{ prop?: number }>({});
|
||||
const parent = reactive({
|
||||
set prop(value) {
|
||||
hiddenValue = value;
|
||||
},
|
||||
get prop() {
|
||||
return hiddenValue;
|
||||
},
|
||||
});
|
||||
Object.setPrototypeOf(obj, parent);
|
||||
watchEffect(() => (dummy = obj.prop));
|
||||
watchEffect(() => (parentDummy = parent.prop));
|
||||
|
||||
expect(dummy).toBe(undefined);
|
||||
expect(parentDummy).toBe(undefined);
|
||||
toRaw(obj).prop = 4;
|
||||
expect(dummy).toBe(undefined);
|
||||
expect(parentDummy).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should avoid implicit infinite recursive loops with itself', () => {
|
||||
const counter = reactive({ num: 0 });
|
||||
|
||||
const counterSpy = jest.fn(() => {
|
||||
counter.num++;
|
||||
});
|
||||
watchEffect(counterSpy);
|
||||
expect(counter.num).toBe(1);
|
||||
expect(counterSpy).toHaveBeenCalledTimes(1);
|
||||
counter.num = 4;
|
||||
expect(counter.num).toBe(5);
|
||||
expect(counterSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should allow explicitly recursive raw function loops', () => {
|
||||
const counter = reactive({ num: 0 });
|
||||
const numSpy = jest.fn(() => {
|
||||
counter.num++;
|
||||
if (counter.num < 10) {
|
||||
numSpy();
|
||||
}
|
||||
});
|
||||
watchEffect(numSpy);
|
||||
expect(counter.num).toEqual(10);
|
||||
expect(numSpy).toHaveBeenCalledTimes(10);
|
||||
});
|
||||
|
||||
it('should avoid infinite loops with other effects', () => {
|
||||
const nums = reactive({ num1: 0, num2: 1 });
|
||||
|
||||
const spy1 = jest.fn(() => (nums.num1 = nums.num2));
|
||||
const spy2 = jest.fn(() => (nums.num2 = nums.num1));
|
||||
watchEffect(spy1);
|
||||
watchEffect(spy2);
|
||||
expect(nums.num1).toBe(1);
|
||||
expect(nums.num2).toBe(1);
|
||||
expect(spy1).toHaveBeenCalledTimes(1);
|
||||
expect(spy2).toHaveBeenCalledTimes(1);
|
||||
nums.num2 = 4;
|
||||
expect(nums.num1).toBe(4);
|
||||
expect(nums.num2).toBe(4);
|
||||
expect(spy1).toHaveBeenCalledTimes(2);
|
||||
expect(spy2).toHaveBeenCalledTimes(2);
|
||||
nums.num1 = 10;
|
||||
expect(nums.num1).toBe(10);
|
||||
expect(nums.num2).toBe(10);
|
||||
expect(spy1).toHaveBeenCalledTimes(3);
|
||||
expect(spy2).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should return a new reactive version of the function', () => {
|
||||
function greet() {
|
||||
return 'Hello World';
|
||||
}
|
||||
|
||||
const effect1 = watchEffect(greet);
|
||||
const effect2 = watchEffect(greet);
|
||||
expect(typeof effect1).toBe('function');
|
||||
expect(typeof effect2).toBe('function');
|
||||
expect(effect1).not.toBe(greet);
|
||||
expect(effect1).not.toBe(effect2);
|
||||
});
|
||||
|
||||
it('should discover new branches while running automatically', () => {
|
||||
let dummy;
|
||||
const obj = reactive({ prop: 'value', run: false });
|
||||
|
||||
const conditionalSpy = jest.fn(() => {
|
||||
dummy = obj.run ? obj.prop : 'other';
|
||||
});
|
||||
watchEffect(conditionalSpy);
|
||||
|
||||
expect(dummy).toBe('other');
|
||||
expect(conditionalSpy).toHaveBeenCalledTimes(1);
|
||||
obj.prop = 'Hi';
|
||||
expect(dummy).toBe('other');
|
||||
expect(conditionalSpy).toHaveBeenCalledTimes(1);
|
||||
obj.run = true;
|
||||
expect(dummy).toBe('Hi');
|
||||
expect(conditionalSpy).toHaveBeenCalledTimes(2);
|
||||
obj.prop = 'World';
|
||||
expect(dummy).toBe('World');
|
||||
expect(conditionalSpy).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should not be triggered by mutating a property, which is used in an inactive branch', () => {
|
||||
let dummy;
|
||||
const obj = reactive({ prop: 'value', run: true });
|
||||
|
||||
const conditionalSpy = jest.fn(() => {
|
||||
dummy = obj.run ? obj.prop : 'other';
|
||||
});
|
||||
watchEffect(conditionalSpy);
|
||||
|
||||
expect(dummy).toBe('value');
|
||||
expect(conditionalSpy).toHaveBeenCalledTimes(1);
|
||||
obj.run = false;
|
||||
expect(dummy).toBe('other');
|
||||
expect(conditionalSpy).toHaveBeenCalledTimes(2);
|
||||
obj.prop = 'value2';
|
||||
expect(dummy).toBe('other');
|
||||
expect(conditionalSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should handle deep effect recursion using cleanup fallback', () => {
|
||||
const results = reactive([0]);
|
||||
const effects = [];
|
||||
for (let i = 1; i < 40; i++) {
|
||||
(index => {
|
||||
const fx = watchEffect(() => {
|
||||
results[index] = results[index - 1] * 2;
|
||||
});
|
||||
effects.push({ fx, index });
|
||||
})(i);
|
||||
}
|
||||
|
||||
expect(results[39]).toBe(0);
|
||||
results[0] = 1;
|
||||
expect(results[39]).toBe(Math.pow(2, 39));
|
||||
});
|
||||
|
||||
it('should run multiple times for a single mutation', () => {
|
||||
let dummy;
|
||||
const obj = reactive<Record<string, number>>({});
|
||||
const fnSpy = jest.fn(() => {
|
||||
for (const key in obj) {
|
||||
dummy = obj[key];
|
||||
}
|
||||
dummy = obj.prop;
|
||||
});
|
||||
watchEffect(fnSpy);
|
||||
|
||||
expect(fnSpy).toHaveBeenCalledTimes(1);
|
||||
obj.prop = 16;
|
||||
expect(dummy).toBe(16);
|
||||
expect(fnSpy).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should observe class method invocations', () => {
|
||||
class Model {
|
||||
count: number;
|
||||
|
||||
constructor() {
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
inc() {
|
||||
this.count++;
|
||||
}
|
||||
}
|
||||
|
||||
const model = reactive(new Model());
|
||||
let dummy;
|
||||
watchEffect(() => {
|
||||
dummy = model.count;
|
||||
});
|
||||
expect(dummy).toBe(0);
|
||||
model.inc();
|
||||
expect(dummy).toBe(1);
|
||||
});
|
||||
|
||||
it('stop', () => {
|
||||
let dummy;
|
||||
const obj = reactive({ prop: 1 });
|
||||
const runner = watchEffect(() => {
|
||||
dummy = obj.prop;
|
||||
});
|
||||
obj.prop = 2;
|
||||
expect(dummy).toBe(2);
|
||||
stop(runner);
|
||||
obj.prop = 3;
|
||||
expect(dummy).toBe(2);
|
||||
});
|
||||
|
||||
it('stop: a stopped effect is nested in a normal effect', () => {
|
||||
let dummy;
|
||||
const obj = reactive({ prop: 1 });
|
||||
const runner = watchEffect(() => {
|
||||
dummy = obj.prop;
|
||||
});
|
||||
runner();
|
||||
obj.prop = 2;
|
||||
expect(dummy).toBe(1);
|
||||
});
|
||||
|
||||
it('should trigger all effects when array length is set to 0', () => {
|
||||
const observed: any = reactive([1]);
|
||||
let dummy, record;
|
||||
watchEffect(() => {
|
||||
dummy = observed.length;
|
||||
});
|
||||
watchEffect(() => {
|
||||
record = observed[0];
|
||||
});
|
||||
expect(dummy).toBe(1);
|
||||
expect(record).toBe(1);
|
||||
|
||||
observed[1] = 2;
|
||||
expect(observed[1]).toBe(2);
|
||||
|
||||
observed.unshift(3);
|
||||
expect(dummy).toBe(3);
|
||||
expect(record).toBe(3);
|
||||
|
||||
observed.length = 0;
|
||||
expect(dummy).toBe(0);
|
||||
expect(record).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should be triggered when set length with string', () => {
|
||||
let ret1 = 'idle';
|
||||
let ret2 = 'idle';
|
||||
const arr1 = reactive(new Array(11).fill(0));
|
||||
const arr2 = reactive(new Array(11).fill(0));
|
||||
watchEffect(() => {
|
||||
ret1 = arr1[10] === undefined ? 'arr[10] is set to empty' : 'idle';
|
||||
});
|
||||
watchEffect(() => {
|
||||
ret2 = arr2[10] === undefined ? 'arr[10] is set to empty' : 'idle';
|
||||
});
|
||||
arr1.length = 2;
|
||||
arr2.length = '2' as any;
|
||||
expect(ret1).toBe(ret2);
|
||||
});
|
||||
|
||||
it('should track hasOwnProperty', () => {
|
||||
const obj: any = reactive({});
|
||||
let has = false;
|
||||
const fnSpy = jest.fn();
|
||||
|
||||
watchEffect(() => {
|
||||
fnSpy();
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
has = obj.hasOwnProperty('foo');
|
||||
});
|
||||
expect(fnSpy).toHaveBeenCalledTimes(1);
|
||||
expect(has).toBe(false);
|
||||
|
||||
obj.foo = 1;
|
||||
expect(fnSpy).toHaveBeenCalledTimes(2);
|
||||
expect(has).toBe(true);
|
||||
|
||||
delete obj.foo;
|
||||
expect(fnSpy).toHaveBeenCalledTimes(3);
|
||||
expect(has).toBe(false);
|
||||
|
||||
// should not trigger on unrelated key
|
||||
obj.bar = 2;
|
||||
expect(fnSpy).toHaveBeenCalledTimes(3);
|
||||
expect(has).toBe(false);
|
||||
});
|
||||
});
|
|
@ -119,7 +119,7 @@ describe('测试store中的Array', () => {
|
|||
});
|
||||
|
||||
it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => {
|
||||
let globalStore = useUserStore();
|
||||
const globalStore = useUserStore();
|
||||
function Child(props) {
|
||||
const userStore = useUserStore();
|
||||
|
||||
|
@ -146,8 +146,7 @@ describe('测试store中的Array', () => {
|
|||
expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// shift
|
||||
//@ts-ignore TODO:why is this argument here?
|
||||
globalStore.$s.persons.shift({ name: 'p0', age: 0 });
|
||||
globalStore.$s.persons.shift();
|
||||
expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3');
|
||||
|
||||
// 赋值[2]
|
||||
|
@ -172,7 +171,7 @@ describe('测试store中的Array', () => {
|
|||
});
|
||||
|
||||
it('测试Array方法: forEach()', () => {
|
||||
let globalStore = useUserStore();
|
||||
const globalStore = useUserStore();
|
||||
function Child(props) {
|
||||
const userStore = useUserStore();
|
||||
|
||||
|
|
|
@ -315,4 +315,60 @@ describe('测试store中的Set', () => {
|
|||
});
|
||||
expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: ');
|
||||
});
|
||||
|
||||
it('测试Set方法,add string', () => {
|
||||
const useUserStore = createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
persons: new Set(),
|
||||
},
|
||||
})();
|
||||
|
||||
useUserStore.persons.add('p1');
|
||||
expect(useUserStore.persons.has('p1')).toBe(true);
|
||||
});
|
||||
|
||||
it('测试Set方法,add obj', () => {
|
||||
const useUserStore = createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
persons: new Set(),
|
||||
},
|
||||
})();
|
||||
|
||||
const obj = { a: 1 };
|
||||
useUserStore.persons.add(obj);
|
||||
expect(useUserStore.persons.has(obj)).toBe(true);
|
||||
});
|
||||
|
||||
it('测试Set方法,default value', () => {
|
||||
const obj = { b: 1 };
|
||||
const useUserStore = createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
persons: new Set(['p1', obj]),
|
||||
},
|
||||
})();
|
||||
|
||||
expect(useUserStore.persons.has('p1')).toBe(true);
|
||||
expect(useUserStore.persons.has(obj)).toBe(true);
|
||||
});
|
||||
|
||||
it('测试Set方法,watch', () => {
|
||||
const obj = { b: 1 };
|
||||
const useUserStore = createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
persons: new Set<any>(['p1', obj]),
|
||||
},
|
||||
})();
|
||||
|
||||
let dummy;
|
||||
const key = {};
|
||||
// effect(() => (dummy = cset.has(key)));
|
||||
useUserStore.persons.watch(() => (dummy = useUserStore.persons.has(key)));
|
||||
expect(dummy).toBe(undefined);
|
||||
useUserStore.persons.add(key);
|
||||
expect(dummy).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
//@ts-ignore
|
||||
import * as Inula from '../../../src/index';
|
||||
import * as LogUtils from '../../jest/logUtils';
|
||||
import { clearStore, createStore, useStore } from '../../../src/inulax/store/StoreHandler';
|
||||
|
|
|
@ -13,10 +13,11 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { createStore, watch } from '../../../src/index';
|
||||
import { createStore, watch, vueReactive } from '../../../src/index';
|
||||
const { watchEffect } = vueReactive;
|
||||
|
||||
describe('watch', () => {
|
||||
it('shouhld watch primitive state variable', async () => {
|
||||
it('should watch primitive state variable', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
variable: 'x',
|
||||
|
@ -38,7 +39,7 @@ describe('watch', () => {
|
|||
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
it('shouhld watch object variable', async () => {
|
||||
it('should watch object variable', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
variable: 'x',
|
||||
|
@ -60,7 +61,7 @@ describe('watch', () => {
|
|||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('shouhld watch array item', async () => {
|
||||
it('should watch array item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
arr: ['x'],
|
||||
|
@ -73,7 +74,7 @@ describe('watch', () => {
|
|||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
store.arr.watch('0', () => {
|
||||
store.$s.arr.watch('0', () => {
|
||||
counter++;
|
||||
});
|
||||
|
||||
|
@ -82,7 +83,7 @@ describe('watch', () => {
|
|||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('shouhld watch collection item', async () => {
|
||||
it('should watch collection item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
collection: new Map([['a', 'a']]),
|
||||
|
@ -141,4 +142,203 @@ describe('watch', () => {
|
|||
expect(counter1).toBe(3);
|
||||
expect(counterAll).toBe(6);
|
||||
});
|
||||
|
||||
it('should watch multiple variables independedntly', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
bool1: true,
|
||||
bool2: false,
|
||||
},
|
||||
actions: {
|
||||
toggle1: state => (state.bool1 = !state.bool1),
|
||||
toggle2: state => (state.bool2 = !state.bool2),
|
||||
},
|
||||
});
|
||||
|
||||
let counter1 = 0;
|
||||
let counterAll = 0;
|
||||
const store = useStore();
|
||||
|
||||
watch(store.$s, () => {
|
||||
counterAll++;
|
||||
});
|
||||
|
||||
store.$s.watch('bool1', () => {
|
||||
counter1++;
|
||||
});
|
||||
|
||||
store.toggle1();
|
||||
store.toggle1();
|
||||
|
||||
store.toggle2();
|
||||
|
||||
store.toggle1();
|
||||
|
||||
store.toggle2();
|
||||
store.toggle2();
|
||||
|
||||
expect(counter1).toBe(3);
|
||||
expect(counterAll).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('watchEffect', () => {
|
||||
it('should watchEffect obj item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
variable1: '1',
|
||||
variable2: '2',
|
||||
},
|
||||
actions: {
|
||||
change1: state => (state.variable1 = '11'),
|
||||
change2: state => (state.variable2 = '22'),
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
watchEffect(() => {
|
||||
store.variable1;
|
||||
counter++;
|
||||
});
|
||||
|
||||
expect(counter).toBe(1);
|
||||
|
||||
store.change1();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
|
||||
store.change2();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
|
||||
it('should watchEffect deep obj item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
obj: {
|
||||
a: 'x',
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
change: state => (state.obj.a = 'a'),
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
watchEffect(() => {
|
||||
store.obj.a;
|
||||
counter++;
|
||||
});
|
||||
|
||||
expect(counter).toBe(1);
|
||||
|
||||
store.change();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
|
||||
it('should watchEffect Map item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
collection: new Map([['a', 'a']]),
|
||||
},
|
||||
actions: {
|
||||
change: state => state.collection.set('a', 'x'),
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
watchEffect(() => {
|
||||
store.collection.get('a');
|
||||
counter++;
|
||||
});
|
||||
|
||||
expect(counter).toBe(1);
|
||||
|
||||
store.change();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
|
||||
it('should watchEffect Set item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
set: new Set(['a']),
|
||||
},
|
||||
actions: {
|
||||
change: state => state.set.delete('a'),
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
watchEffect(() => {
|
||||
store.set.has('a');
|
||||
counter++;
|
||||
});
|
||||
|
||||
expect(counter).toBe(1);
|
||||
|
||||
store.change();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
|
||||
it('should watchEffect WeakSet item', async () => {
|
||||
const obj = { a: 1 };
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
set: new WeakSet([obj]),
|
||||
},
|
||||
actions: {
|
||||
change: state => state.set.delete(obj),
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
watchEffect(() => {
|
||||
store.$s.set.has(obj);
|
||||
counter++;
|
||||
});
|
||||
|
||||
expect(counter).toBe(1);
|
||||
|
||||
store.change();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
|
||||
it('should watchEffect array item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
arr: ['x'],
|
||||
},
|
||||
actions: {
|
||||
change: state => (state.arr[0] = 'a'),
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
watchEffect(() => {
|
||||
store.arr[0];
|
||||
counter++;
|
||||
});
|
||||
|
||||
expect(counter).toBe(1);
|
||||
|
||||
store.change();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { createStore, useStore } from '../../../src/index';
|
||||
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
||||
import { createStore } from '../../../src/index';
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
describe('Using deep variables', () => {
|
||||
it('should listen to object variable change', () => {
|
||||
|
@ -100,6 +100,7 @@ describe('Using deep variables', () => {
|
|||
const key = Array.from(testStore.data.keys())[0];
|
||||
|
||||
expect(testStore.data.has(key)).toBe(true);
|
||||
expect(testStore.data.has(data.key)).toBe(true);
|
||||
|
||||
testStore.data.set(data.key, data.value);
|
||||
testStore.data.set(data.key, data.value);
|
||||
|
@ -158,6 +159,8 @@ describe('Using deep variables', () => {
|
|||
|
||||
testStore.data.set(data.key, data.value);
|
||||
|
||||
expect(testStore.data.has(data.key)).toBe(true);
|
||||
|
||||
let counter = 0;
|
||||
testStore.$subscribe(mutation => {
|
||||
counter++;
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
*/
|
||||
|
||||
import { createProxy } from '../../../src/inulax/proxy/ProxyHandler';
|
||||
import { readonlyProxy } from '../../../src/inulax/proxy/readonlyProxy';
|
||||
import { readonlyProxy } from '../../../src/inulax/proxy/ReadonlyProxy';
|
||||
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
||||
|
||||
describe('Proxy', () => {
|
||||
|
|
|
@ -56,8 +56,11 @@ import {
|
|||
isPortal,
|
||||
} from './external/InulaIs';
|
||||
import { createStore, useStore, clearStore } from './inulax/store/StoreHandler';
|
||||
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 } from './inulax/proxy/watch';
|
||||
import { watch, watchEffect, useWatch } from './inulax/reactive/Watch';
|
||||
import { computed, useComputed } from './inulax/reactive/Computed';
|
||||
import { act } from './external/TestUtil';
|
||||
|
||||
import {
|
||||
|
@ -70,7 +73,25 @@ import {
|
|||
} from './dom/DOMExternal';
|
||||
|
||||
import { syncUpdates as flushSync } from './renderer/TreeBuilder';
|
||||
import { toRaw } from './inulax/proxy/ProxyHandler';
|
||||
import { isReactive, isShallow } from './inulax/CommonUtils';
|
||||
|
||||
const vueReactive = {
|
||||
ref,
|
||||
useReference,
|
||||
isRef,
|
||||
unref,
|
||||
shallowRef,
|
||||
reactive,
|
||||
useReactive,
|
||||
isReactive,
|
||||
isShallow,
|
||||
computed,
|
||||
useComputed,
|
||||
watchEffect,
|
||||
watch,
|
||||
useWatch,
|
||||
toRaw,
|
||||
};
|
||||
|
||||
const Inula = {
|
||||
Children,
|
||||
|
@ -122,9 +143,11 @@ const Inula = {
|
|||
Profiler,
|
||||
StrictMode,
|
||||
Suspense,
|
||||
// vue reactive api
|
||||
vueReactive,
|
||||
};
|
||||
|
||||
export const version = __VERSION__;
|
||||
export const version = '';
|
||||
export {
|
||||
Children,
|
||||
createRef,
|
||||
|
@ -161,7 +184,7 @@ export {
|
|||
clearStore,
|
||||
reduxAdapter,
|
||||
watch,
|
||||
toRaw,
|
||||
|
||||
// 兼容ReactIs
|
||||
isFragment,
|
||||
isElement,
|
||||
|
@ -178,7 +201,12 @@ export {
|
|||
Profiler,
|
||||
StrictMode,
|
||||
Suspense,
|
||||
// vue reactive api
|
||||
vueReactive,
|
||||
};
|
||||
|
||||
export * from './types';
|
||||
export * from './inulax/types/ReactiveTypes';
|
||||
export * from './inulax/types/ProxyTypes';
|
||||
|
||||
export default Inula;
|
||||
|
|
|
@ -13,9 +13,17 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { KeyTypes, ReactiveFlags } from './Constants';
|
||||
import { Mutation } from './types/ProxyTypes';
|
||||
|
||||
export function isObject(obj: any): boolean {
|
||||
const type = typeof obj;
|
||||
return (obj !== null || obj !== undefined) && (type === 'object' || type === 'function');
|
||||
return (obj !== null || obj !== undefined) && type === 'object';
|
||||
}
|
||||
|
||||
export function isPrimitive(obj: unknown): boolean {
|
||||
const type = typeof obj;
|
||||
return obj != null && type !== 'object' && type !== 'function';
|
||||
}
|
||||
|
||||
export function isSet(obj: any): boolean {
|
||||
|
@ -126,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 {
|
||||
|
@ -200,8 +211,10 @@ export function resolveMutation(from, to) {
|
|||
}
|
||||
}
|
||||
|
||||
export function omit(obj, ...attrs) {
|
||||
const res = { ...obj };
|
||||
attrs.forEach(attr => delete res[attr]);
|
||||
return res;
|
||||
export function isShallow(value: unknown): boolean {
|
||||
return !!(value && value[ReactiveFlags.IS_SHALLOW]);
|
||||
}
|
||||
|
||||
export function isReactive(value: unknown) {
|
||||
return !!(value && !!value[KeyTypes.RAW_VALUE]);
|
||||
}
|
||||
|
|
|
@ -15,4 +15,21 @@
|
|||
|
||||
export const OBSERVER_KEY = typeof Symbol === 'function' ? Symbol('_inulaObserver') : '_inulaObserver';
|
||||
|
||||
export const RAW_VALUE = '_rawValue';
|
||||
// 特殊处理的keys
|
||||
export enum KeyTypes {
|
||||
RAW_VALUE = '_rawValue',
|
||||
COLLECTION_CHANGE = '_collectionChange',
|
||||
GET = 'get',
|
||||
SIZE = 'size',
|
||||
VALUE = 'value',
|
||||
WATCH = 'watch',
|
||||
LENGTH = 'length',
|
||||
PROTOTYPE = 'prototype',
|
||||
HAS_OWN_PROPERTY = 'hasOwnProperty',
|
||||
ADD_LISTENER = 'addListener',
|
||||
REMOVE_LISTENER = 'removeListener',
|
||||
}
|
||||
|
||||
export enum ReactiveFlags {
|
||||
IS_SHALLOW = '_isShallow',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
优化内容:
|
||||
1. 抽取getValOrProxy函数,减少18处重复代码
|
||||
2. 抽取watchProp函数,减少7处重复代码
|
||||
3. 抽取watchEffect函数
|
||||
4. 抽取registerListener函数
|
||||
5. 删除无用代码,hookObserver其实并没有使用
|
||||
```js
|
||||
const boundHandler = {};
|
||||
Object.entries(handler).forEach(([id, val]) => {
|
||||
boundHandler[id] = (...args: any[]) => {
|
||||
return (val as any)(...args, hookObserver);
|
||||
};
|
||||
});
|
||||
return new Proxy(rawObj, { ...boundHandler });
|
||||
```
|
||||
6. 统一抽取常量,如:
|
||||
```js
|
||||
```
|
||||
7. 增加watchEffect函数
|
||||
8. 删除WeakMapProxy中handler的add和clear方法,因为WeakMap并不存在这两个方法
|
||||
9. ObjectProxy中的handler增加deleteProperty方法,处理delete操作
|
||||
```js
|
||||
let dummy;
|
||||
const obj = reactive<{
|
||||
prop?: string;
|
||||
}>({ prop: 'value' });
|
||||
effect(() => (dummy = obj.prop));
|
||||
|
||||
expect(dummy).toBe('value');
|
||||
delete obj.prop;
|
||||
expect(dummy).toBe(undefined);
|
||||
```
|
||||
10. ObjectProxy中的handler增加has方法,处理in操作
|
||||
```js
|
||||
let dummy;
|
||||
const obj = reactive<{ prop?: string | number }>({ prop: 'value' });
|
||||
effect(() => {
|
||||
dummy = 'prop' in obj;
|
||||
});
|
||||
|
||||
expect(dummy).toBe(true);
|
||||
delete obj.prop;
|
||||
expect(dummy).toBe(false);
|
||||
obj.prop = 12;
|
||||
expect(dummy).toBe(true);
|
||||
```
|
||||
11. 当前不支持for (let key in numbers)这种写法
|
||||
```js
|
||||
it('should observe enumeration', () => {
|
||||
let dummy = 0;
|
||||
const numbers = reactive<Record<string, number>>({ num1: 3 });
|
||||
effect(() => {
|
||||
dummy = 0;
|
||||
for (let key in numbers) {
|
||||
dummy += numbers[key];
|
||||
}
|
||||
});
|
||||
|
||||
expect(dummy).toBe(3);
|
||||
numbers.num2 = 4;
|
||||
expect(dummy).toBe(7);
|
||||
delete numbers.num1;
|
||||
expect(dummy).toBe(4);
|
||||
});
|
||||
```
|
||||
|
||||
12. watchEffect中的watcher不支持第二个参数options
|
||||
```js
|
||||
const runner = effect(
|
||||
() => {
|
||||
dummy = obj.foo;
|
||||
},
|
||||
{ onTrigger }
|
||||
);
|
||||
```
|
||||
13. 不支持readonly
|
|
@ -0,0 +1,242 @@
|
|||
### reactive() 接口差异:
|
||||
|
||||
1、当前不支持markRaw接口。
|
||||
```js
|
||||
const obj = reactive({
|
||||
foo: { a: 1 },
|
||||
bar: markRaw({ b: 2 }),
|
||||
});
|
||||
expect(isReactive(obj.foo)).toBe(true);
|
||||
expect(isReactive(obj.bar)).toBe(false);
|
||||
```
|
||||
|
||||
2、对non-extensible属性当前会报错,不支持。
|
||||
```js
|
||||
it('should not observe non-extensible objects', () => {
|
||||
const obj = reactive({
|
||||
foo: Object.preventExtensions({ a: 1 }),
|
||||
// sealed or frozen objects are considered non-extensible as well
|
||||
bar: Object.freeze({ a: 1 }),
|
||||
baz: Object.seal({ a: 1 }),
|
||||
});
|
||||
expect(isReactive(obj.foo)).toBe(false);
|
||||
expect(isReactive(obj.bar)).toBe(false);
|
||||
expect(isReactive(obj.baz)).toBe(false);
|
||||
});
|
||||
```
|
||||
|
||||
3、不支持shallowReactive。
|
||||
```js
|
||||
it('should not make non-reactive properties reactive', () => {
|
||||
const props = shallowReactive({ n: { foo: 1 } })
|
||||
expect(isReactive(props.n)).toBe(false)
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### ref() 接口差异:
|
||||
1、不支持triggerRef。
|
||||
```js
|
||||
test('shallowRef force trigger', () => {
|
||||
const sref = shallowRef({ a: 1 })
|
||||
let dummy
|
||||
effect(() => {
|
||||
dummy = sref.value.a
|
||||
})
|
||||
expect(dummy).toBe(1)
|
||||
|
||||
sref.value.a = 2
|
||||
expect(dummy).toBe(1) // should not trigger yet
|
||||
|
||||
// force trigger
|
||||
triggerRef(sref)
|
||||
expect(dummy).toBe(2)
|
||||
})
|
||||
```
|
||||
|
||||
2、不支持toRef。
|
||||
```js
|
||||
it.skip('toRef on array', () => {
|
||||
const a = reactive(['a', 'b']);
|
||||
const r = toRef(a, 1);
|
||||
expect(r.value).toBe('b');
|
||||
r.value = 'c';
|
||||
expect(r.value).toBe('c');
|
||||
expect(a[1]).toBe('c');
|
||||
});
|
||||
```
|
||||
|
||||
3、不支持toRefs。
|
||||
```js
|
||||
it('toRefs', () => {
|
||||
const a = reactive({
|
||||
x: 1,
|
||||
y: 2,
|
||||
});
|
||||
|
||||
const {x, y} = toRefs(a);
|
||||
|
||||
expect(isRef(x)).toBe(true);
|
||||
expect(isRef(y)).toBe(true);
|
||||
expect(x.value).toBe(1);
|
||||
expect(y.value).toBe(2);
|
||||
});
|
||||
```
|
||||
4、不支持customRef。
|
||||
```js
|
||||
it('customRef', () => {
|
||||
let value = 1;
|
||||
let _trigger: () => void;
|
||||
|
||||
const custom = customRef((track, trigger) => ({
|
||||
get() {
|
||||
track();
|
||||
return value;
|
||||
},
|
||||
set(newValue: number) {
|
||||
value = newValue;
|
||||
_trigger = trigger;
|
||||
},
|
||||
}));
|
||||
|
||||
expect(isRef(custom)).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
### computed接口差异:
|
||||
1、不是延迟计算,而是立即计算,这与vue的computed不同。
|
||||
```js
|
||||
it('should not compute lazily', () => {
|
||||
const value = reactive<{ foo?: number }>({});
|
||||
const getter = vi.fn(() => value.foo);
|
||||
const cValue = computed(getter);
|
||||
|
||||
// not lazy
|
||||
expect(getter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
```
|
||||
|
||||
2、不支持setter。
|
||||
```js
|
||||
it('should not support setter', () => {
|
||||
const n = ref(1);
|
||||
const plusOne = computed({
|
||||
get: () => n.value + 1,
|
||||
set: val => {
|
||||
n.value = val - 1;
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
3、不是延时计算,会有副作用,每个数据变化都会触发。
|
||||
```js
|
||||
it('should trigger by each data changed', () => {
|
||||
const n = ref(0);
|
||||
const plusOne = computed(() => n.value + 1);
|
||||
const fn = vi.fn(() => {
|
||||
n.value;
|
||||
plusOne.value;
|
||||
});
|
||||
effect(fn);
|
||||
n.value++;
|
||||
// should call fn 3 times, 1 for init, 1 for n, 1 for plusOne
|
||||
expect(fn).toBeCalledTimes(3);
|
||||
});
|
||||
```
|
||||
|
||||
4、不支持isReadonly。
|
||||
```js
|
||||
it('should not support isReadonly', () => {
|
||||
const n = ref(1);
|
||||
const c = computed(() => n.value);
|
||||
expect(isReadonly(c)).toBe(false);
|
||||
});
|
||||
```
|
||||
|
||||
5. computed中不支持第二个参数debugOptions。
|
||||
```js
|
||||
const c = computed(() => 1, {
|
||||
onTrack, // 不支持
|
||||
});
|
||||
```
|
||||
|
||||
5. computed中不支持第二个参数debugOptions。
|
||||
```js
|
||||
const c = computed(() => 1, {
|
||||
onTrack, // 不支持
|
||||
});
|
||||
```
|
||||
|
||||
6. computed.effect.stop 改为 computed.stop。
|
||||
```js
|
||||
it('should no longer update when stopped', () => {
|
||||
const value = reactive<{ foo?: number }>({});
|
||||
const cValue = computed(() => value.foo);
|
||||
let dummy;
|
||||
effect(() => {
|
||||
dummy = cValue.value;
|
||||
});
|
||||
expect(dummy).toBe(undefined);
|
||||
value.foo = 1;
|
||||
expect(dummy).toBe(1);
|
||||
cValue.stop(); // cValue.effect.stop 改为 cValue.stop
|
||||
value.foo = 2;
|
||||
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);
|
||||
})
|
||||
```
|
|
@ -13,7 +13,7 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import type { IObserver } from './Observer';
|
||||
import { IObserver } from '../types/ProxyTypes';
|
||||
|
||||
/**
|
||||
* 一个对象(对象、数组、集合)对应一个Observer
|
||||
|
@ -49,5 +49,7 @@ export class HooklessObserver implements IObserver {
|
|||
|
||||
allChange(): void {}
|
||||
|
||||
arrayLengthChange(): void {}
|
||||
|
||||
clearByVNode(vNode): void {}
|
||||
}
|
||||
|
|
|
@ -17,24 +17,10 @@ import { launchUpdateFromVNode } from '../../renderer/TreeBuilder';
|
|||
import { getProcessingVNode } from '../../renderer/GlobalVar';
|
||||
import { VNode } from '../../renderer/vnode/VNode';
|
||||
import { devtools } from '../devtools';
|
||||
import { KeyTypes } from '../Constants';
|
||||
import { addRContext, RContextSet } from '../reactive/RContext';
|
||||
|
||||
export interface IObserver {
|
||||
useProp: (key: string) => void;
|
||||
|
||||
addListener: (listener: () => void) => void;
|
||||
|
||||
removeListener: (listener: () => void) => void;
|
||||
|
||||
setProp: (key: string, mutation: any) => void;
|
||||
|
||||
triggerChangeListeners: (mutation: any) => void;
|
||||
|
||||
triggerUpdate: (vNode: any) => void;
|
||||
|
||||
allChange: () => void;
|
||||
|
||||
clearByVNode: (vNode: any) => void;
|
||||
}
|
||||
import { IObserver, Listener, Mutation } from '../types/ProxyTypes';
|
||||
|
||||
/**
|
||||
* 一个对象(对象、数组、集合)对应一个Observer
|
||||
|
@ -44,12 +30,25 @@ export class Observer implements IObserver {
|
|||
|
||||
keyVNodes = new Map();
|
||||
|
||||
listeners: ((mutation) => void)[] = [];
|
||||
listeners: Listener[] = [];
|
||||
|
||||
watchers = {} as { [key: string]: ((key: string, oldValue: any, newValue: any, mutation: any) => void)[] };
|
||||
watchers = {};
|
||||
|
||||
rContexts: {
|
||||
[key: string | symbol]: RContextSet;
|
||||
} = {};
|
||||
|
||||
// 对象的属性被使用时调用
|
||||
useProp(key: string | symbol): void {
|
||||
// 用于watchEffect的监听
|
||||
addRContext(this, key);
|
||||
|
||||
let vNodes = this.keyVNodes.get(key);
|
||||
if (!vNodes) {
|
||||
vNodes = new Set();
|
||||
this.keyVNodes.set(key, vNodes);
|
||||
}
|
||||
|
||||
const processingVNode = getProcessingVNode();
|
||||
if (processingVNode === null || !processingVNode.observers) {
|
||||
// 异常场景
|
||||
|
@ -60,11 +59,6 @@ export class Observer implements IObserver {
|
|||
processingVNode.observers.add(this);
|
||||
|
||||
// key -> vNodes,记录这个prop被哪些VNode使用了
|
||||
let vNodes = this.keyVNodes.get(key);
|
||||
if (!vNodes) {
|
||||
vNodes = new Set();
|
||||
this.keyVNodes.set(key, vNodes);
|
||||
}
|
||||
vNodes.add(processingVNode);
|
||||
|
||||
// vNode -> keys,记录这个VNode使用了哪些props
|
||||
|
@ -77,9 +71,9 @@ export class Observer implements IObserver {
|
|||
}
|
||||
|
||||
// 对象的属性被赋值时调用
|
||||
setProp(key: string | symbol, mutation: 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
|
||||
// NOTE: using Set directly can lead to deadlock
|
||||
const vNodeArray = Array.from(vNodes || []);
|
||||
vNodeArray.forEach((vNode: VNode) => {
|
||||
if (vNode.isStoreChange) {
|
||||
|
@ -92,8 +86,28 @@ export class Observer implements IObserver {
|
|||
this.triggerUpdate(vNode);
|
||||
});
|
||||
|
||||
// NOTE: mutations are different in dev and production.
|
||||
this.triggerChangeListeners({ mutation, vNodes });
|
||||
// 这里需要过滤调COLLECTION_CHANGE,因为这个是集合的变化,不是具体的某个prop的变化,否则会重复触发
|
||||
if (key !== KeyTypes.COLLECTION_CHANGE) {
|
||||
// NOTE: mutations are different in dev and production.
|
||||
this.triggerChangeListeners({ mutation, vNodes });
|
||||
|
||||
// 值不一样,触发监听器
|
||||
if (this.watchers[key]) {
|
||||
this.watchers[key].forEach(cb => {
|
||||
cb(key, oldValue, newValue, mutation);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.rContexts[key]) {
|
||||
// clone this.rContexts[key] to avoid concurrent modification
|
||||
const rContexts = Array.from(this.rContexts[key]);
|
||||
rContexts.forEach(rContext => {
|
||||
if (!rContext.runs) {
|
||||
rContext.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
triggerUpdate(vNode: VNode): void {
|
||||
|
@ -101,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);
|
||||
}
|
||||
|
||||
|
@ -142,6 +156,17 @@ export class Observer implements IObserver {
|
|||
}
|
||||
}
|
||||
|
||||
arrayLengthChange(length: number): void {
|
||||
const keyIt = this.keyVNodes.keys();
|
||||
let keyItem = keyIt.next();
|
||||
while (!keyItem.done) {
|
||||
if (keyItem.value >= length) {
|
||||
this.setProp(keyItem.value, {});
|
||||
}
|
||||
keyItem = keyIt.next();
|
||||
}
|
||||
}
|
||||
|
||||
// 删除Observer中保存的这个VNode的关系数据
|
||||
clearByVNode(vNode: VNode): void {
|
||||
const keys = this.vNodeKeys.get(vNode);
|
||||
|
|
|
@ -19,65 +19,59 @@ import { HooklessObserver } from './HooklessObserver';
|
|||
import { isArray, isCollection, isObject } from '../CommonUtils';
|
||||
import { createArrayProxy } from './handlers/ArrayProxyHandler';
|
||||
import { createCollectionProxy } from './handlers/CollectionProxyHandler';
|
||||
import type { IObserver } from '../types';
|
||||
import { OBSERVER_KEY, RAW_VALUE } from '../Constants';
|
||||
import { IObserver, CurrentListener } from '../types/ProxyTypes';
|
||||
|
||||
// 保存rawObj -> Proxy
|
||||
const proxyMap = new WeakMap();
|
||||
// Save rawObj -> Proxy
|
||||
const proxyMap = new WeakMap<any, ProxyHandler<any>>();
|
||||
|
||||
export const hookObserverMap = new WeakMap();
|
||||
// Record whether rawObj has been deeply proxied
|
||||
export const deepProxyMap = new WeakMap<any, boolean>();
|
||||
|
||||
export function getObserver(rawObj: any): Observer {
|
||||
return rawObj[OBSERVER_KEY];
|
||||
// Use WeakMap to save rawObj -> Observer, without polluting the original object
|
||||
const rawObserverMap = new WeakMap<any, IObserver>();
|
||||
|
||||
export function getObserver(rawObj: any): IObserver {
|
||||
return rawObserverMap.get(rawObj) as IObserver;
|
||||
}
|
||||
|
||||
const setObserverKey =
|
||||
typeof OBSERVER_KEY === 'string'
|
||||
? (rawObj, observer) => {
|
||||
Object.defineProperty(rawObj, OBSERVER_KEY, {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
value: observer,
|
||||
});
|
||||
}
|
||||
: (rawObj, observer) => {
|
||||
rawObj[OBSERVER_KEY] = observer;
|
||||
};
|
||||
function setObserverKey(rawObj: any, observer: IObserver): void {
|
||||
rawObserverMap.set(rawObj, observer);
|
||||
}
|
||||
|
||||
export function createProxy(rawObj: any, listener: { current: (...args) => any }, isHookObserver = true): any {
|
||||
// 不是对象(是原始数据类型)不用代理
|
||||
export function createProxy(rawObj: any, listener?: CurrentListener, isDeepProxy = true): any {
|
||||
// No need to proxy if it's not an object (i.e., it's a primitive data type)
|
||||
if (!(rawObj && isObject(rawObj))) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
// 已代理过
|
||||
// Already exists
|
||||
const existProxy = proxyMap.get(rawObj);
|
||||
if (existProxy) {
|
||||
return existProxy;
|
||||
}
|
||||
|
||||
// Observer不需要代理
|
||||
// Observer does not need to be approached
|
||||
if (rawObj instanceof Observer) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
// 创建Observer
|
||||
let observer: IObserver = getObserver(rawObj);
|
||||
// Create Observer
|
||||
let observer = getObserver(rawObj);
|
||||
if (!observer) {
|
||||
observer = isHookObserver ? new Observer() : new HooklessObserver();
|
||||
observer = (isDeepProxy ? new Observer() : new HooklessObserver()) as IObserver;
|
||||
setObserverKey(rawObj, observer);
|
||||
}
|
||||
|
||||
hookObserverMap.set(rawObj, isHookObserver);
|
||||
deepProxyMap.set(rawObj, isDeepProxy);
|
||||
|
||||
// 创建Proxy
|
||||
let proxyObj;
|
||||
if (!isHookObserver) {
|
||||
let proxyObj: ProxyHandler<any>;
|
||||
if (!isDeepProxy) {
|
||||
proxyObj = createObjectProxy(
|
||||
rawObj,
|
||||
{
|
||||
current: change => {
|
||||
listener.current(change);
|
||||
listener?.current(change);
|
||||
},
|
||||
},
|
||||
true
|
||||
|
@ -86,27 +80,23 @@ export function createProxy(rawObj: any, listener: { current: (...args) => any }
|
|||
// 数组
|
||||
proxyObj = createArrayProxy(rawObj as [], {
|
||||
current: change => {
|
||||
listener.current(change);
|
||||
listener?.current(change);
|
||||
},
|
||||
});
|
||||
} else if (isCollection(rawObj)) {
|
||||
// 集合
|
||||
proxyObj = createCollectionProxy(
|
||||
rawObj,
|
||||
{
|
||||
current: change => {
|
||||
listener.current(change);
|
||||
},
|
||||
proxyObj = createCollectionProxy(rawObj, {
|
||||
current: change => {
|
||||
listener?.current(change);
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// 原生对象 或 函数
|
||||
proxyObj = createObjectProxy(
|
||||
rawObj,
|
||||
{
|
||||
current: change => {
|
||||
listener.current(change);
|
||||
listener?.current(change);
|
||||
},
|
||||
},
|
||||
false
|
||||
|
@ -118,7 +108,3 @@ export function createProxy(rawObj: any, listener: { current: (...args) => any }
|
|||
|
||||
return proxyObj;
|
||||
}
|
||||
|
||||
export function toRaw<T>(observed: T): T {
|
||||
return observed && observed[RAW_VALUE];
|
||||
}
|
||||
|
|
|
@ -13,150 +13,23 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { isSame, isValidIntegerKey } from '../../CommonUtils';
|
||||
import { resolveMutation } from '../../CommonUtils';
|
||||
import { isPanelActive } from '../../devtools';
|
||||
import { OBSERVER_KEY, RAW_VALUE } from '../../Constants';
|
||||
import { registerListener } from './HandlerUtils';
|
||||
import { baseSetFun, baseGetFun } from './BaseObjectHandler';
|
||||
import { CurrentListener, Listeners, ObjectType } from '../../types/ProxyTypes';
|
||||
|
||||
function set(rawObj: any[], key: string, value: any, receiver: any) {
|
||||
const oldValue = rawObj[key];
|
||||
const oldLength = rawObj.length;
|
||||
const newValue = value;
|
||||
export function createArrayProxy<T extends any[]>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
|
||||
const listeners: Listeners = [];
|
||||
|
||||
const oldArray = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
||||
|
||||
const ret = Reflect.set(rawObj, key, newValue, receiver);
|
||||
|
||||
const newLength = rawObj.length;
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
const mutation = isPanelActive() ? resolveMutation(oldArray, rawObj) : resolveMutation(null, rawObj);
|
||||
|
||||
if (!isSame(newValue, oldValue)) {
|
||||
// 值不一样,触发监听器
|
||||
if (observer.watchers?.[key]) {
|
||||
observer.watchers[key].forEach(cb => {
|
||||
cb(key, oldValue, newValue, mutation);
|
||||
});
|
||||
}
|
||||
|
||||
// 触发属性变化
|
||||
observer.setProp(key, mutation);
|
||||
function get(rawObj: T, key: KeyType, receiver: any) {
|
||||
return baseGetFun(rawObj, key, receiver, listener, listeners);
|
||||
}
|
||||
|
||||
if (oldLength !== newLength) {
|
||||
// 触发数组的大小变化
|
||||
observer.setProp('length', mutation);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function createArrayProxy(rawObj: any[], listener: { current: (...args) => any }): any[] {
|
||||
let listeners = [] as ((...args) => void)[];
|
||||
|
||||
function objectGet(rawObj: Record<string, any>, key: string | symbol, receiver: any, singleLevel = false): any {
|
||||
// The observer object of symbol ('_inulaObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep.
|
||||
if (key === OBSERVER_KEY) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
if (key === 'watch') {
|
||||
return (prop, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'addListener') {
|
||||
return listener => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'removeListener') {
|
||||
return listener => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
|
||||
observer.useProp(key);
|
||||
|
||||
const value = Reflect.get(rawObj, key, receiver);
|
||||
|
||||
// 对于prototype不做代理
|
||||
if (key !== 'prototype') {
|
||||
// 对于value也需要进一步代理
|
||||
const valProxy = singleLevel
|
||||
? value
|
||||
: createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [key]: change.mutation.from },
|
||||
{ ...rawObj, [key]: change.mutation.to }
|
||||
);
|
||||
listener.current(mutation);
|
||||
listeners.forEach(lst => lst(mutation));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
|
||||
return valProxy;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function get(rawObj: any[], key: string, receiver: any) {
|
||||
if (key === 'watch') {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (isValidIntegerKey(key) || key === 'length') {
|
||||
return objectGet(rawObj, key, receiver);
|
||||
}
|
||||
|
||||
if (key === RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
return Reflect.get(rawObj, key, receiver);
|
||||
}
|
||||
|
||||
const handle = {
|
||||
const handler = {
|
||||
get,
|
||||
set,
|
||||
set: baseSetFun,
|
||||
};
|
||||
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
registerListener(rawObj, listener, listeners);
|
||||
|
||||
return new Proxy(rawObj, handle);
|
||||
return new Proxy(rawObj as ObjectType, handler);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* 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 { KeyTypes } from '../../Constants';
|
||||
import { getObserver } from '../ProxyHandler';
|
||||
import { getValOrProxy, getWatchFn, triggerSetWatchers } from './HandlerUtils';
|
||||
import {
|
||||
CollectionStringTypes,
|
||||
CollectionTypes,
|
||||
CurrentListener,
|
||||
IterableTypes,
|
||||
Listener,
|
||||
Listeners,
|
||||
MapTypes,
|
||||
ObjectType,
|
||||
SetTypes,
|
||||
} from '../../types/ProxyTypes';
|
||||
import { resolveMutation } from '../../CommonUtils';
|
||||
|
||||
export function baseGetFun(
|
||||
rawObj: MapTypes | SetTypes,
|
||||
key: any,
|
||||
receiver: any,
|
||||
listeners: Listeners,
|
||||
handler: ObjectType,
|
||||
type: CollectionStringTypes,
|
||||
getFun?: (rawObj: MapTypes, key: any) => any
|
||||
): any {
|
||||
if (key === KeyTypes.VALUE) {
|
||||
return receiver;
|
||||
}
|
||||
|
||||
if ((type === 'Map' || type === 'Set') && key === KeyTypes.SIZE) {
|
||||
return baseSizeFun(rawObj as IterableTypes);
|
||||
}
|
||||
|
||||
if ((type === 'Map' || type === 'WeakMap') && key === KeyTypes.GET) {
|
||||
return getFun!.bind(null, rawObj);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
||||
const value = Reflect.get(handler, key, receiver);
|
||||
return value.bind(null, rawObj);
|
||||
}
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
if (key === KeyTypes.WATCH) {
|
||||
return getWatchFn(observer);
|
||||
}
|
||||
|
||||
if (key === KeyTypes.ADD_LISTENER) {
|
||||
return (listener: Listener) => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === KeyTypes.REMOVE_LISTENER) {
|
||||
return (listener: Listener) => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === KeyTypes.RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
return Reflect.get(rawObj, key, receiver);
|
||||
}
|
||||
|
||||
function baseSizeFun(rawObj: IterableTypes) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||
return rawObj.size;
|
||||
}
|
||||
|
||||
export function baseForEach(
|
||||
rawObj: CollectionTypes,
|
||||
callback: (valProxy: any, keyProxy: any, rawObj: any) => void,
|
||||
listener: CurrentListener,
|
||||
listeners: Listeners
|
||||
) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||
rawObj.forEach((value, key) => {
|
||||
const valProxy = getValOrProxy('valueChange', false, value, rawObj, listener, listeners);
|
||||
const keyProxy = getValOrProxy('keyChange', false, key, rawObj, listener, listeners);
|
||||
// 最后一个参数要返回代理对象
|
||||
return callback(valProxy, keyProxy, rawObj);
|
||||
});
|
||||
}
|
||||
|
||||
export function baseClearFun(rawObj: IterableTypes, proxies: Map<any, any>, type: CollectionStringTypes) {
|
||||
const oldSize = rawObj.size;
|
||||
rawObj.clear();
|
||||
proxies.clear();
|
||||
|
||||
if (oldSize > 0) {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
if (type === 'Set') {
|
||||
triggerSetWatchers(observer);
|
||||
}
|
||||
observer.allChange();
|
||||
}
|
||||
}
|
||||
|
||||
export function baseDeleteFun(
|
||||
rawObj: MapTypes | SetTypes,
|
||||
value: any,
|
||||
type: CollectionStringTypes,
|
||||
proxies?: MapTypes
|
||||
) {
|
||||
// 通过new Set([{a: 1}])创建的值并没有加入proxies,所以还需要判断一下
|
||||
const val = proxies?.get(value) || value;
|
||||
|
||||
if (baseHasFun(rawObj, value, proxies)) {
|
||||
let oldValues;
|
||||
if (type === 'Set') {
|
||||
oldValues = Array.from((rawObj as Set<any>).values());
|
||||
} else if (type === 'Map') {
|
||||
oldValues = [...Array.from((rawObj as Map<any, any>).entries())];
|
||||
}
|
||||
|
||||
rawObj.delete(val);
|
||||
proxies?.delete(value);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
if (type === 'Set' || type === 'WeakSet') {
|
||||
triggerSetWatchers(observer);
|
||||
}
|
||||
|
||||
let mutation;
|
||||
if (type === 'Set') {
|
||||
mutation = resolveMutation(
|
||||
{
|
||||
_type: type,
|
||||
values: oldValues,
|
||||
},
|
||||
{
|
||||
_type: type,
|
||||
values: Array.from((rawObj as Set<any>).values()),
|
||||
}
|
||||
);
|
||||
} else if (type === 'Map') {
|
||||
mutation = resolveMutation(
|
||||
{
|
||||
_type: type,
|
||||
entries: oldValues,
|
||||
},
|
||||
{
|
||||
_type: type,
|
||||
entries: Array.from((rawObj as Map<any, any>).entries()),
|
||||
}
|
||||
);
|
||||
} else {
|
||||
mutation = { mutation: true, from: value, to: rawObj };
|
||||
}
|
||||
|
||||
observer.setProp(value, mutation);
|
||||
|
||||
if (type === 'Set' || type === 'Map') {
|
||||
observer.setProp(KeyTypes.COLLECTION_CHANGE, mutation);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function baseAddFunOfSet(
|
||||
rawObj: SetTypes,
|
||||
value: any,
|
||||
listener: CurrentListener,
|
||||
listeners: Listeners,
|
||||
type: CollectionStringTypes,
|
||||
proxies?: MapTypes
|
||||
): Record<string, any> {
|
||||
if (!baseHasFun(rawObj, value, proxies)) {
|
||||
const proxy = getValOrProxy('valueChange', false, value, rawObj, listener, listeners);
|
||||
|
||||
let oldValues;
|
||||
if (type === 'Set') {
|
||||
oldValues = Array.from((rawObj as Set<any>).values());
|
||||
}
|
||||
|
||||
// 更新
|
||||
proxies?.set(value, proxy);
|
||||
rawObj.add(proxy);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
triggerSetWatchers(observer);
|
||||
|
||||
let mutation;
|
||||
if (type === 'Set') {
|
||||
mutation = resolveMutation(
|
||||
{
|
||||
_type: type,
|
||||
values: oldValues,
|
||||
},
|
||||
{
|
||||
_type: type,
|
||||
values: Array.from((rawObj as Set<any>).values()),
|
||||
}
|
||||
);
|
||||
} else {
|
||||
mutation = { mutation: true, from: rawObj, to: value };
|
||||
}
|
||||
|
||||
observer.setProp(value, mutation, undefined, value);
|
||||
if (type === 'Set') {
|
||||
observer.setProp(KeyTypes.COLLECTION_CHANGE, mutation);
|
||||
}
|
||||
}
|
||||
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
export function baseHasFun(rawObj: MapTypes | SetTypes, value: any, proxies?: MapTypes): boolean {
|
||||
// 通过new Set([{a: 1}])创建的值并没有加入proxies,所以还需要判断一下
|
||||
return proxies?.has(value) || rawObj.has(value);
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* 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 { isPanelActive } from '../../devtools';
|
||||
import { getObserver } from '../ProxyHandler';
|
||||
import { isArray, isSame, isValidIntegerKey, resolveMutation } from '../../CommonUtils';
|
||||
import { isRef } from '../../reactive/Ref';
|
||||
import { KeyTypes, OBSERVER_KEY } from '../../Constants';
|
||||
import { getValOrProxy, getWatchFn } from './HandlerUtils';
|
||||
import { toRaw } from '../../reactive/Reactive';
|
||||
import { CurrentListener, Listeners, Listener, ObjectType, KeyType } from '../../types/ProxyTypes';
|
||||
|
||||
// Object 和 Array 公用的 proxy handler set
|
||||
export function baseSetFun(rawObj: any[], key: string, value: any, receiver: any) {
|
||||
const oldValue = rawObj[key];
|
||||
const newValue = value;
|
||||
const isArr = isArray(rawObj);
|
||||
|
||||
if (!isArr && isRef(oldValue) && !isRef(newValue)) {
|
||||
oldValue.value = newValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
const oldLength = isArr ? rawObj.length : 0;
|
||||
const oldObj = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
||||
|
||||
const hadKey =
|
||||
isArr && isValidIntegerKey(key) ? Number(key) < rawObj.length : Object.prototype.hasOwnProperty.call(rawObj, key);
|
||||
|
||||
const ret = Reflect.set(rawObj, key, newValue, receiver);
|
||||
|
||||
const newLength = isArr ? rawObj.length : 0;
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
if (!isSame(newValue, oldValue)) {
|
||||
const mutation = resolveMutation(oldObj, rawObj);
|
||||
|
||||
// 触发属性变化
|
||||
observer.setProp(key, mutation, oldValue, newValue);
|
||||
|
||||
if (isArr) {
|
||||
if (oldLength !== newLength) {
|
||||
if (key === KeyTypes.LENGTH) {
|
||||
// 只需要触发比新数组长度大的部分
|
||||
observer.arrayLengthChange(newLength);
|
||||
} else {
|
||||
// 触发数组的大小变化
|
||||
observer.setProp('length', mutation);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!hadKey) {
|
||||
// 触发数组的大小变化
|
||||
observer.setProp('length', mutation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function baseGetFun<T extends Record<string | symbol, any> | any[]>(
|
||||
rawObj: T,
|
||||
key: KeyType,
|
||||
receiver: any,
|
||||
listener: CurrentListener,
|
||||
listeners: Listeners,
|
||||
singleLevel = false
|
||||
) {
|
||||
if (key === OBSERVER_KEY) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (key === KeyTypes.VALUE) {
|
||||
return receiver;
|
||||
}
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
if (key === KeyTypes.WATCH) {
|
||||
return getWatchFn(observer);
|
||||
}
|
||||
|
||||
if (key === KeyTypes.ADD_LISTENER) {
|
||||
return (listener: Listener) => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === KeyTypes.REMOVE_LISTENER) {
|
||||
return (listener: Listener) => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === KeyTypes.RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
if (key === KeyTypes.HAS_OWN_PROPERTY) {
|
||||
return hasOwnProperty;
|
||||
}
|
||||
|
||||
const value = Reflect.get(rawObj, key, receiver);
|
||||
|
||||
const isArr = isArray(rawObj);
|
||||
|
||||
if (isArr) {
|
||||
// 数组只代理数字索引和length
|
||||
if (isValidIntegerKey(key) || key === KeyTypes.LENGTH) {
|
||||
observer.useProp(key);
|
||||
|
||||
// 对于value也需要进一步代理
|
||||
return getValOrProxy(key, singleLevel, value, rawObj, listener, listeners);
|
||||
}
|
||||
} else {
|
||||
if (key !== KeyTypes.PROTOTYPE) {
|
||||
observer.useProp(key);
|
||||
|
||||
// 对于value也需要进一步代理
|
||||
return getValOrProxy(key, singleLevel, value, rawObj, listener, listeners);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function has<T extends ObjectType>(rawObj: T, key: KeyType) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(key);
|
||||
|
||||
return Reflect.has(rawObj, key);
|
||||
}
|
||||
|
||||
export function deleteProperty<T extends ObjectType | any[]>(rawObj: T, key: KeyType) {
|
||||
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 = resolveMutation(oldObj, rawObj);
|
||||
|
||||
if (!isSame(newValue, oldValue)) {
|
||||
observer.setProp(key, mutation, oldValue, newValue);
|
||||
|
||||
// 触发数组的大小变化
|
||||
observer.setProp('length', mutation);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// 代理 for (const key in obj) 场景
|
||||
export function ownKeys(rawObj: ObjectType): (string | symbol)[] {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
observer.useProp('length');
|
||||
|
||||
return Reflect.ownKeys(rawObj);
|
||||
}
|
||||
|
||||
function hasOwnProperty(this: Record<string, any>, key: string) {
|
||||
const obj = toRaw(this);
|
||||
has(obj, key);
|
||||
return Object.prototype.hasOwnProperty.call(obj, key);
|
||||
}
|
|
@ -19,19 +19,20 @@ import { createSetProxy } from './SetProxy';
|
|||
import { createWeakMapProxy } from './WeakMapProxy';
|
||||
import { createMapProxy } from './MapProxy';
|
||||
|
||||
export function createCollectionProxy(
|
||||
rawObj: Record<string, unknown>,
|
||||
listener: { current: (...args) => any },
|
||||
hookObserver = true
|
||||
): ProxyHandler<Record<string, unknown>> {
|
||||
import { CurrentListener } from '../../types/ProxyTypes';
|
||||
|
||||
export function createCollectionProxy<T extends any>(rawObj: T, listener: CurrentListener) {
|
||||
if (isWeakSet(rawObj)) {
|
||||
return createWeakSetProxy(rawObj, listener, hookObserver);
|
||||
return createWeakSetProxy(rawObj as WeakSet<any>, listener);
|
||||
}
|
||||
|
||||
if (isSet(rawObj)) {
|
||||
return createSetProxy(rawObj, listener, hookObserver);
|
||||
return createSetProxy(rawObj as Set<any>, listener);
|
||||
}
|
||||
|
||||
if (isWeakMap(rawObj)) {
|
||||
return createWeakMapProxy(rawObj, listener, hookObserver);
|
||||
return createWeakMapProxy(rawObj as WeakMap<any, any>, listener);
|
||||
}
|
||||
return createMapProxy(rawObj, listener, hookObserver);
|
||||
|
||||
return createMapProxy(rawObj as Map<any, any>, listener);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
import { createProxy, getObserver, deepProxyMap } from '../ProxyHandler';
|
||||
import { isArray, isValidIntegerKey, resolveMutation } from '../../CommonUtils';
|
||||
import { isRef } from '../../reactive/Ref';
|
||||
import { CurrentListener, IObserver, Listeners, WatchFn, WatchHandler } from '../../types/ProxyTypes';
|
||||
|
||||
export const SET_WATCH_KEY = '_setWatchKey';
|
||||
|
||||
// 获取观察者函数
|
||||
export function getWatchFn(observer: IObserver): WatchFn {
|
||||
// 返回一个函数,该函数接受属性和处理程序作为参数
|
||||
return (prop: any, handler?: WatchHandler) => {
|
||||
// Set不需要指定prop
|
||||
if (typeof prop === 'function') {
|
||||
handler = prop;
|
||||
prop = SET_WATCH_KEY;
|
||||
}
|
||||
|
||||
// 观察指定的属性
|
||||
watchProp(observer, prop, handler as WatchHandler);
|
||||
};
|
||||
}
|
||||
|
||||
// 观察属性
|
||||
function watchProp(observer: IObserver, prop: any, handler: WatchHandler) {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [];
|
||||
}
|
||||
|
||||
// 将处理程序添加到观察者数组中
|
||||
if (!observer.watchers[prop].includes(handler)) {
|
||||
observer.watchers[prop].push(handler);
|
||||
}
|
||||
|
||||
return () => {
|
||||
// 从观察者数组中移除处理程序
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
}
|
||||
|
||||
export function triggerSetWatchers(observer: IObserver) {
|
||||
if (observer.watchers[SET_WATCH_KEY]) {
|
||||
observer.watchers[SET_WATCH_KEY].forEach(cb => {
|
||||
cb();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getValOrProxy(
|
||||
key: string | symbol,
|
||||
singleLevel: boolean,
|
||||
value: any,
|
||||
rawObj: Record<string, any>,
|
||||
listener: CurrentListener,
|
||||
listeners: Listeners
|
||||
): any {
|
||||
if (isRef(value)) {
|
||||
// ref unwrapping
|
||||
return isArray(rawObj) && isValidIntegerKey(key) ? value : value.value;
|
||||
}
|
||||
|
||||
// 对于value也需要进一步代理
|
||||
return singleLevel
|
||||
? value
|
||||
: createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [key]: change.mutation.from },
|
||||
{ ...rawObj, [key]: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
deepProxyMap.get(rawObj)
|
||||
);
|
||||
}
|
||||
|
||||
export function registerListener(rawObj: any, listener: CurrentListener, listeners: Listeners) {
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
}
|
|
@ -13,252 +13,108 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { createProxy, getObserver, deepProxyMap } from '../ProxyHandler';
|
||||
import { isSame } from '../../CommonUtils';
|
||||
import { resolveMutation } from '../../CommonUtils';
|
||||
import { isPanelActive } from '../../devtools';
|
||||
import { RAW_VALUE } from '../../Constants';
|
||||
import { KeyTypes } from '../../Constants';
|
||||
import { getValOrProxy, registerListener } from './HandlerUtils';
|
||||
import { baseDeleteFun, baseHasFun, baseForEach, baseGetFun, baseClearFun } from './BaseCollectionHandler';
|
||||
import { CurrentListener, Listeners, ObjectType } from '../../types/ProxyTypes';
|
||||
|
||||
const COLLECTION_CHANGE = '_collectionChange';
|
||||
type IteratorTypes = 'keys' | 'values' | 'entries';
|
||||
|
||||
export function createMapProxy(
|
||||
rawObj: Record<string, any>,
|
||||
listener: { current: (...args) => any },
|
||||
hookObserver = true
|
||||
): Record<string, any> {
|
||||
let listeners: ((mutation) => Record<string, any>)[] = [];
|
||||
let oldData: [any, any][] = [];
|
||||
const proxies = new Map();
|
||||
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;
|
||||
const keyProxies = new Map();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function getFun(rawObj: { get: (key: any) => any; has: (key: any) => boolean }, key: any): any {
|
||||
const keyProxy = rawObj.has(key) ? key : proxies.get(key);
|
||||
function getFun(rawObj: T, key: any): any {
|
||||
const keyProxy = rawObj.has(key) ? key : keyProxies.get(key);
|
||||
if (!keyProxy) return;
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(key);
|
||||
|
||||
const value = rawObj.get(keyProxy);
|
||||
|
||||
// 对于value也需要进一步代理
|
||||
const valProxy = createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [key]: change.mutation.from },
|
||||
{ ...rawObj, [key]: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
|
||||
return valProxy;
|
||||
return getValOrProxy(key, false, value, rawObj, listener, listeners);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function get(rawObj: T, key: any, receiver: any): any {
|
||||
return baseGetFun(rawObj, key, receiver, listeners, handler, 'Map', getFun);
|
||||
}
|
||||
|
||||
// Map的set方法
|
||||
function set(
|
||||
rawObj: {
|
||||
get: (key: any) => any;
|
||||
set: (key: any, value: any) => any;
|
||||
has: (key: any) => boolean;
|
||||
entries: () => [any, any][];
|
||||
},
|
||||
key: any,
|
||||
value: any
|
||||
): any {
|
||||
if (rawObj.has(key) || rawObj.has(proxies.get(key))) {
|
||||
// VALUE CHANGE (whole value for selected key is changed)
|
||||
const oldValue = rawObj.get(proxies.get(key));
|
||||
if (isSame(value, oldValue)) return;
|
||||
rawObj.set(proxies.get(key), value);
|
||||
const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : resolveMutation(null, rawObj);
|
||||
const observer = getObserver(rawObj);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
|
||||
if (observer.watchers[key]) {
|
||||
observer.watchers[key].forEach(cb => {
|
||||
cb(key, oldValue, value, mutation);
|
||||
});
|
||||
function set(rawObj: T, key: any, value: any): any {
|
||||
let keyProxy;
|
||||
let oldValue;
|
||||
if (baseHasFun(rawObj, key, keyProxies)) {
|
||||
keyProxy = keyProxies.has(key) ? keyProxies.get(key) : key;
|
||||
oldValue = rawObj.get(keyProxy);
|
||||
if (isSame(value, oldValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
observer.setProp(key, mutation);
|
||||
oldData = [...Array.from(rawObj.entries())];
|
||||
} else {
|
||||
// NEW VALUE
|
||||
const keyProxy = createProxy(
|
||||
key,
|
||||
{
|
||||
current: change => {
|
||||
// KEY CHANGE
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, ['_keyChange']: change.mutation.from },
|
||||
{ ...rawObj, ['_keyChange']: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
proxies.set(key, keyProxy);
|
||||
|
||||
rawObj.set(keyProxy, value);
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{
|
||||
_type: 'Map',
|
||||
entries: oldData,
|
||||
},
|
||||
{
|
||||
_type: 'Map',
|
||||
entries: Array.from(rawObj.entries()),
|
||||
}
|
||||
);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
|
||||
if (observer.watchers?.[key]) {
|
||||
observer.watchers[key].forEach(cb => {
|
||||
cb(key, null, value, mutation);
|
||||
});
|
||||
}
|
||||
observer.setProp(key, mutation);
|
||||
oldData = [...Array.from(rawObj.entries())];
|
||||
keyProxy = getValOrProxy('keyChange', false, key, rawObj, listener, listeners);
|
||||
keyProxies.set(key, keyProxy);
|
||||
}
|
||||
|
||||
const oldValues = [...Array.from(rawObj.entries())];
|
||||
|
||||
rawObj.set(keyProxy, value);
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{
|
||||
_type: 'Map',
|
||||
entries: oldValues,
|
||||
},
|
||||
{
|
||||
_type: 'Map',
|
||||
entries: Array.from(rawObj.entries()),
|
||||
}
|
||||
);
|
||||
observer.setProp(KeyTypes.COLLECTION_CHANGE, mutation);
|
||||
observer.setProp(key, mutation, oldValue, value);
|
||||
|
||||
return rawObj;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function has(rawObj: { has: (any) => boolean }, key: any): boolean {
|
||||
|
||||
function has(rawObj: T, key: any): boolean {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(key);
|
||||
if (rawObj.has(key)) {
|
||||
return true;
|
||||
}
|
||||
return proxies.has(key);
|
||||
|
||||
return baseHasFun(rawObj, key, keyProxies);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function clear(rawObj: { size: number; clear: () => void; entries: () => [any, any][] }) {
|
||||
const oldSize = rawObj.size;
|
||||
rawObj.clear();
|
||||
|
||||
if (oldSize > 0) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.allChange();
|
||||
oldData = [...Array.from(rawObj.entries())];
|
||||
}
|
||||
function clear(rawObj: T) {
|
||||
baseClearFun(rawObj, keyProxies, 'Map');
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function deleteFun(
|
||||
rawObj: { has: (key: any) => boolean; delete: (key: any) => void; entries: () => [any, any][] },
|
||||
key: any
|
||||
) {
|
||||
if (rawObj.has(key) || proxies.has(key)) {
|
||||
rawObj.delete(key || proxies.get(key));
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{
|
||||
_type: 'Map',
|
||||
entries: oldData,
|
||||
},
|
||||
{
|
||||
_type: 'Map',
|
||||
entries: Array.from(rawObj.entries()),
|
||||
}
|
||||
);
|
||||
observer.setProp(key, mutation);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
|
||||
oldData = [...Array.from(rawObj.entries())];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
function deleteFun(rawObj: T, key: any) {
|
||||
return baseDeleteFun(rawObj, key, 'Map', keyProxies);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function forEach(
|
||||
rawObj: { forEach: (callback: (value: any, key: any) => void) => void },
|
||||
callback: (valProxy: any, keyProxy: any, rawObj: any) => void
|
||||
) {
|
||||
|
||||
function forEach(rawObj: T, callback: (valProxy: any, keyProxy: any, rawObj: any) => void) {
|
||||
baseForEach(rawObj, callback, listener, listeners);
|
||||
}
|
||||
|
||||
function wrapIterator(rawObj: T, rawIt: IterableIterator<any>, type: IteratorTypes) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
rawObj.forEach((value, key) => {
|
||||
const keyProxy = createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
//KEY ATTRIBUTES CHANGED
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, ['_keyChange']: change.mutation.from },
|
||||
{ ...rawObj, ['_keyChange']: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
const valProxy = createProxy(
|
||||
key,
|
||||
{
|
||||
current: change => {
|
||||
// VALUE ATTRIBUTE CHANGED
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, key: change.mutation.from },
|
||||
{ ...rawObj, key: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
// 最后一个参数要返回代理对象
|
||||
return callback(keyProxy, valProxy, rawObj);
|
||||
});
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function wrapIterator(rawObj: Record<string, any>, rawIt: { next: () => { value: any; done: boolean } }, type) {
|
||||
const observer = getObserver(rawObj);
|
||||
const hookObserver = hookObserverMap.get(rawObj);
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
const hookObserver = deepProxyMap.get(rawObj);
|
||||
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||
|
||||
return {
|
||||
next() {
|
||||
const { value, done } = rawIt.next();
|
||||
if (done) {
|
||||
return {
|
||||
value: createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [value]: change.mutation.from },
|
||||
{ ...rawObj, [value]: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserver
|
||||
),
|
||||
value: getValOrProxy(value, false, value, rawObj, listener, listeners),
|
||||
done,
|
||||
};
|
||||
}
|
||||
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||
let newVal;
|
||||
if (type === 'entries') {
|
||||
//ENTRY CHANGED
|
||||
|
@ -298,22 +154,7 @@ export function createMapProxy(
|
|||
];
|
||||
} else {
|
||||
// SINGLE VALUE CHANGED
|
||||
newVal = createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [type === 'keys' ? 'key' : 'value']: change.mutation.from },
|
||||
{ ...rawObj, [type === 'keys' ? 'key' : 'value']: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserver
|
||||
);
|
||||
newVal = getValOrProxy(type === 'keys' ? 'key' : 'value', false, value, rawObj, listener, listeners);
|
||||
}
|
||||
|
||||
return { value: newVal, done };
|
||||
|
@ -325,29 +166,19 @@ export function createMapProxy(
|
|||
};
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function size(rawObj: { size: number }) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
return rawObj.size;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function keys(rawObj: { keys: () => { next: () => { value: any; done: boolean } } }) {
|
||||
function keys(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.keys(), 'keys');
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) {
|
||||
|
||||
function values(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.values(), 'values');
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) {
|
||||
|
||||
function entries(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.entries(), 'entries');
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function forOf(rawObj: {
|
||||
entries: () => { next: () => { value: any; done: boolean } };
|
||||
values: () => { next: () => { value: any; done: boolean } };
|
||||
}) {
|
||||
|
||||
function forOf(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.entries(), 'entries');
|
||||
}
|
||||
|
||||
|
@ -365,65 +196,7 @@ export function createMapProxy(
|
|||
[typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf,
|
||||
};
|
||||
|
||||
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
||||
if (key === 'size') {
|
||||
return size(rawObj);
|
||||
}
|
||||
registerListener(rawObj, listener, listeners);
|
||||
|
||||
if (key === 'get') {
|
||||
return getFun.bind(null, rawObj);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
||||
const value = Reflect.get(handler, key, receiver);
|
||||
return value.bind(null, rawObj);
|
||||
}
|
||||
|
||||
if (key === 'watch') {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'addListener') {
|
||||
return listener => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'removeListener') {
|
||||
return listener => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
return Reflect.get(rawObj, key, receiver);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const boundHandler = {};
|
||||
Object.entries(handler).forEach(([id, val]) => {
|
||||
boundHandler[id] = (...args: any[]) => {
|
||||
return (val as any)(...args, hookObserver);
|
||||
};
|
||||
});
|
||||
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
return new Proxy(rawObj, { ...boundHandler });
|
||||
return new Proxy(rawObj as ObjectType, handler);
|
||||
}
|
||||
|
|
|
@ -13,118 +13,30 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { isSame, resolveMutation } from '../../CommonUtils';
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { OBSERVER_KEY, RAW_VALUE } from '../../Constants';
|
||||
import { isPanelActive } from '../../devtools';
|
||||
import { registerListener } from './HandlerUtils';
|
||||
import { baseSetFun, baseGetFun, has, deleteProperty, ownKeys } from './BaseObjectHandler';
|
||||
import { CurrentListener, KeyType, Listeners, ObjectType } from '../../types/ProxyTypes';
|
||||
|
||||
function set(rawObj: Record<string, any>, key: string, value: any, receiver: any): boolean {
|
||||
const oldObject = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
const oldValue = rawObj[key];
|
||||
const newValue = value;
|
||||
|
||||
const ret = Reflect.set(rawObj, key, newValue, receiver);
|
||||
const mutation = isPanelActive() ? resolveMutation(oldObject, rawObj) : resolveMutation(null, rawObj);
|
||||
|
||||
if (!isSame(newValue, oldValue)) {
|
||||
if (observer.watchers?.[key]) {
|
||||
observer.watchers[key].forEach(cb => {
|
||||
cb(key, oldValue, newValue, mutation);
|
||||
});
|
||||
}
|
||||
observer.setProp(key, mutation);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function createObjectProxy<T extends Record<string, any>>(
|
||||
export function createObjectProxy<T extends ObjectType>(
|
||||
rawObj: T,
|
||||
listener: { current: (...args) => any },
|
||||
listener: CurrentListener,
|
||||
singleLevel = false
|
||||
): ProxyHandler<T> {
|
||||
let listeners = [] as ((...args) => void)[];
|
||||
const listeners: Listeners = [];
|
||||
|
||||
function get(rawObj: Record<string, any>, key: string | symbol, receiver: any): any {
|
||||
// The observer object of symbol ('_inulaObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep.
|
||||
if (key === OBSERVER_KEY) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
if (key === 'watch') {
|
||||
return (prop, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'addListener') {
|
||||
return listener => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'removeListener') {
|
||||
return listener => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
observer.useProp(key);
|
||||
|
||||
const value = Reflect.get(rawObj, key, receiver);
|
||||
|
||||
// 对于prototype不做代理
|
||||
if (key !== 'prototype') {
|
||||
// 对于value也需要进一步代理
|
||||
const valProxy = singleLevel
|
||||
? value
|
||||
: createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [key]: change.mutation.from },
|
||||
{ ...rawObj, [key]: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
|
||||
return valProxy;
|
||||
}
|
||||
|
||||
return value;
|
||||
function get(rawObj: T, key: KeyType, receiver: any): any {
|
||||
return baseGetFun(rawObj, key, receiver, listener, listeners, singleLevel);
|
||||
}
|
||||
|
||||
const proxy = new Proxy(rawObj, {
|
||||
const handler = {
|
||||
get,
|
||||
set,
|
||||
});
|
||||
set: baseSetFun,
|
||||
deleteProperty,
|
||||
has,
|
||||
ownKeys,
|
||||
};
|
||||
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
registerListener(rawObj, listener, listeners);
|
||||
|
||||
return proxy;
|
||||
return new Proxy(rawObj, handler);
|
||||
}
|
||||
|
|
|
@ -13,130 +13,43 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { resolveMutation } from '../../CommonUtils';
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { RAW_VALUE } from '../../Constants';
|
||||
import { getObserver } from '../ProxyHandler';
|
||||
import { KeyTypes } from '../../Constants';
|
||||
import { getValOrProxy, registerListener } from './HandlerUtils';
|
||||
import {
|
||||
baseGetFun,
|
||||
baseForEach,
|
||||
baseAddFunOfSet,
|
||||
baseHasFun,
|
||||
baseDeleteFun,
|
||||
baseClearFun,
|
||||
} from './BaseCollectionHandler';
|
||||
import { CurrentListener, Listeners } from '../../types/ProxyTypes';
|
||||
|
||||
const COLLECTION_CHANGE = '_collectionChange';
|
||||
export function createSetProxy<T extends Set<any>>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
|
||||
const listeners: Listeners = [];
|
||||
// 因为rawObj是Set类型,里面存放的是proxy对象,所以需要一个map来存放真实的对象和proxy对象的映射关系
|
||||
const valProxies = new Map();
|
||||
|
||||
export function createSetProxy<T extends Record<string, any>>(
|
||||
rawObj: T,
|
||||
listener: { current: (...args) => any },
|
||||
hookObserver = true
|
||||
): ProxyHandler<T> {
|
||||
let listeners: ((mutation) => Record<string, any>)[] = [];
|
||||
const proxies = new WeakMap();
|
||||
|
||||
// Set的add方法
|
||||
function add(
|
||||
rawObj: { add: (any) => void; has: (any) => boolean; values: () => any[] },
|
||||
value: any
|
||||
): Record<string, any> {
|
||||
if (!rawObj.has(proxies.get(value))) {
|
||||
const proxy = createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, valueChange: change.mutation.from },
|
||||
{ ...rawObj, valueChange: change.mutation.to }
|
||||
);
|
||||
listener.current({
|
||||
...change,
|
||||
mutation,
|
||||
});
|
||||
listeners.forEach(lst =>
|
||||
lst({
|
||||
...change,
|
||||
mutation,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
const oldValues = Array.from(rawObj.values());
|
||||
|
||||
proxies.set(value, proxy);
|
||||
|
||||
rawObj.add(proxies.get(value));
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{
|
||||
_type: 'Set',
|
||||
values: oldValues,
|
||||
},
|
||||
{
|
||||
_type: 'Set',
|
||||
values: Array.from(rawObj.values()),
|
||||
}
|
||||
);
|
||||
|
||||
observer.setProp(value, mutation);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
}
|
||||
|
||||
return rawObj;
|
||||
function add(rawObj: T, value: any): Record<string, any> {
|
||||
return baseAddFunOfSet(rawObj, value, listener, listeners, 'Set', valProxies);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function has(rawObj: { has: (string) => boolean }, value: any): boolean {
|
||||
|
||||
function has(rawObj: T, value: any): boolean {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(value);
|
||||
|
||||
return rawObj.has(proxies.get(value));
|
||||
return baseHasFun(rawObj, value, valProxies);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function deleteFun(
|
||||
rawObj: { has: (key: any) => boolean; delete: (value: any) => void; values: () => any[] },
|
||||
value: any
|
||||
) {
|
||||
const val = rawObj.has(proxies.get(value)) ? proxies.get(value) : value;
|
||||
if (rawObj.has(val)) {
|
||||
const oldValues = Array.from(rawObj.values());
|
||||
rawObj.delete(val);
|
||||
|
||||
proxies.delete(value);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{
|
||||
_type: 'Set',
|
||||
values: oldValues,
|
||||
},
|
||||
{
|
||||
_type: 'Set',
|
||||
values: Array.from(rawObj.values()),
|
||||
}
|
||||
);
|
||||
|
||||
observer.setProp(value, mutation);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
function deleteFun(rawObj: T, value: any) {
|
||||
return baseDeleteFun(rawObj, value, 'Set', valProxies);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function clear(rawObj: { size: number; clear: () => void }) {
|
||||
const oldSize = rawObj.size;
|
||||
rawObj.clear();
|
||||
|
||||
if (oldSize > 0) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.allChange();
|
||||
}
|
||||
function clear(rawObj: T) {
|
||||
baseClearFun(rawObj, valProxies, 'Set');
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function size(rawObj: { size: number }) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
return rawObj.size;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const handler = {
|
||||
get,
|
||||
add,
|
||||
|
@ -151,86 +64,21 @@ export function createSetProxy<T extends Record<string, any>>(
|
|||
[typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf,
|
||||
};
|
||||
|
||||
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
||||
if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
||||
const value = Reflect.get(handler, key, receiver);
|
||||
return value.bind(null, rawObj);
|
||||
}
|
||||
|
||||
if (key === 'size') {
|
||||
return size(rawObj);
|
||||
}
|
||||
|
||||
if (key === 'addListener') {
|
||||
return listener => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'removeListener') {
|
||||
return listener => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
if (key === 'watch') {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (key === RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
return Reflect.get(rawObj, key, receiver);
|
||||
function get(rawObj: T, key: any, receiver: any): any {
|
||||
return baseGetFun(rawObj, key, receiver, listeners, handler, 'Set');
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function wrapIterator(rawObj: Record<string, any>, rawIt: { next: () => { value: any; done: boolean } }) {
|
||||
function wrapIterator(rawObj: T, rawIt: IterableIterator<any>) {
|
||||
const observer = getObserver(rawObj);
|
||||
const hookObserver = hookObserverMap.get(rawObj);
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||
|
||||
return {
|
||||
next() {
|
||||
const currentListener = {
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, valueChange: change.mutation.from },
|
||||
{ ...rawObj, valueChange: change.mutation.to }
|
||||
);
|
||||
listener.current({
|
||||
...change,
|
||||
mutation,
|
||||
});
|
||||
listeners.forEach(lst =>
|
||||
lst({
|
||||
...change,
|
||||
mutation,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
const { value, done } = rawIt.next();
|
||||
if (done) {
|
||||
return { value: createProxy(value, currentListener, hookObserver), done };
|
||||
if (!done) {
|
||||
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||
}
|
||||
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
|
||||
const newVal = createProxy(value, currentListener, hookObserver);
|
||||
|
||||
return { value: newVal, done };
|
||||
return { value: getValOrProxy('valueChange', false, value, rawObj, listener, listeners), done };
|
||||
},
|
||||
// 判断Symbol类型,兼容IE
|
||||
[typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']() {
|
||||
|
@ -239,72 +87,27 @@ export function createSetProxy<T extends Record<string, any>>(
|
|||
};
|
||||
}
|
||||
|
||||
function keys(rawObj: { keys: () => { next: () => { value: any; done: boolean } } }) {
|
||||
function keys(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.keys());
|
||||
}
|
||||
|
||||
function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) {
|
||||
function values(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.values());
|
||||
}
|
||||
|
||||
function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) {
|
||||
function entries(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.entries());
|
||||
}
|
||||
|
||||
function forOf(rawObj: {
|
||||
entries: () => { next: () => { value: any; done: boolean } };
|
||||
values: () => { next: () => { value: any; done: boolean } };
|
||||
}) {
|
||||
const iterator = rawObj.values();
|
||||
return wrapIterator(rawObj, iterator);
|
||||
function forOf(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.values());
|
||||
}
|
||||
|
||||
function forEach(
|
||||
rawObj: { forEach: (callback: (value: any, key: any) => void) => void },
|
||||
callback: (valProxy: any, keyProxy: any, rawObj: any) => void
|
||||
) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
rawObj.forEach((value, key) => {
|
||||
const currentListener = {
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, valueChange: change.mutation.from },
|
||||
{ ...rawObj, valueChange: change.mutation.to }
|
||||
);
|
||||
listener.current({
|
||||
...change,
|
||||
mutation,
|
||||
});
|
||||
listeners.forEach(lst =>
|
||||
lst({
|
||||
...change,
|
||||
mutation,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
const valProxy = createProxy(value, currentListener, hookObserverMap.get(rawObj));
|
||||
const keyProxy = createProxy(key, currentListener, hookObserverMap.get(rawObj));
|
||||
// 最后一个参数要返回代理对象
|
||||
return callback(valProxy, keyProxy, rawObj);
|
||||
});
|
||||
function forEach(rawObj: T, callback: (valProxy: any, keyProxy: any, rawObj: any) => void) {
|
||||
baseForEach(rawObj, callback, listener, listeners);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const boundHandler = {};
|
||||
Object.entries(handler).forEach(([id, val]) => {
|
||||
boundHandler[id] = (...args: any[]) => {
|
||||
return (val as any)(...args, hookObserver);
|
||||
};
|
||||
});
|
||||
return new Proxy(rawObj, { ...boundHandler });
|
||||
|
||||
registerListener(rawObj, listener, listeners);
|
||||
|
||||
return new Proxy(rawObj, handler);
|
||||
}
|
||||
|
|
|
@ -13,197 +13,64 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { getObserver } from '../ProxyHandler';
|
||||
import { isSame } from '../../CommonUtils';
|
||||
import { resolveMutation } from '../../CommonUtils';
|
||||
import { isPanelActive } from '../../devtools';
|
||||
import { RAW_VALUE } from '../../Constants';
|
||||
import { getValOrProxy, registerListener } from './HandlerUtils';
|
||||
import { CurrentListener, Listeners, ObjectType } from '../../types/ProxyTypes';
|
||||
import { baseDeleteFun, baseGetFun } from './BaseCollectionHandler';
|
||||
|
||||
const COLLECTION_CHANGE = '_collectionChange';
|
||||
|
||||
export function createWeakMapProxy(
|
||||
rawObj: Record<string, any>,
|
||||
listener: { current: (...args) => any },
|
||||
hookObserver = true
|
||||
): Record<string, any> {
|
||||
let listeners: ((mutation) => Record<string, any>)[] = [];
|
||||
export function createWeakMapProxy<T extends WeakMap<any, any>>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
|
||||
const listeners: Listeners = [];
|
||||
|
||||
const handler = {
|
||||
get,
|
||||
set,
|
||||
add,
|
||||
delete: deleteFun,
|
||||
clear,
|
||||
has,
|
||||
};
|
||||
|
||||
function getFun(rawObj: { get: (key: any) => any }, key: any) {
|
||||
function getFun(rawObj: T, key: any) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(key);
|
||||
|
||||
const value = rawObj.get(key);
|
||||
// 对于value也需要进一步代理
|
||||
const valProxy = createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [key]: change.mutation.from },
|
||||
{ ...rawObj, [key]: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
|
||||
return valProxy;
|
||||
return getValOrProxy(key, false, value, rawObj, listener, listeners);
|
||||
}
|
||||
|
||||
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
||||
if (key === 'get') {
|
||||
return getFun.bind(null, rawObj);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
||||
const value = Reflect.get(handler, key, receiver);
|
||||
return value.bind(null, rawObj);
|
||||
}
|
||||
|
||||
if (key === 'watch') {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'addListener') {
|
||||
return listener => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'removeListener') {
|
||||
return listener => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
return Reflect.get(rawObj, key, receiver);
|
||||
function get(rawObj: T, key: any, receiver: any): any {
|
||||
return baseGetFun(rawObj, key, receiver, listeners, handler, 'WeakMap', getFun);
|
||||
}
|
||||
|
||||
// Map的set方法
|
||||
function set(
|
||||
rawObj: { get: (key: any) => any; set: (key: any, value: any) => any; has: (key: any) => boolean },
|
||||
key: any,
|
||||
value: any
|
||||
) {
|
||||
function set(rawObj: T, key: any, value: any) {
|
||||
const oldValue = rawObj.get(key);
|
||||
const newValue = value;
|
||||
rawObj.set(key, newValue);
|
||||
const valChange = !isSame(newValue, oldValue);
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
rawObj.set(key, value);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : resolveMutation(null, rawObj);
|
||||
|
||||
if (valChange || !rawObj.has(key)) {
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
}
|
||||
|
||||
if (valChange) {
|
||||
if (observer.watchers?.[key]) {
|
||||
observer.watchers[key].forEach(cb => {
|
||||
cb(key, oldValue, newValue, mutation);
|
||||
});
|
||||
}
|
||||
|
||||
observer.setProp(key, mutation);
|
||||
if (!isSame(value, oldValue)) {
|
||||
observer.setProp(key, mutation, oldValue, value);
|
||||
}
|
||||
|
||||
return rawObj;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Set的add方法
|
||||
function add(
|
||||
rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean },
|
||||
value: any
|
||||
): Record<string, any> {
|
||||
const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
||||
if (!rawObj.has(value)) {
|
||||
rawObj.add(value);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = isPanelActive()
|
||||
? resolveMutation(oldCollection, rawObj)
|
||||
: { mutation: true, from: null, to: rawObj };
|
||||
observer.setProp(value, mutation);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
}
|
||||
|
||||
return rawObj;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function has(rawObj: { has: (string) => boolean }, key: any): boolean {
|
||||
function has(rawObj: T, key: any): boolean {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(key);
|
||||
|
||||
return rawObj.has(key);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function clear(rawObj: { size: number; clear: () => void }) {
|
||||
const oldSize = rawObj.size;
|
||||
rawObj.clear();
|
||||
|
||||
if (oldSize > 0) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.allChange();
|
||||
}
|
||||
function deleteFun(rawObj: T, key: any) {
|
||||
return baseDeleteFun(rawObj, key, 'WeakMap');
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) {
|
||||
const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
||||
if (rawObj.has(key)) {
|
||||
rawObj.delete(key);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = isPanelActive()
|
||||
? resolveMutation(oldCollection, rawObj)
|
||||
: { mutation: true, from: null, to: rawObj };
|
||||
observer.setProp(key, mutation);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
registerListener(rawObj, listener, listeners);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const boundHandler = {};
|
||||
Object.entries(handler).forEach(([id, val]) => {
|
||||
boundHandler[id] = (...args: any[]) => {
|
||||
return (val as any)(...args, hookObserver);
|
||||
};
|
||||
});
|
||||
return new Proxy(rawObj, { ...boundHandler });
|
||||
return new Proxy(rawObj as ObjectType, handler as any);
|
||||
}
|
||||
|
|
|
@ -13,16 +13,14 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { resolveMutation } from '../../CommonUtils';
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { RAW_VALUE } from '../../Constants';
|
||||
import { registerListener } from './HandlerUtils';
|
||||
import { CurrentListener, Listeners } from '../../types/ProxyTypes';
|
||||
import { baseGetFun, baseAddFunOfSet, baseHasFun, baseDeleteFun } from './BaseCollectionHandler';
|
||||
import { getObserver } from '../ProxyHandler';
|
||||
|
||||
export function createWeakSetProxy<T extends Record<string, any>>(
|
||||
rawObj: T,
|
||||
listener: { current: (...args) => any },
|
||||
hookObserver = true
|
||||
): ProxyHandler<T> {
|
||||
let listeners: ((mutation) => Record<string, any>)[] = [];
|
||||
export function createWeakSetProxy<T extends WeakSet<any>>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
|
||||
const listeners: Listeners = [];
|
||||
// 因为rawObj是WeakSet类型,里面存放的是proxy对象,所以需要一个map来存放真实的对象和proxy对象的映射关系
|
||||
const proxies = new WeakMap();
|
||||
|
||||
const handler = {
|
||||
|
@ -32,112 +30,26 @@ export function createWeakSetProxy<T extends Record<string, any>>(
|
|||
has,
|
||||
};
|
||||
|
||||
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
||||
if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
||||
const value = Reflect.get(handler, key, receiver);
|
||||
return value.bind(null, rawObj);
|
||||
}
|
||||
if (key === 'addListener') {
|
||||
return listener => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'removeListener') {
|
||||
return listener => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
if (key === 'watch') {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (key === RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
return Reflect.get(rawObj, key, receiver);
|
||||
function get(rawObj: T, key: any, receiver: any): any {
|
||||
return baseGetFun(rawObj, key, receiver, listeners, handler, 'WeakSet');
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Set的add方法
|
||||
function add(rawObj: { add: (any) => void; has: (any) => boolean }, value: any): Record<string, any> {
|
||||
if (!rawObj.has(proxies.get(value))) {
|
||||
const proxy = createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [value]: change.mutation.from },
|
||||
{ ...rawObj, [value]: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
|
||||
proxies.set(value, proxy);
|
||||
|
||||
rawObj.add(proxies.get(value));
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = { mutation: true, from: rawObj, to: value };
|
||||
|
||||
observer.setProp(value, mutation);
|
||||
}
|
||||
|
||||
return rawObj;
|
||||
function add(rawObj: T, value: any): Record<string, any> {
|
||||
return baseAddFunOfSet(rawObj, value, listener, listeners, 'WeakSet', proxies);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function has(rawObj: { has: (string) => boolean }, value: any): boolean {
|
||||
|
||||
function has(rawObj: T, value: any): boolean {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(value);
|
||||
|
||||
return rawObj.has(proxies.get(value));
|
||||
return baseHasFun(rawObj, value, proxies);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function deleteFun(rawObj: { has: (key: any) => boolean; delete: (value: any) => void }, value: any) {
|
||||
if (rawObj.has(proxies.get(value))) {
|
||||
rawObj.delete(proxies.get(value));
|
||||
|
||||
proxies.delete(value);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = { mutation: true, from: value, to: rawObj };
|
||||
|
||||
observer.setProp(value, mutation);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
function deleteFun(rawObj: T, value: any) {
|
||||
return baseDeleteFun(rawObj, value, 'WeakSet', proxies);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const boundHandler = {};
|
||||
Object.entries(handler).forEach(([id, val]) => {
|
||||
boundHandler[id] = (...args: any[]) => {
|
||||
return (val as any)(...args, hookObserver);
|
||||
};
|
||||
});
|
||||
return new Proxy(rawObj, { ...boundHandler });
|
||||
|
||||
registerListener(rawObj, listener, listeners);
|
||||
|
||||
return new Proxy(rawObj, handler);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 { 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;
|
||||
|
||||
export function computed<T>(fn: ComputedFN<T>): ComputedImpl<T> {
|
||||
return new ComputedImpl(fn);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
private readonly observer: Observer = new Observer();
|
||||
readonly _isRef = true;
|
||||
|
||||
constructor(fn: ComputedFN<T>) {
|
||||
this.fn = fn;
|
||||
this.rContext = new RContext(this.updateValue.bind(this));
|
||||
|
||||
// 先运行一次
|
||||
this.rContext.run();
|
||||
}
|
||||
|
||||
get value() {
|
||||
this.observer.useProp('value');
|
||||
|
||||
return this._value;
|
||||
}
|
||||
|
||||
updateValue() {
|
||||
const oldValue = this._value;
|
||||
this._value = this.fn(oldValue);
|
||||
|
||||
if (!isSame(oldValue, this._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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { Observer } from '../proxy/Observer';
|
||||
import { FnType } from '../types/ProxyTypes';
|
||||
|
||||
export type RContextFn = FnType;
|
||||
export type RContextSet = Set<RContext>;
|
||||
let currentRContext: RContext | null = null;
|
||||
|
||||
const reactiveContextStack: RContext[] = [];
|
||||
|
||||
export class RContext {
|
||||
// 记录当前RContext的运行次数,用于解决在watchEffect中的数据发生变化导致RContext重新运行的问题
|
||||
runs = 0;
|
||||
|
||||
fn: RContextFn;
|
||||
|
||||
// 记录该RContext中使用到的Reactive中的RContextSet
|
||||
reactiveDependents: Set<RContextSet> | null = null;
|
||||
|
||||
constructor(fn: RContextFn) {
|
||||
this.fn = fn;
|
||||
}
|
||||
|
||||
start() {
|
||||
cleanupRContext(this);
|
||||
currentRContext = this;
|
||||
reactiveContextStack.push(this);
|
||||
|
||||
return endRContext;
|
||||
}
|
||||
|
||||
run() {
|
||||
const end = this.start();
|
||||
try {
|
||||
this.runs++;
|
||||
this.fn();
|
||||
} finally {
|
||||
this.runs--;
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
cleanupRContext(this);
|
||||
}
|
||||
}
|
||||
|
||||
function endRContext() {
|
||||
reactiveContextStack.pop();
|
||||
currentRContext = reactiveContextStack[reactiveContextStack.length - 1] ?? null;
|
||||
}
|
||||
|
||||
// 清除 RContext和响应式数据的绑定,双向清除
|
||||
function cleanupRContext(rContext: RContext) {
|
||||
if (rContext.reactiveDependents !== null) {
|
||||
for (const usedRContexts of rContext.reactiveDependents) {
|
||||
usedRContexts.delete(rContext);
|
||||
}
|
||||
|
||||
rContext.reactiveDependents.clear();
|
||||
rContext.reactiveDependents = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function addRContext(observer: Observer, prop: string | symbol) {
|
||||
if (currentRContext !== null) {
|
||||
if (!observer.rContexts[prop]) {
|
||||
observer.rContexts[prop] = new Set();
|
||||
}
|
||||
|
||||
if (!observer.rContexts[prop].has(currentRContext)) {
|
||||
observer.rContexts[prop].add(currentRContext);
|
||||
}
|
||||
|
||||
if (currentRContext.reactiveDependents === null) {
|
||||
currentRContext.reactiveDependents = new Set<RContextSet>();
|
||||
}
|
||||
currentRContext.reactiveDependents.add(observer.rContexts[prop]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { 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;
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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 { isObject, isSame, isShallow } from '../CommonUtils';
|
||||
import { reactive, toRaw } from './Reactive';
|
||||
import { Observer } from '../proxy/Observer';
|
||||
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>>;
|
||||
export function ref(value?: unknown) {
|
||||
return createRef(value, false);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
class RefImpl<T> {
|
||||
private _value: T;
|
||||
private _rawValue: T;
|
||||
|
||||
observer: Observer = new Observer();
|
||||
readonly _isRef = true;
|
||||
_isShallow = false;
|
||||
|
||||
constructor(value: T, isShallow: boolean) {
|
||||
this._isShallow = isShallow;
|
||||
this._rawValue = isShallow ? value : toRaw(value);
|
||||
this._value = isShallow ? value : toReactive(value);
|
||||
}
|
||||
|
||||
get value() {
|
||||
this.observer.useProp('value');
|
||||
return this._value;
|
||||
}
|
||||
|
||||
set value(newVal) {
|
||||
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', 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>;
|
||||
export function isRef(ref: any): ref is RefType {
|
||||
return Boolean(ref && ref._isRef);
|
||||
}
|
||||
|
||||
export function toReactive<T extends unknown>(value: T): T {
|
||||
return isObject(value) ? createProxy(value) : value;
|
||||
}
|
||||
|
||||
export function unref<T>(ref: MaybeRef<T>): T {
|
||||
return isRef(ref) ? ref.value : ref;
|
||||
}
|
||||
|
||||
declare const ShallowRefMarker: unique symbol;
|
||||
export type ShallowRef<T = any> = RefType<T> & { [ShallowRefMarker]?: true };
|
||||
export type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
|
||||
|
||||
export function shallowRef<T>(
|
||||
value: T
|
||||
): RefType extends T ? (T extends RefType ? IfAny<T, ShallowRef<T>, T> : ShallowRef<T>) : ShallowRef<T>;
|
||||
export function shallowRef<T = any>(): ShallowRef<T | undefined>;
|
||||
export function shallowRef(value?: unknown) {
|
||||
return createRef(value, true);
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 { 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 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 () => {
|
||||
source.removeListener(cb);
|
||||
};
|
||||
}
|
||||
|
||||
export function watchEffect(fn: () => void): any {
|
||||
if (typeof fn === 'function') {
|
||||
const rContext = new RContext(fn);
|
||||
|
||||
rContext.run();
|
||||
|
||||
return () => {
|
||||
rContext.stop();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
import { useEffect, useRef } from '../../renderer/hooks/HookExternal';
|
||||
import { getProcessingVNode } from '../../renderer/GlobalVar';
|
||||
import { createProxy } from '../proxy/ProxyHandler';
|
||||
import readonlyProxy from '../proxy/readonlyProxy';
|
||||
import readonlyProxy from '../proxy/ReadonlyProxy';
|
||||
import { Observer } from '../proxy/Observer';
|
||||
import { FunctionComponent, ClassComponent } from '../../renderer/vnode/VNodeTags';
|
||||
import { isPromise } from '../CommonUtils';
|
||||
|
@ -30,7 +30,7 @@ import type {
|
|||
StoreObj,
|
||||
UserActions,
|
||||
UserComputedValues,
|
||||
} from '../types';
|
||||
} from '../types/StoreTypes';
|
||||
import { VNode } from '../../renderer/vnode/VNode';
|
||||
import { devtools } from '../devtools';
|
||||
import {
|
||||
|
@ -43,6 +43,7 @@ import {
|
|||
SUBSCRIBED,
|
||||
UNSUBSCRIBED,
|
||||
} from '../devtools/constants';
|
||||
import { CurrentListener } from '../types/ProxyTypes';
|
||||
|
||||
const idGenerator = {
|
||||
id: 0,
|
||||
|
@ -99,7 +100,7 @@ export function clearVNodeObservers(vNode: VNode) {
|
|||
}
|
||||
|
||||
// 注册VNode销毁时的清理动作
|
||||
function registerDestroyFunction() {
|
||||
export function registerDestroyFunction() {
|
||||
const processingVNode = getProcessingVNode();
|
||||
|
||||
// 获取不到当前运行的VNode,说明不在组件中运行,属于非法场景
|
||||
|
@ -157,13 +158,15 @@ export function createStore<S extends Record<string, any>, A extends UserActions
|
|||
|
||||
const id = config.id || idGenerator.get('UNNAMED_STORE');
|
||||
|
||||
const listener = {
|
||||
const listener: CurrentListener = {
|
||||
current: listener => {},
|
||||
};
|
||||
|
||||
const proxyObj = createProxy(config.state, listener, !config.options?.isReduxAdapter);
|
||||
|
||||
proxyObj.$pending = false;
|
||||
if (proxyObj !== undefined) {
|
||||
proxyObj.$pending = false;
|
||||
}
|
||||
|
||||
const $a: Partial<StoreActions<S, A>> = {};
|
||||
const $queue: Partial<StoreActions<S, A>> = {};
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type KeyType = string | symbol;
|
||||
export type ObjectType = Record<KeyType, any>;
|
||||
export type FnType = () => any;
|
||||
|
||||
// Collection types
|
||||
export type MapTypes = Map<any, any> | WeakMap<any, any>;
|
||||
export type SetTypes = Set<any> | WeakSet<any>;
|
||||
export type IterableTypes = Map<any, any> | Set<any>;
|
||||
export type CollectionTypes = Map<any, any> | Set<any>;
|
||||
export type CollectionStringTypes = 'Map' | 'WeakMap' | 'Set' | 'WeakSet';
|
||||
|
||||
export type Listener = (change: any) => void;
|
||||
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> =
|
||||
T extends Map<infer K, infer V>
|
||||
? WatchProp<Map<K, AddWatchProp<V>>>
|
||||
: T extends WeakMap<infer K, infer V>
|
||||
? WatchProp<WeakMap<K, AddWatchProp<V>>>
|
||||
: T extends Set<infer U>
|
||||
? WatchProp<Set<AddWatchProp<U>>>
|
||||
: T extends WeakSet<infer U>
|
||||
? WatchProp<WeakSet<AddWatchProp<U>>>
|
||||
: T extends ObjectType
|
||||
? WatchProp<{ [K in keyof T]: AddWatchProp<T[K]> }>
|
||||
: T;
|
||||
|
||||
export interface IObserver {
|
||||
watchers: {
|
||||
[key: KeyType]: WatchHandler[];
|
||||
};
|
||||
|
||||
useProp: (key: KeyType) => void;
|
||||
|
||||
addListener: (listener: Listener) => void;
|
||||
|
||||
removeListener: (listener: () => void) => void;
|
||||
|
||||
setProp: (key: KeyType, mutation: any, oldValue?: any, newValue?: any) => void;
|
||||
|
||||
triggerChangeListeners: (mutation: any) => void;
|
||||
|
||||
triggerUpdate: (vNode: any) => void;
|
||||
|
||||
allChange: () => void;
|
||||
|
||||
arrayLengthChange: (length: number) => void;
|
||||
|
||||
clearByVNode: (vNode: any) => void;
|
||||
}
|
||||
|
||||
export type Mutation<T = any> = {
|
||||
mutation: boolean;
|
||||
from: T;
|
||||
to: T;
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { ShallowRef } from '../reactive/Ref';
|
||||
import { FnType, ObjectType } from './ProxyTypes';
|
||||
|
||||
export interface RefType<T = any> {
|
||||
value: T;
|
||||
}
|
||||
|
||||
export type MaybeRef<T = any> = T | RefType<T>;
|
||||
|
||||
export type UnwrapRef<T> =
|
||||
T extends ShallowRef<infer V> ? V : T extends RefType<infer V> ? ReactiveRet<V> : ReactiveRet<T>;
|
||||
|
||||
type BaseTypes = string | number | boolean;
|
||||
|
||||
export type ReactiveRet<T> = T extends FnType | BaseTypes | RefType
|
||||
? T
|
||||
: T extends Map<infer K, infer V>
|
||||
? Map<K, ReactiveRet<V>> & UnwrapRef<Omit<T, keyof Map<any, any>>>
|
||||
: T extends WeakMap<infer K, infer V>
|
||||
? WeakMap<K, ReactiveRet<V>> & UnwrapRef<Omit<T, keyof WeakMap<any, any>>>
|
||||
: T extends Set<infer V>
|
||||
? Set<ReactiveRet<V>> & UnwrapRef<Omit<T, keyof Set<any>>>
|
||||
: T extends WeakSet<infer V>
|
||||
? WeakSet<ReactiveRet<V>> & UnwrapRef<Omit<T, keyof WeakSet<any>>>
|
||||
: T extends ReadonlyArray<any>
|
||||
? { [K in keyof T]: ReactiveRet<T[K]> }
|
||||
: T extends ObjectType
|
||||
? {
|
||||
[P in keyof T]: P extends symbol ? T[P] : UnwrapRef<T[P]>;
|
||||
}
|
||||
: T;
|
|
@ -13,25 +13,13 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
export interface IObserver {
|
||||
useProp: (key: string | symbol) => void;
|
||||
import { AddWatchProp } from './ProxyTypes';
|
||||
|
||||
addListener: (listener: (mutation: any) => void) => void;
|
||||
|
||||
removeListener: (listener: (mutation: any) => void) => void;
|
||||
|
||||
setProp: (key: string | symbol, mutation: any) => void;
|
||||
|
||||
triggerChangeListeners: (mutation: any) => void;
|
||||
|
||||
triggerUpdate: (vNode: any) => void;
|
||||
|
||||
allChange: () => void;
|
||||
|
||||
clearByVNode: (vNode: any) => void;
|
||||
}
|
||||
|
||||
export type StoreConfig<S extends Record<string, unknown>, A extends UserActions<S>, C extends UserComputedValues<S>> = {
|
||||
export type StoreConfig<
|
||||
S extends Record<string, unknown>,
|
||||
A extends UserActions<S>,
|
||||
C extends UserComputedValues<S>,
|
||||
> = {
|
||||
id?: string;
|
||||
state?: S;
|
||||
actions?: A;
|
||||
|
@ -45,7 +33,11 @@ export type UserActions<S extends Record<string, unknown>> = {
|
|||
[K: string]: ActionFunction<S>;
|
||||
};
|
||||
|
||||
export type ActionFunction<S extends Record<string, unknown>> = (this: StoreObj<S, any, any>, state: S, ...args: any[]) => any;
|
||||
export type ActionFunction<S extends Record<string, unknown>> = (
|
||||
this: StoreObj<S, any, any>,
|
||||
state: S,
|
||||
...args: any[]
|
||||
) => any;
|
||||
|
||||
export type StoreActions<S extends Record<string, unknown>, A extends UserActions<S>> = {
|
||||
[K in keyof A]: Action<A[K], S>;
|
||||
|
@ -57,14 +49,19 @@ type Action<T extends ActionFunction<any>, S extends Record<string, unknown>> =
|
|||
) => ReturnType<T>;
|
||||
|
||||
export type StoreObj<S extends Record<string, unknown>, A extends UserActions<S>, C extends UserComputedValues<S>> = {
|
||||
$s: S;
|
||||
$s: AddWatchProp<S>;
|
||||
$state: AddWatchProp<S>;
|
||||
// $s: S;
|
||||
// $state: S;
|
||||
$a: StoreActions<S, A>;
|
||||
$c: UserComputedValues<S>;
|
||||
$queue: QueuedStoreActions<S, A>;
|
||||
$listeners;
|
||||
$subscribe: (listener: (mutation) => void) => void;
|
||||
$unsubscribe: (listener: (mutation) => void) => void;
|
||||
} & { [K in keyof S]: S[K] } & { [K in keyof A]: Action<A[K], S> } & { [K in keyof C]: ReturnType<C[K]> };
|
||||
} & { [K in keyof AddWatchProp<S>]: AddWatchProp<S>[K] } & { [K in keyof A]: Action<A[K], S> } & {
|
||||
[K in keyof C]: ReturnType<C[K]>;
|
||||
};
|
||||
|
||||
export type PlannedAction<S extends Record<string, unknown>, F extends ActionFunction<S>> = {
|
||||
action: string;
|
|
@ -1,3 +1,4 @@
|
|||
packages:
|
||||
# all packages in direct subdirs of packages/
|
||||
- 'packages/*'
|
||||
- 'packages/**/*'
|
||||
|
|
Loading…
Reference in New Issue