Compare commits

...

5 Commits

Author SHA1 Message Date
chaoling d780a17448 feat: update vue-adapter 2024-04-22 21:12:17 +08:00
chaoling 8e7c6b3a72 feat: delete reactive/index 2024-04-19 15:26:56 +08:00
chaoling b725b0d98c feat: add lifecycle 2024-04-19 15:21:17 +08:00
chaoling d7101ff5e3 feat: add useWatch 2024-04-18 15:55:20 +08:00
chaoling 05f0610c99 feat: add vue-reactive apis 2024-04-11 14:13:49 +08:00
64 changed files with 5262 additions and 1195 deletions

View File

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

View File

@ -0,0 +1,18 @@
接口差异:
1、$patch不需要可以直接赋值
```ts
actions: {
setFoo(foo) {
// not support
this.$patch({ nested: { foo } });
// support
this.nested = { foo };
}
}
```
2、watch写法不同

View File

@ -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"
}
}

View File

@ -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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true
},
"ts-node": {
"esm": true
}
}

View File

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

View File

@ -0,0 +1,2 @@
## 适配Vue

View File

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

View File

@ -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"
}
}

View File

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

View File

@ -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')];

View File

@ -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';

View File

@ -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?.();
};
}, []);
}

View File

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

View File

@ -0,0 +1,10 @@
{
"files": [
"src/index.ts"
],
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": true,
"declarationDir": "./build/@types"
},
}

View File

@ -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, // jsts.
"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"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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';

View File

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

View File

@ -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++;

View File

@ -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', () => {

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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 {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
// 满足两个UT1、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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>> = {};

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
packages:
# all packages in direct subdirs of packages/
- 'packages/*'
- 'packages/**/*'