Compare commits

...

5 Commits

Author SHA1 Message Date
chaoling 96c4f0c337 feat: add inula-adapter/pinia 和 inula-adapter/vuex 2024-05-23 10:03:30 +08:00
chaoling 0f75e48f79 feat: update vue-adapter 2024-04-22 21:23:26 +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
79 changed files with 7560 additions and 1220 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,7 @@
## 适配Vue
### 生命周期
### pinia
### vuex

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,101 @@
## pinia接口差异
1、不支createPinia的相关函数使用了是没有效果的
```js
it('not support createPinia', () => {
const pinia = createPinia(); // 接口可以调用,但是没有效果
const store = useStore(pinia); // 传入pinia也没有效果
expect(store.name).toBe('a');
expect(store.$state.name).toBe('a');
expect(pinia.state.value.main.name).toBe('a'); // 不能通过pinia.state.value.main获取store
});
```
2、因为不支持createPinia同样也不支持setActivePinia、getActivePinia()
3、不支持store.$patch
```js
it('can not be set with patch', () => {
const pinia = createPinia();
const store = useStore(pinia);
store.$patch({ name: 'a' }); // 不支持
// 改成
store.$state.name = 'a'; // 可以改成直接赋值
expect(store.name).toBe('a');
});
```
4、不支持store.$reset();
```ts
it('can not reset the state', () => {
const store = useStore();
store.name = 'Ed';
store.nested.n++;
store.$reset(); // 不支持
expect(store.$state).toEqual({
counter: 0,
name: 'Eduardo',
nested: {
n: 0,
},
});
});
```
5、不支持store.$dispose可以用store.$unsubscribe代替
```js
it('can not be disposed', () => {
const useStore = defineStore({
id: 'main',
state: () => ({ n: 0 }),
});
const store = useStore();
const spy = vi.fn();
store.$subscribe(spy, { flush: 'sync' });
store.$state.n++;
expect(spy).toHaveBeenCalledTimes(1);
expect(useStore()).toBe(store);
// store.$dispose();
// 改成
store.$unsubscribe(spy);
store.$state.n++;
expect(spy).toHaveBeenCalledTimes(1);
});
```
6、支持$subscribe不需要flush默认就是sync
```js
it('can be $unsubscribe', () => {
const useStore = defineStore({
id: 'main',
state: () => ({ n: 0 }),
});
const store = useStore();
const spy = vi.fn();
// store.$subscribe(spy, { flush: 'sync' });
// 不需要flush默认就是sync
store.$subscribe(spy, { flush: 'sync' });
store.$state.n++;
expect(spy).toHaveBeenCalledTimes(1);
expect(useStore()).toBe(store);
store.$unsubscribe(spy);
store.$state.n++;
expect(spy).toHaveBeenCalledTimes(1);
});
```

View File

@ -0,0 +1,10 @@
## 适配Vue
### 生命周期
onBeforeMount
onMounted
onBeforeUpdate
onUpdated
onBeforeUnmount
onUnmounted

View File

@ -0,0 +1,188 @@
## vuex接口差异
1、createStore接口中的state、mutations、actions、getters不能用相同名字属性
```js
it('dispatching actions, sync', () => {
const store = createStore({
state: {
a: 1
},
mutations: {
[TEST] (state, n) {
state.a += n
}
},
actions: {
[TEST] ({ commit }, n) {
commit(TEST, n)
}
}
})
store.dispatch(TEST, 2)
expect(store.state.a).toBe(3)
})
```
2、createStore接口不支持strict属性
```js
const store = createStore({
state: {
a: 1
},
mutations: {
[TEST] (state, n) {
state.a += n
}
},
actions: {
[TEST] ({ commit }, n) {
commit(TEST, n)
}
},
strict: true
})
```
3、store.registerModule不支持传入数组只支持一层的module注册
```js
it('dynamic module registration with namespace inheritance', () => {
const store = createStore({
modules: {
a: {
namespaced: true,
},
},
});
const actionSpy = vi.fn();
const mutationSpy = vi.fn();
store.registerModule(['a', 'b'], {
state: { value: 1 },
getters: { foo: state => state.value },
actions: { foo: actionSpy },
mutations: { foo: mutationSpy },
});
expect(store.state.a.b.value).toBe(1);
expect(store.getters['a/foo']).toBe(1);
store.dispatch('a/foo');
expect(actionSpy).toHaveBeenCalled();
store.commit('a/foo');
expect(mutationSpy).toHaveBeenCalled();
});
```
4、createStore不支持多层mudule注册
```js
it('module: mutation', function () {
const mutations = {
[TEST](state, n) {
state.a += n;
},
};
const store = createStore({
state: {
a: 1,
},
mutations,
modules: { // 第一层module支持
nested: {
state: {a: 2},
mutations,
modules: { // 第二层module不支持
one: {
state: {a: 3},
mutations,
},
nested: {
modules: { // 第三层module不支持
two: {
state: {a: 4},
mutations,
},
three: {
state: {a: 5},
mutations,
},
},
},
},
},
four: {
state: {a: 6},
mutations,
},
},
});
});
```
4、不支持store.replaceState
```js
const store = createStore({});
store.replaceState({ a: { foo: 'state' } });
```
5、store.registerModule不支持传入options参数
```js
store.registerModule(
'a',
{
namespaced: true,
getters: { foo: state => state.foo },
actions: { foo: actionSpy },
mutations: { foo: mutationSpy },
},
{ preserveState: true }
);
```
5、dispatch不支持传入options参数
```js
dispatch('foo', null, {root: true});
```
6、不支持action中的root属性action需要是个函数
```js
const store = createStore({
modules: {
a: {
namespaced: true,
actions: {
[TEST]: {
root: true,
handler() {
return 1;
},
},
},
},
}
});
```
7、createStore中不支持plugins
```js
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST](state, n) {
state.a += n;
},
},
actions: {
[TEST]: actionSpy,
},
plugins: [ // 不支持plugins
store => {
initState = store.state;
store.subscribe((mut, state) => {
expect(state).toBe(state);
mutations.push(mut);
});
store.subscribeAction(subscribeActionSpy);
},
],
});
```

View File

@ -0,0 +1,5 @@
{
"module": "./esm/pinia-adapter.js",
"main": "./cjs/pinia-adapter.js",
"types": "./@types/index.d.ts"
}

View File

@ -0,0 +1,5 @@
{
"module": "./esm/vuex-adapter.js",
"main": "./cjs/vuex-adapter.js",
"types": "./@types/index.d.ts"
}

View File

@ -0,0 +1,62 @@
{
"name": "@inula/vue-adapter",
"version": "0.0.1",
"description": "vue adapter",
"main": "./vue/cjs/vue-adapter.js",
"module": "./vue/esm/vue-adapter.js",
"types": "./vue/@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.vue.json && tsc -p tsconfig.pinia.json && tsc -p tsconfig.vuex.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",
"@testing-library/user-event": "^12.1.10"
},
"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(name) {
return {
input: [`./build/${name}/@types/${name}/index.d.ts`],
output: {
file: `./build/${name}/@types/index.d.ts`,
},
plugins: [dts(), cleanUp([`./build/${name}/@types/`])],
};
}
export default [buildTypeConfig('vue'), buildTypeConfig('pinia'), buildTypeConfig('vuex')];

View File

@ -0,0 +1,113 @@
/*
* 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 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, name) => {
const prod = mode.startsWith('prod');
const outputList = [
{
file: path.join(outDir, `${name}/cjs/${name}-adapter.${prod ? 'min.' : ''}js`),
sourcemap: 'true',
format: 'cjs',
},
{
file: path.join(outDir, `${name}/umd/${name}-adapter.${prod ? 'min.' : ''}js`),
name: 'VueAdapter',
sourcemap: 'true',
format: 'umd',
},
];
if (!prod) {
outputList.push({
file: path.join(outDir, `${name}/esm/${name}-adapter.js`),
sourcemap: 'true',
format: 'esm',
});
}
return {
input: path.join(rootDir, `/src/${name}/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(),
name === 'vue'
? copyFiles([
{
from: path.join(rootDir, 'package.json'),
to: path.join(outDir, 'package.json'),
},
{
from: path.join(rootDir, 'README.md'),
to: path.join(outDir, 'README.md'),
},
])
: copyFiles([
{
from: path.join(rootDir, `npm/${name}/package.json`),
to: path.join(outDir, `${name}/package.json`),
},
]),
],
};
};
function copyFiles(copyPairs) {
return {
name: 'copy-files',
generateBundle() {
copyPairs.forEach(({ from, to }) => {
const destDir = path.dirname(to);
// 判断目标文件夹是否存在
if (!fs.existsSync(destDir)) {
// 目标文件夹不存在,创建它
fs.mkdirSync(destDir, { recursive: true });
}
fs.copyFileSync(from, to);
});
},
};
}
export default [
getConfig('dev', 'vue'),
getConfig('prod', 'vue'),
getConfig('dev', 'pinia'),
getConfig('prod', 'pinia'),
getConfig('dev', 'vuex'),
getConfig('prod', 'vuex'),
];

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
* 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.
@ -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,208 @@
/*
* 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 { createStore, StoreObj, vueReactive } from 'openinula';
import {
FilterAction,
FilterComputed,
FilterState,
StoreDefinition,
StoreSetup,
Store,
AnyFunction,
ActionType,
StoreToRefsReturn,
} from './types';
const { ref, isRef, toRef, isReactive, isReadonly } = vueReactive;
const storeMap = new Map<string, any>();
export function defineStore<
Id extends string,
S extends Record<string, unknown>,
A extends Record<string, AnyFunction>,
C extends Record<string, AnyFunction>,
>(definition: StoreDefinition<Id, S, A, C>): (pinia?: any) => Store<S, A, C>;
export function defineStore<
Id extends string,
S extends Record<string, unknown>,
A extends Record<string, AnyFunction>,
C extends Record<string, AnyFunction>,
>(id: Id, definition: Omit<StoreDefinition<Id, S, A, C>, 'id'>): (pinia?: any) => Store<S, A, C>;
export function defineStore<Id extends string, SS extends Record<any, unknown>>(
id: Id,
setup: StoreSetup<SS>
): (pinia?: any) => Store<FilterState<SS>, FilterAction<SS>, FilterComputed<SS>>;
export function defineStore(idOrDef: any, setupOrDef?: any) {
let id: string;
let definition: StoreDefinition | StoreSetup;
let isSetup = false;
if (typeof idOrDef === 'string') {
isSetup = typeof setupOrDef === 'function';
id = idOrDef;
definition = setupOrDef;
} else {
id = idOrDef.id;
definition = idOrDef;
}
if (isSetup) {
return defineSetupStore(id, definition as StoreSetup);
} else {
return defineOptionsStore(id, definition as StoreDefinition);
}
}
/**
* createStore实现中会给actions增加第一个参数storepinia不需要
* @param actions
*/
function enhanceActions(
actions?: ActionType<Record<string, AnyFunction>, Record<string, unknown>, Record<string, AnyFunction>>
) {
if (!actions) {
return {};
}
return Object.fromEntries(
Object.entries(actions).map(([key, value]) => {
return [
key,
function (this: StoreObj, state: Record<string, unknown>, ...args: any[]) {
return value.bind(this)(...args);
},
];
})
);
}
function defineOptionsStore(id: string, definition: StoreDefinition) {
const state = definition.state ? definition.state() : {};
const computed = definition.getters || {};
const actions = enhanceActions(definition.actions) || {};
return () => {
if (storeMap.has(id)) {
return storeMap.get(id)!();
}
const useStore = createStore({
id,
state,
actions,
computed,
});
storeMap.set(id, useStore);
return useStore();
};
}
function defineSetupStore<SS extends Record<string, unknown>>(id: string, storeSetup: StoreSetup<SS>) {
return () => {
const data = storeSetup();
if (!data) {
return {};
}
if (storeMap.has(id)) {
return storeMap.get(id)!();
}
const state: Record<string, unknown> = {};
const actions: Record<string, AnyFunction> = {};
const getters: Record<string, AnyFunction> = {};
for (const key in data) {
const prop = data[key];
if ((isRef(prop) && !isReadonly(prop)) || isReactive(prop)) {
// state
state[key] = prop;
} else if (typeof prop === 'function') {
// action
actions[key] = prop as AnyFunction;
} else if (isRef(prop) && isReadonly(prop)) {
// getters
getters[key] = (prop as any).fn as AnyFunction;
}
}
const useStore = createStore({
id,
state,
computed: getters,
actions: enhanceActions(actions),
});
storeMap.set(id, useStore);
return useStore();
};
}
export function mapStores<
S extends Record<string, unknown>,
A extends Record<string, AnyFunction>,
C extends Record<string, AnyFunction>,
>(...stores: (() => Store<S, A, C>)[]): { [key: string]: () => Store<S, A, C> } {
const result: { [key: string]: () => Store<S, A, C> } = {};
stores.forEach((store: () => Store<S, A, C>) => {
const expandedStore = store();
result[`${expandedStore.id}Store`] = () => expandedStore;
});
return result;
}
export function storeToRefs<
S extends Record<string, unknown>,
A extends Record<string, AnyFunction>,
C extends Record<string, AnyFunction>,
>(store: Store<S, A, C>): StoreToRefsReturn<S, C> {
const stateRefs = Object.fromEntries(
Object.entries(store.$s || {}).map(([key, value]) => {
return [key, ref(value)];
})
);
const getterRefs = Object.fromEntries(
Object.entries(store.$config.computed || {}).map(([key, value]) => {
const computeFn = (value as () => any).bind(store, store.$s);
return [key, toRef(computeFn)];
})
);
return { ...stateRefs, ...getterRefs } as StoreToRefsReturn<S, C>;
}
export function createPinia() {
console.warn(
`The pinia-adapter in Horizon does not support the createPinia interface. Please modify your code accordingly.`
);
const result = {
install: (app: any) => {},
use: (plugin: any) => result,
state: {},
};
return result;
}

View File

@ -0,0 +1,104 @@
/*
* 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 type {RefType, StoreObj, UnwrapRef, UserActions, UserComputedValues, ComputedImpl} from 'openinula';
import type { RefType, UnwrapRef, ComputedImpl } from 'openinula';
export type StoreSetup<R = Record<string, unknown>> = () => R;
export type AnyFunction = (...args: any[]) => any;
// defineStore init type
export interface StoreDefinition<
Id extends string = string,
S extends Record<string, unknown> = Record<string, unknown>,
A extends Record<string, AnyFunction> = Record<string, AnyFunction>,
C extends Record<string, AnyFunction> = Record<string, AnyFunction>,
> {
id?: Id;
state?: () => S;
actions?: ActionType<A, S, C>;
getters?: ComputedType<C, S>;
}
// defineStore return type
export type Store<
S extends Record<string, unknown>,
A extends Record<string, AnyFunction>,
C extends Record<string, AnyFunction>,
> = {
$s: S;
$state: S;
$a: ActionType<A, S, C>;
$c: ComputedType<C, S>;
$subscribe: (listener: Listener) => void;
$unsubscribe: (listener: Listener) => void;
} & { [K in keyof S]: S[K] } & { [K in keyof ActionType<A, S, C>]: ActionType<A, S, C>[K] } & {
[K in keyof ComputedType<C, S>]: ReturnType<ComputedType<C, S>[K]>;
};
export type ActionType<A, S, C> = A & ThisType<A & UnwrapRef<S> & WithGetters<C>>;
type ComputedType<C, S> = {
[K in keyof C]: AddFirstArg<C[K], S>;
} & ThisType<UnwrapRef<S> & WithGetters<C>>;
type AddFirstArg<T, S> = T extends (...args: infer A) => infer R
? (state: S, ...args: A) => R
: T extends () => infer R
? (state: S) => R
: T;
// In Getter function, make this.xx can refer to other getters
export type WithGetters<G> = {
readonly [k in keyof G]: G[k] extends (...args: any[]) => infer R ? R : UnwrapRef<G[k]>;
};
type Listener = (change: any) => void;
// Filter state properties
export type FilterState<T extends Record<string, unknown>> = {
[K in FilterStateProperties<T>]: UnwrapRef<T[K]>;
};
type FilterStateProperties<T extends Record<string, unknown>> = {
[K in keyof T]: T[K] extends ComputedImpl
? never
: T[K] extends RefType
? K
: T[K] extends Record<any, unknown> // Reactive类型
? K
: never;
}[keyof T];
// Filter action properties
export type FilterAction<T extends Record<string, unknown>> = {
[K in FilterFunctionProperties<T>]: T[K] extends AnyFunction ? T[K] : never;
};
type FilterFunctionProperties<T extends Record<string, unknown>> = {
[K in keyof T]: T[K] extends AnyFunction ? K : never;
}[keyof T];
// Filter computed properties
export type FilterComputed<T extends Record<string, unknown>> = {
[K in FilterComputedProperties<T>]: T[K] extends ComputedImpl<infer T> ? (T extends AnyFunction ? T : never) : never;
};
type FilterComputedProperties<T extends Record<string, unknown>> = {
[K in keyof T]: T[K] extends ComputedImpl ? K : never;
}[keyof T];
export type StoreToRefsReturn<S extends Record<string, unknown>, C extends Record<string, AnyFunction>> = {
[K in keyof S]: RefType<S[K]>;
} & {
[K in keyof ComputedType<C, S>]: Readonly<RefType<ReturnType<ComputedType<C, S>[K]>>>;
};

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,69 @@
/*
* 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 { useEffect, useLayoutEffect, useRef } from 'openinula';
import { FN } from './types';
// 用于存储组件是否已挂载的状态
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return isMounted.current;
};
export const onBeforeMount = (fn: FN) => {
const isMounted = useIsMounted();
if (!isMounted) {
fn?.();
}
};
export function onMounted(fn: FN) {
useEffect(() => {
fn?.();
}, []);
}
export function onBeforeUpdated(fn: FN) {
useEffect(() => {
fn?.();
});
}
export function onUpdated(fn: FN) {
useEffect(() => {
fn?.();
});
}
export const onBeforeUnmount = (fn: FN) => {
useLayoutEffect(() => {
return () => {
fn?.();
};
}, []);
};
export function onUnmounted(fn: FN) {
useEffect(() => {
return () => {
fn?.();
};
}, []);
}

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 type FN = () => void;

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 './vuex';

View File

@ -0,0 +1,104 @@
/*
* 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 AnyFunction = (...args: any[]) => any;
export interface VuexStoreOptions<
State extends Record<string, unknown> = Record<string, unknown>,
Mutations extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Actions extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Getters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
RootState extends Record<string, unknown> = Record<string, unknown>,
RootGetters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Modules extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>,
> {
namespaced?: boolean;
state?: State | (() => State);
mutations?: MutationsType<Mutations, State>;
actions?: ActionsType<Actions, State, Getters, RootState, RootGetters>;
getters?: GettersType<State, Getters, State, Getters>;
modules?: {
[k in keyof Modules]: VuexStoreOptions<Modules[k]>;
};
}
type MutationsType<Mutations, State> = {
[K in keyof Mutations]: AddFirstArg<Mutations[K], State>;
};
type ActionsType<Actions, State, Getters, RootState, RootGetters> = {
[K in keyof Actions]: AddFirstArg<
Actions[K],
{
commit: CommitType;
dispatch: DispatchType;
state: State;
getters: Getters;
rootState: RootState;
rootGetters: RootGetters;
}
>;
};
type AddFirstArg<T, S> = T extends (arg1: any, ...args: infer A) => infer R
? (state: S, ...args: A) => R
: T extends () => infer R
? (state: S) => R
: T;
type GettersType<State, Getters, RootState, RootGetters> = {
[K in keyof Getters]: AddArgs<Getters[K], [State, Getters, RootState, RootGetters]>;
};
type AddArgs<T, Args extends any[]> = T extends (...args: infer A) => infer R
? (...args: [...Args, ...A]) => R
: T extends () => infer R
? (...args: Args) => R
: T;
export type CommitType = (
type: string | (Record<string, unknown> & { type: string }),
payload?: any,
options?: Record<string, unknown>,
moduleName?: string
) => void;
export type DispatchType = (
type: string | (Record<string, unknown> & { type: string }),
payload?: any,
options?: Record<string, unknown>,
moduleName?: string
) => any;
export type VuexStore<
State extends Record<string, unknown> = Record<string, unknown>,
Getters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Modules extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>,
> = {
state: State & {
[K in keyof Modules]: Modules[K] extends { state: infer ModuleState } ? ModuleState : Modules[K];
};
getters: {
[K in keyof Getters]: ReturnType<Getters[K]>;
};
commit: CommitType;
dispatch: DispatchType;
subscribe: AnyFunction;
subscribeAction: AnyFunction;
watch: (fn: (state: State, getters: Getters) => void, cb: AnyFunction) => void;
registerModule: (moduleName: string, module: VuexStoreOptions) => void;
unregisterModule: (moduleName: string) => void;
hasModule: (moduleName: string) => boolean;
};

View File

@ -0,0 +1,315 @@
/*
* 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 { createStore as createStoreX, StoreObj, vueReactive } from 'openinula';
import { VuexStore, VuexStoreOptions } from './types';
import { AnyFunction } from '../pinia/types';
const { watch } = vueReactive;
const MUTATION_PREFIX = 'm_';
const GETTER_PREFIX = 'g_';
type GettersMap<T extends StoreObj = StoreObj> = {
[K in keyof T['$c']]: ReturnType<T['$c'][K]>;
};
export function createStore<
State extends Record<string, unknown> = Record<string, unknown>,
Mutations extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Actions extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Getters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
RootState extends Record<string, unknown> = Record<string, unknown>,
RootGetters extends Record<string, AnyFunction> = Record<string, AnyFunction>,
Modules extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>,
>(
options: VuexStoreOptions<State, Mutations, Actions, Getters, RootState, RootGetters, Modules>
): VuexStore<State, Getters, Modules> {
const modules = options.modules || {};
const _modules: Record<string, { storeX: StoreObj; namespaced: boolean }> = {};
const _getters: GettersMap = {};
const vuexStore: VuexStore = {
state: new Proxy(
{},
{
get: (_, key) => {
if (key in _modules) {
return _modules[key as string].storeX;
} else {
return rootStoreX[key as string];
}
},
}
),
getters: new Proxy(
{},
{
get: (_, key) => {
if (typeof key === 'string') {
// 如果key包含/说明是访问模块的getters进行split
if (key.includes('/')) {
const [moduleName, getterKey] = key.split('/');
return _modules[moduleName].storeX[`${GETTER_PREFIX}${getterKey}`];
} else {
return _getters[`${GETTER_PREFIX}${key}`];
}
}
},
}
),
commit: (_type, _payload, _options, moduleName) => {
const { type, payload, options } = prepareTypeParams(_type, _payload, _options);
// 如果options.root为true调用根store的action
if (options?.root) {
return rootStoreX[`${MUTATION_PREFIX}${type}`](payload);
}
// 包含/说明是访问模块的mutation
if (type.includes('/')) {
const [moduleName, key] = type.split('/');
return _modules[moduleName].storeX[`${MUTATION_PREFIX}${key}`](payload);
}
if (moduleName != undefined) {
// dispatch到指定的module
return _modules[moduleName].storeX[`${MUTATION_PREFIX}${type}`](payload);
}
// 调用所有非namespaced的modules的mutation
Object.values(_modules).forEach(module => {
if (!module.namespaced) {
const mutation = module.storeX[`${MUTATION_PREFIX}${type}`];
if (typeof mutation === 'function') {
mutation(payload);
}
}
});
// 调用storeX对象上的方法
if (rootStoreX[`${MUTATION_PREFIX}${type}`]) {
rootStoreX[`${MUTATION_PREFIX}${type}`](payload);
}
},
dispatch: (_type, _payload, _options, moduleName) => {
const { type, payload, options } = prepareTypeParams(_type, _payload, _options);
// 如果options.root为true调用根store的action
if (options?.root) {
return rootStoreX[type](payload);
}
// 包含/说明是访问模块的action
if (type.includes('/')) {
const [moduleName, key] = type.split('/');
return _modules[moduleName].storeX[key](payload);
}
if (moduleName != undefined) {
// dispatch到指定的module
return _modules[moduleName].storeX[type](payload);
}
// 把每个action的返回值合并起来支持then链式调用
const results: any[] = [];
// 调用所有非namespaced的modules的action
Object.values(_modules).forEach(module => {
if (!module.namespaced) {
const action = module.storeX[type];
if (typeof action === 'function') {
results.push(action(payload));
}
}
});
// 调用storeX对象上的方法
if (typeof rootStoreX[type] === 'function') {
results.push(rootStoreX[type](payload));
}
// 返回一个Promise内容是results支持then链式调用
return Promise.all(results);
},
subscribe(fn) {
return rootStoreX.$subscribe(fn);
},
subscribeAction(fn) {
return rootStoreX.$subscribe(fn);
},
watch(fn, cb) {
watch(() => fn(vuexStore.state, vuexStore.getters), cb);
},
// 动态注册模块
registerModule(key, module) {
_modules[key] = { storeX: _createStoreX(key, module, vuexStore, rootStoreX), namespaced: !!module.namespaced };
collectGetters(_modules[key].storeX, _getters);
},
// 动态注销模块
unregisterModule(key) {
deleteGetters(_modules[key].storeX, _getters);
delete _modules[key];
},
hasModule(path) {
return path in _modules;
},
};
const rootStoreX = _createStoreX(undefined, options as VuexStoreOptions, vuexStore);
collectGetters(rootStoreX, _getters);
// 递归创建子模块
for (const [moduleName, moduleOptions] of Object.entries(modules)) {
_modules[moduleName] = {
storeX: _createStoreX(moduleName, moduleOptions as VuexStoreOptions, vuexStore, rootStoreX),
namespaced: !!(moduleOptions as VuexStoreOptions).namespaced,
};
collectGetters(_modules[moduleName].storeX, _getters);
}
return vuexStore as VuexStore<State, Getters, Modules>;
}
export function prepareTypeParams(
type: string | (Record<string, unknown> & { type: string }),
payload?: any,
options?: Record<string, unknown>
) {
if (typeof type === 'object' && type.type) {
options = payload;
payload = type;
type = type.type;
}
return { type, payload, options } as {
type: string;
payload: any;
options: Record<string, unknown>;
};
}
function _createStoreX(
moduleName: string | undefined,
options: VuexStoreOptions,
store: VuexStore,
rootStoreX?: any
): StoreObj {
const { mutations = {}, actions = {}, getters = {} } = options;
const state = typeof options.state === 'function' ? options.state() : options.state;
const storeX: StoreObj = createStoreX({
id: moduleName,
state: state,
actions: {
// 给mutations的key增加一个前缀避免和actions的key冲突
...Object.fromEntries(
Object.entries(mutations).map(([key, mutation]) => {
return [`${MUTATION_PREFIX}${key}`, mutation];
})
),
// 重新定义action的方法绑定this修改第一参数
...Object.fromEntries(
Object.entries(actions).map(([key, action]) => [
key,
function (this: StoreObj, state: Record<string, unknown>, payload) {
rootStoreX = rootStoreX || storeX;
const argFirst = {
...store,
// 覆盖commit方法多传一个参数moduleName
commit: (
type: string | (Record<string, unknown> & { type: string }),
payload?: any,
options?: Record<string, unknown>
) => {
store.commit(type, payload, options, moduleName);
},
// 覆盖dispatch方法多传一个参数moduleName
dispatch: (
type: string | (Record<string, unknown> & { type: string }),
payload?: any,
options?: Record<string, unknown>
) => {
return store.dispatch(type, payload, options, moduleName);
},
state: storeX.$state,
rootState: store.state,
getter: store.getters,
rootGetters: moduleGettersProxy(rootStoreX),
};
return action.call(storeX, argFirst, payload);
},
])
),
},
computed: {
...Object.fromEntries(
Object.entries(getters).map(([key, getter]) => {
return [
// 给getters的key增加一个前缀避免和actions, mutations的key冲突
`${GETTER_PREFIX}${key}`,
// 重新定义getter的方法绑定this修改参数: state, getters, rootState, rootGetters
function (state: Record<string, unknown>) {
rootStoreX = rootStoreX || storeX;
return getter.call(
storeX,
storeX.$state,
store.getters,
rootStoreX.$state,
moduleGettersProxy(rootStoreX)
);
},
];
})
),
},
})();
return storeX;
}
function collectGetters(storeX: StoreObj, gettersMap: GettersMap): void {
Object.keys(storeX.$config.computed).forEach(type => {
Object.defineProperty(gettersMap, type, {
get: () => storeX.$c[type],
configurable: true,
});
});
}
function deleteGetters(storeX: StoreObj, gettersMap: GettersMap): void {
Object.keys(storeX.$config.computed).forEach(type => {
// 删除Object.defineProperty定义的属性
Object.defineProperty(gettersMap, type, {
value: undefined,
writable: true,
enumerable: true,
configurable: true,
});
delete gettersMap[type];
});
}
function moduleGettersProxy(storeX: StoreObj) {
return new Proxy(
{},
{
get: (_, key) => {
return storeX[`${GETTER_PREFIX}${key as string}`];
},
}
);
}

View File

@ -0,0 +1,139 @@
/*
* 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 { describe, it, vi, expect, beforeEach } from 'vitest';
import { defineStore } from '../../src/pinia/pinia';
let id = 0;
function createStore() {
return defineStore({
id: String(id++),
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 = foo;
},
combined() {
this.toggle();
this.setFoo('bar');
},
throws() {
throw new Error('fail');
},
async rejects() {
throw 'fail';
},
},
});
}
describe('pinia state', () => {
let useStore = createStore();
beforeEach(() => {
useStore = createStore();
});
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(
{
simple,
// otherwise it would fail
toggle() {},
}.simple()
).toBe('simple');
});
});

View File

@ -0,0 +1,97 @@
/*
* 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 { describe, it, expect, beforeEach } from 'vitest';
import { defineStore } from '../../src/pinia/pinia';
let id = 0;
function createStore() {
return defineStore({
id: String(id++),
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;
return state.name.toUpperCase();
},
},
actions: {
o() {
this.arrowUpper.toUpperCase();
this.o().toUpperCase();
return 'a string';
},
},
});
}
describe('pinia getters', () => {
let useStore = createStore();
let useB;
let useA;
beforeEach(() => {
useStore = createStore();
useB = defineStore({
id: 'B',
state: () => ({ b: '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');
});
it('keeps getters reactive when hydrating', () => {
const store = useStore();
store.name = 'Jack';
expect(store.name).toBe('Jack');
expect(store.upperCaseName).toBe('JACK');
store.name = 'Ed';
expect(store.upperCaseName).toBe('ED');
});
});

View File

@ -0,0 +1,138 @@
/*
* 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 { beforeEach, describe, it, vi, expect } from 'vitest';
import { createPinia, defineStore } from '../../src/pinia/pinia';
import { vueReactive } from 'openinula';
const { watch, computed, ref, reactive } = vueReactive;
let id = 0;
function createStore() {
return defineStore(String(id++), {
state: () => ({
name: 'Eduardo',
counter: 0,
nested: { n: 0 },
}),
actions: {
increment(state, amount) {
this.counter += amount;
},
},
getters: {
upperCased() {
return this.name.toUpperCase();
},
},
});
}
describe('pinia state', () => {
let useStore = createStore();
beforeEach(() => {
useStore = createStore();
});
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('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 set on store', () => {
const pinia = createPinia();
const store = useStore(pinia);
store.name = 'a';
expect(store.name).toBe('a');
expect(store.$state.name).toBe('a');
});
it('can be set on store.$state', () => {
const pinia = createPinia();
const store = useStore(pinia);
store.$state.name = 'a';
expect(store.name).toBe('a');
expect(store.$state.name).toBe('a');
});
it('can be nested set on store', () => {
const pinia = createPinia();
const store = useStore(pinia);
store.nested.n = 3;
expect(store.nested.n).toBe(3);
expect(store.$state.nested.n).toBe(3);
});
it('can be nested set on store.$state', () => {
const pinia = createPinia();
const store = useStore(pinia);
store.$state.nested.n = 3;
expect(store.nested.n).toBe(3);
expect(store.$state.nested.n).toBe(3);
});
it('state can be watched', async () => {
const store = useStore();
const spy = vi.fn();
watch(() => store.name, spy);
expect(spy).not.toHaveBeenCalled();
store.name = 'Ed';
expect(spy).toHaveBeenCalledTimes(1);
});
it('state can be watched when a ref is given', async () => {
const store = useStore();
const spy = vi.fn();
watch(() => store.name, spy);
expect(spy).not.toHaveBeenCalled();
const nameRef = ref('Ed');
// @ts-expect-error
store.$state.name = nameRef;
expect(spy).toHaveBeenCalledTimes(1);
});
it('can be given a ref', () => {
const pinia = createPinia();
const store = useStore(pinia);
// @ts-expect-error
store.$state.name = ref('Ed');
expect(store.name).toBe('Ed');
expect(store.$state.name).toBe('Ed');
store.name = 'Other';
expect(store.name).toBe('Other');
expect(store.$state.name).toBe('Other');
});
});

View File

@ -0,0 +1,120 @@
/*
* 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 { beforeEach, describe, it, vi, expect } from 'vitest';
import { defineStore } from '../../src/pinia/pinia';
import { vueReactive } from 'openinula';
const { watch } = vueReactive;
let id = 0;
function createStore() {
return defineStore({
id: String(id++),
state: () => ({
a: true,
nested: {
foo: 'foo',
a: { b: 'string' },
},
}),
});
}
describe('pinia state', () => {
let useStore = createStore();
beforeEach(() => {
useStore = createStore();
});
it('reuses a store', () => {
const useStore = defineStore({ id: String(id++) });
expect(useStore()).toBe(useStore());
});
it('works with id as first argument', () => {
const useStore = defineStore(String(id++), {
state: () => ({
a: true,
nested: {
foo: 'foo',
a: { b: 'string' },
},
}),
});
expect(useStore()).toBe(useStore());
const useStoreEmpty = defineStore(String(id++), {});
expect(useStoreEmpty()).toBe(useStoreEmpty());
});
it('sets the initial state', () => {
const store = useStore();
expect(store.$state).toEqual({
a: true,
nested: {
foo: 'foo',
a: { b: 'string' },
},
});
});
it.skip('can replace its state', () => {
const store = useStore();
const spy = vi.fn();
watch(() => store.a, spy);
expect(store.a).toBe(true);
expect(spy).toHaveBeenCalledTimes(0);
store.$state = {
a: false,
nested: {
foo: 'bar',
a: {
b: 'hey',
},
},
};
expect(spy).toHaveBeenCalledTimes(1);
expect(store.$state).toEqual({
a: false,
nested: {
foo: 'bar',
a: { b: 'hey' },
},
});
});
it('can be $unsubscribe', () => {
const useStore = defineStore({
id: 'main',
state: () => ({ n: 0 }),
});
const store = useStore();
const spy = vi.fn();
store.$subscribe(spy);
store.$state.n++;
expect(spy).toHaveBeenCalledTimes(1);
expect(useStore()).toBe(store);
store.$unsubscribe(spy);
store.$state.n++;
expect(spy).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,152 @@
/*
* 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 { beforeEach, describe, it, vi, expect } from 'vitest';
import { defineStore } from '../../src/pinia/pinia';
import { vueReactive } from 'openinula';
const { ref, watch, computed } = vueReactive;
function expectType<T>(_value: T): void {}
describe('store with setup syntax', () => {
function mainFn() {
const name = ref('Eduardo');
const counter = ref(0);
function increment(amount = 1) {
counter.value += amount;
}
const double = computed(() => counter.value * 2);
return { name, counter, increment, double };
}
let id = 0;
function createStore() {
return defineStore(String(id++), mainFn);
}
let useStore = createStore();
beforeEach(() => {
useStore = createStore();
});
it('should extract the $state', () => {
const store = useStore();
expectType<{ name: string; counter: number }>(store.$state);
expect(store.$state).toEqual({ name: 'Eduardo', counter: 0 });
expect(store.name).toBe('Eduardo');
expect(store.counter).toBe(0);
expect(store.double).toBe(0);
store.increment();
expect(store.counter).toBe(1);
expect(store.double).toBe(2);
expect(store.$state).toEqual({ name: 'Eduardo', counter: 1 });
expect(store.$state).not.toHaveProperty('double');
expect(store.$state).not.toHaveProperty('increment');
});
it('can store a function', () => {
const store = defineStore(String(id++), () => {
const fn = ref(() => {});
function action() {}
return { fn, action };
})();
expectType<{ fn: () => void }>(store.$state);
expect(store.$state).toEqual({ fn: expect.any(Function) });
expect(store.fn).toEqual(expect.any(Function));
store.action();
});
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('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('state can be watched', async () => {
const store = useStore();
const spy = vi.fn();
watch(() => store.name, spy);
expect(spy).not.toHaveBeenCalled();
store.name = 'Ed';
expect(spy).toHaveBeenCalledTimes(1);
});
it('state refs can be watched', async () => {
const store = useStore();
const spy = vi.fn();
watch(() => store.name, spy);
expect(spy).not.toHaveBeenCalled();
const nameRef = ref('Ed');
store.name = nameRef;
expect(spy).toHaveBeenCalledTimes(1);
});
it('unwraps refs', () => {
const name = ref('Eduardo');
const counter = ref(0);
const double = computed(() => {
return counter.value * 2;
});
// const pinia = createPinia();
// setActivePinia(pinia);
const useStore = defineStore({
id: String(id++),
state: () => ({
name,
counter,
double,
}),
});
const store = useStore();
expect(store.name).toBe('Eduardo');
expect(store.$state.name).toBe('Eduardo');
expect(store.$state).toEqual({
name: 'Eduardo',
counter: 0,
double: 0,
});
name.value = 'Ed';
expect(store.name).toBe('Ed');
expect(store.$state.name).toBe('Ed');
store.$state.name = 'Edu';
expect(store.name).toBe('Edu');
// store.$patch({ counter: 2 });
store.counter = 2;
expect(store.counter).toBe(2);
expect(counter.value).toBe(2);
});
});

View File

@ -0,0 +1,141 @@
/*
* 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 { beforeEach, describe, it, vi, expect } from 'vitest';
import { defineStore, storeToRefs } from '../../src/pinia/pinia';
import { vueReactive } from 'openinula';
const { ref, computed, reactive } = vueReactive;
let id = 0;
describe('storeToRefs', () => {
beforeEach(() => {});
function objectOfRefs<O extends Record<any, any>>(o: O) {
return Object.keys(o).reduce((newO, key) => {
// @ts-expect-error: we only need to match
newO[key] = expect.objectContaining({ value: o[key] });
return newO;
}, {});
}
it('empty state', () => {
expect(storeToRefs(defineStore(String(id++), {})())).toEqual({});
expect(storeToRefs(defineStore({ id: String(id++) })())).toEqual({});
});
it('plain values', () => {
const store = defineStore(String(id++), {
state: () => ({ a: null as null | undefined, b: false, c: 1, d: 'd' }),
})();
const { a, b, c, d } = storeToRefs(store);
expect(a.value).toBe(null);
expect(b.value).toBe(false);
expect(c.value).toBe(1);
expect(d.value).toBe('d');
a.value = undefined;
expect(a.value).toBe(undefined);
b.value = true;
expect(b.value).toBe(true);
c.value = 2;
expect(c.value).toBe(2);
d.value = 'e';
expect(d.value).toBe('e');
});
it('setup store', () => {
const store = defineStore(String(id++), () => {
return {
a: ref<null | undefined>(null),
b: ref(false),
c: ref(1),
d: ref('d'),
r: reactive({ n: 1 }),
};
})();
const { a, b, c, d, r } = storeToRefs(store);
expect(a.value).toBe(null);
expect(b.value).toBe(false);
expect(c.value).toBe(1);
expect(d.value).toBe('d');
expect(r.value).toEqual({ n: 1 });
a.value = undefined;
expect(a.value).toBe(undefined);
b.value = true;
expect(b.value).toBe(true);
c.value = 2;
expect(c.value).toBe(2);
d.value = 'e';
expect(d.value).toBe('e');
r.value.n++;
expect(r.value).toEqual({ n: 2 });
expect(store.r).toEqual({ n: 2 });
store.r.n++;
expect(r.value).toEqual({ n: 3 });
expect(store.r).toEqual({ n: 3 });
});
it('empty getters', () => {
expect(
storeToRefs(
defineStore(String(id++), {
state: () => ({ n: 0 }),
})()
)
).toEqual(objectOfRefs({ n: 0 }));
expect(
storeToRefs(
defineStore(String(id++), () => {
return { n: ref(0) };
})()
)
).toEqual(objectOfRefs({ n: 0 }));
});
it('contains getters', () => {
const refs = storeToRefs(
defineStore(String(id++), {
state: () => ({ n: 1 }),
getters: {
double: state => state.n * 2,
},
})()
);
expect(refs).toEqual(objectOfRefs({ n: 1, double: 2 }));
const setupRefs = storeToRefs(
defineStore(String(id++), () => {
const n = ref(1);
const double = computed(() => n.value * 2);
return { n, double };
})()
);
expect(setupRefs).toEqual(objectOfRefs({ n: 1, double: 2 }));
});
});

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/vue/lifecycle';
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,480 @@
/*
* 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 { describe, it, expect, beforeEach, vi } from 'vitest';
import { createStore } from '../../src/vuex/vuex';
const TEST = 'TEST';
describe('Modules', () => {
it('dynamic module registration', () => {
const store = createStore({
modules: {
foo: {
state: { bar: 1 },
mutations: { inc: state => state.bar++ },
actions: { inc: ({ commit }) => commit('inc') },
getters: { fooBar: state => state.bar },
},
one: {
state: { a: 0 },
mutations: {
aaa(state, n) {
state.a += n;
},
},
},
},
});
store.registerModule('hi', {
state: { a: 1 },
mutations: { inc: state => state.a++ },
actions: { incHi: ({ commit }) => commit('inc') },
getters: { ga: state => state.a },
});
// expect(store._mutations.inc.length).toBe(2);
expect(store.state.hi.a).toBe(1);
expect(store.getters.ga).toBe(1);
// assert initial modules work as expected after dynamic registration
expect(store.state.foo.bar).toBe(1);
expect(store.getters.fooBar).toBe(1);
// test dispatching actions defined in dynamic module
store.dispatch('incHi');
expect(store.state.hi.a).toBe(2);
expect(store.getters.ga).toBe(2);
expect(store.state.foo.bar).toBe(1);
expect(store.getters.fooBar).toBe(1);
// unregister
store.unregisterModule('hi');
expect(store.state.hi).toBeUndefined();
expect(store.getters.ga).toBeUndefined();
// assert initial modules still work as expected after unregister
store.dispatch('inc');
expect(store.state.foo.bar).toBe(2);
expect(store.getters.fooBar).toBe(2);
});
it('dynamic module registration with namespace inheritance', () => {
const store = createStore({
modules: {
a: {
namespaced: true,
},
},
});
const actionSpy = vi.fn();
const mutationSpy = vi.fn();
store.registerModule('b', {
state: { value: 1 },
getters: { foo: state => state.value },
actions: { foo: actionSpy },
mutations: { foo: mutationSpy },
});
expect(store.state.b.value).toBe(1);
expect(store.getters['foo']).toBe(1);
store.dispatch('foo');
expect(actionSpy).toHaveBeenCalled();
store.commit('foo');
expect(mutationSpy).toHaveBeenCalled();
});
it('dynamic module existance test', () => {
const store = createStore({});
store.registerModule('bonjour', {});
expect(store.hasModule('bonjour')).toBe(true);
store.unregisterModule('bonjour');
expect(store.hasModule('bonjour')).toBe(false);
});
it('should keep getters when component gets destroyed', async () => {
const store = createStore({});
const spy = vi.fn();
const moduleA = {
namespaced: true,
state: () => ({ value: 1 }),
getters: {
getState(state) {
spy();
return state.value;
},
},
mutations: {
increment: state => {
state.value++;
},
},
};
store.registerModule('moduleA', moduleA);
expect(store.getters['moduleA/getState']).toBe(1);
expect(spy).toHaveBeenCalledTimes(1);
store.commit('moduleA/increment');
expect(store.getters['moduleA/getState']).toBe(2);
expect(spy).toHaveBeenCalledTimes(2);
});
it('should not fire an unrelated watcher', () => {
const spy = vi.fn();
const store = createStore({
modules: {
a: {
state: { value: 1 },
},
b: {},
},
});
store.watch(state => state.a, spy);
store.registerModule('c', {
state: { value: 2 },
});
expect(spy).not.toHaveBeenCalled();
});
it('state as function (multiple module in same store)', () => {
const store = createStore({
modules: {
one: {
state: { a: 0 },
mutations: {
[TEST](state, n) {
state.a += n;
},
},
},
two: {
state() {
return { a: 0 };
},
mutations: {
[TEST](state, n) {
state.a += n;
},
},
},
},
});
expect(store.state.one.a).toBe(0);
expect(store.state.two.a).toBe(0);
store.commit(TEST, 1);
expect(store.state.one.a).toBe(1);
expect(store.state.two.a).toBe(1);
});
it('state as function (same module in multiple stores)', () => {
const storeA = createStore({
modules: {
foo: {
state() {
return { a: 0 };
},
mutations: {
[TEST](state, n) {
state.a += n;
},
},
},
},
});
const storeB = createStore({
modules: {
bar: {
state() {
return { a: 0 };
},
mutations: {
[TEST](state, n) {
state.a += n;
},
},
},
},
});
expect(storeA.state.foo.a).toBe(0);
expect(storeB.state.bar.a).toBe(0);
storeA.commit(TEST, 1);
expect(storeA.state.foo.a).toBe(1);
expect(storeB.state.bar.a).toBe(0);
storeB.commit(TEST, 2);
expect(storeA.state.foo.a).toBe(1);
expect(storeB.state.bar.a).toBe(2);
});
it('module: mutation', function () {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST](state, n) {
state.a += n;
},
},
modules: {
nested: {
state: { a: 2 },
mutations: {
[TEST](state, n) {
state.a += n;
},
},
},
four: {
state: { a: 6 },
mutations: {
[TEST](state, n) {
state.a += n;
},
},
},
},
});
store.commit(TEST, 1);
expect(store.state.a).toBe(2);
expect(store.state.nested.a).toBe(3);
expect(store.state.four.a).toBe(7);
});
it('module: action', function () {
let calls = 0;
const store = createStore({
state: {
a: 1,
},
actions: {
[TEST]({ state, rootState }) {
calls++;
expect(state.a).toBe(1);
expect(rootState).toBe(store.state);
},
},
modules: {
nested: {
state: { a: 2 },
actions: {
[TEST]({ state, rootState }) {
calls++;
expect(state.a).toBe(2);
expect(rootState).toBe(store.state);
},
},
},
four: {
state: { a: 6 },
actions: {
[TEST]({ state, rootState }) {
calls++;
expect(state.a).toBe(6);
expect(rootState).toBe(store.state);
},
},
},
},
});
store.dispatch(TEST);
expect(calls).toBe(3);
});
it('module: getters', function () {
const store = createStore({
state: {
a: 1,
},
getters: {
constant: () => 0,
[`getter1`]: (state, getters, rootState) => {
expect(getters.constant).toBe(0);
expect(rootState.a).toBe(store.state.a);
return state.a;
},
},
modules: {
nested: {
state: { a: 2 },
getters: {
[`getter2`]: (state, getters, rootState) => {
expect(getters.constant).toBe(0);
expect(rootState.a).toBe(store.state.a);
return state.a;
},
},
},
four: {
state: { a: 6 },
getters: {
[`getter6`]: (state, getters, rootState) => {
expect(getters.constant).toBe(0);
expect(rootState.a).toBe(store.state.a);
return state.a;
},
},
},
},
});
[1, 2, 6].forEach(n => {
expect(store.getters[`getter${n}`]).toBe(n);
});
});
it('module: namespace', () => {
const actionSpy = vi.fn();
const mutationSpy = vi.fn();
const store = createStore({
modules: {
a: {
namespaced: true,
state: {
a: 1,
},
getters: {
b: () => 2,
},
actions: {
[TEST]: actionSpy,
},
mutations: {
[TEST]: mutationSpy,
},
},
},
});
expect(store.state.a.a).toBe(1);
expect(store.getters['a/b']).toBe(2);
store.dispatch('a/' + TEST);
expect(actionSpy).toHaveBeenCalled();
store.commit('a/' + TEST);
expect(mutationSpy).toHaveBeenCalled();
});
it('module: getters are namespaced in namespaced module', () => {
const store = createStore({
state: { value: 'root' },
getters: {
foo: state => state.value,
},
modules: {
a: {
namespaced: true,
state: { value: 'module' },
getters: {
foo: state => {
return state.value;
},
bar: (state, getters) => {
return getters.foo;
},
baz: (state, getters, rootState, rootGetters) => rootGetters.foo,
},
},
},
});
expect(store.getters['a/foo']).toBe('module');
expect(store.getters['a/bar']).toBe('module');
expect(store.getters['a/baz']).toBe('root');
});
it('module: action context is namespaced in namespaced module', () => {
const rootActionSpy = vi.fn();
const rootMutationSpy = vi.fn();
const moduleActionSpy = vi.fn();
const moduleMutationSpy = vi.fn();
const store = createStore({
state: { value: 'root' },
getters: { foo: state => state.value },
actions: { foo: rootActionSpy },
mutations: { foo: rootMutationSpy },
modules: {
a: {
namespaced: true,
state: { value: 'module' },
getters: { foo: state => state.value },
actions: {
foo: moduleActionSpy,
test({ dispatch, commit, getters, rootGetters }) {
expect(getters.foo).toBe('module');
expect(rootGetters.foo).toBe('root');
dispatch('foo');
expect(moduleActionSpy).toHaveBeenCalledTimes(1);
dispatch('foo', null, { root: true });
expect(rootActionSpy).toHaveBeenCalledTimes(1);
commit('foo');
expect(moduleMutationSpy).toHaveBeenCalledTimes(1);
commit('foo', null, { root: true });
expect(rootMutationSpy).toHaveBeenCalledTimes(1);
},
},
mutations: { foo: moduleMutationSpy },
},
},
});
store.dispatch('a/test');
});
it('dispatching multiple actions in different modules', () => {
const store = createStore({
modules: {
a: {
actions: {
[TEST]() {
return 1;
},
},
},
b: {
actions: {
[TEST]() {
return new Promise(r => r(2));
},
},
},
},
});
store.dispatch(TEST).then(res => {
expect(res[0]).toBe(1);
expect(res[1]).toBe(2);
});
});
});

View File

@ -0,0 +1,335 @@
/*
* 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 { describe, it, expect, vi } from 'vitest';
import { createStore } from '../../src/vuex/vuex';
const TEST_M = 'TEST_M';
const TEST_A = 'TEST_A';
describe('vuex Store', () => {
it('committing mutations', () => {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
});
store.commit(TEST_M, 2);
expect(store.state.a).toBe(3);
});
it('committing with object style', () => {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state, payload) {
state.a += payload.amount;
},
},
});
store.commit({
type: TEST_M,
amount: 2,
});
expect(store.state.a).toBe(3);
});
it('dispatching actions, sync', () => {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
actions: {
[TEST_A]({ commit }, n) {
commit(TEST_M, n);
},
},
});
store.dispatch(TEST_A, 2);
expect(store.state.a).toBe(3);
});
it('dispatching with object style', () => {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
actions: {
[TEST_A]({ commit }, payload) {
commit(TEST_M, payload.amount);
},
},
});
store.dispatch({
type: TEST_A,
amount: 2,
});
expect(store.state.a).toBe(3);
});
it('dispatching actions, with returned Promise', () => {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
actions: {
[TEST_A]({ commit }, n) {
return new Promise(resolve => {
setTimeout(() => {
commit(TEST_M, n);
resolve('');
}, 0);
});
},
},
});
expect(store.state.a).toBe(1);
store.dispatch(TEST_A, 2).then(() => {
expect(store.state.a).toBe(3);
});
});
it('composing actions with async/await', () => {
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
actions: {
[TEST_A]({ commit }, n) {
return new Promise(resolve => {
setTimeout(() => {
commit(TEST_M, n);
resolve('');
}, 0);
});
},
two: async ({ commit, dispatch }, n) => {
await dispatch(TEST_A, 1);
expect(store.state.a).toBe(2);
commit(TEST_M, n);
},
},
});
expect(store.state.a).toBe(1);
store.dispatch('two', 3).then(() => {
expect(store.state.a).toBe(5);
});
});
it('detecting action Promise errors', () => {
const store = createStore({
actions: {
[TEST_A]() {
return new Promise((resolve, reject) => {
reject('no');
});
},
},
});
const thenSpy = vi.fn();
store
.dispatch(TEST_A)
.then(thenSpy)
.catch((err: string) => {
expect(thenSpy).not.toHaveBeenCalled();
expect(err).toBe('no');
});
});
it('getters', () => {
const store = createStore({
state: {
a: 0,
},
getters: {
state: state => (state.a > 0 ? 'hasAny' : 'none'),
},
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
actions: {
check({ getters }, value) {
// check for exposing getters into actions
expect(getters.state).toBe(value);
},
},
});
expect(store.getters.state).toBe('none');
store.dispatch('check', 'none');
store.commit(TEST_M, 1);
expect(store.getters.state).toBe('hasAny');
store.dispatch('check', 'hasAny');
});
it('should accept state as function', () => {
const store = createStore({
state: () => ({
a: 1,
}),
mutations: {
[TEST_M](state, n) {
state.a += n;
},
},
});
expect(store.state.a).toBe(1);
store.commit(TEST_M, 2);
expect(store.state.a).toBe(3);
});
it('subscribe: should handle subscriptions / unsubscriptions', () => {
const subscribeSpy = vi.fn();
const secondSubscribeSpy = vi.fn();
const testPayload = 2;
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state) {
state.a++;
},
},
});
const unsubscribe = store.subscribe(subscribeSpy);
store.subscribe(secondSubscribeSpy);
store.commit(TEST_M, testPayload);
unsubscribe();
store.commit(TEST_M, testPayload);
expect(subscribeSpy).toHaveBeenCalledTimes(1);
expect(secondSubscribeSpy).toHaveBeenCalledTimes(2);
});
it('subscribe: should handle subscriptions with synchronous unsubscriptions', () => {
const subscribeSpy = vi.fn();
const testPayload = 2;
const store = createStore({
state: {
a: 1,
},
mutations: {
[TEST_M](state) {
state.a++;
},
},
});
const unsubscribe = store.subscribe(() => unsubscribe());
store.subscribe(subscribeSpy);
store.commit(TEST_M, testPayload);
expect(subscribeSpy).toHaveBeenCalledTimes(1);
});
it('subscribeAction: should handle subscriptions with synchronous unsubscriptions', () => {
const subscribeSpy = vi.fn();
const testPayload = 2;
const store = createStore({
state: {
a: 1,
},
actions: {
[TEST_A]({ state }) {
state.a++;
},
},
});
const unsubscribe = store.subscribeAction(() => unsubscribe());
store.subscribeAction(subscribeSpy);
store.dispatch(TEST_A, testPayload);
expect(subscribeSpy).toHaveBeenCalledTimes(1);
});
it('watch: with resetting vm', () => {
const store = createStore({
state: {
count: 0,
},
mutations: {
[TEST_M]: state => state.count++,
},
});
const spy = vi.fn();
store.watch(state => state.count, spy);
store.commit(TEST_M);
expect(store.state.count).toBe(1);
expect(spy).toHaveBeenCalledTimes(1);
});
it("watch: getter function has access to store's getters object", () => {
const store = createStore({
state: {
count: 0,
},
mutations: {
[TEST_M]: state => state.count++,
},
getters: {
getCount: state => state.count,
},
});
const getter = function getter(state: any) {
return state.count;
};
const spy = vi.spyOn({ getter }, 'getter');
const spyCb = vi.fn();
store.watch(spy as any, spyCb);
store.commit(TEST_M);
expect(store.state.count).toBe(1);
expect(spy).toHaveBeenCalledWith(store.state, store.getters);
});
});

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,10 @@
{
"files": [
"src/pinia/index.ts"
],
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": true,
"declarationDir": "./build/pinia/@types"
},
}

View File

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

View File

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

View File

@ -0,0 +1,32 @@
/*
* 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 react from '@vitejs/plugin-react';
const 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,138 @@
/*
* 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, isRef, reactive, unref, toRef, toRefs, isReadonly, watchEffect } = vueReactive;
describe('test toRef, toRefs', () => {
it('toRef', () => {
const a = reactive({
x: 1,
});
const x = toRef(a, 'x');
expect(isRef(x)).toBe(true);
expect(x.value).toBe(1);
// source -> proxy
a.x = 2;
expect(x.value).toBe(2);
// proxy -> source
x.value = 3;
expect(a.x).toBe(3);
// reactivity
let dummyX;
watchEffect(() => {
dummyX = x.value;
});
expect(dummyX).toBe(x.value);
// mutating source should trigger effect using the proxy refs
a.x = 4;
expect(dummyX).toBe(4);
// should keep ref
const r = { x: ref(1) };
expect(toRef(r, 'x')).toBe(r.x);
});
it('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');
});
it('toRef default value', () => {
const a: { x: number | undefined } = { x: undefined };
const x = toRef(a, 'x', 1);
expect(x.value).toBe(1);
a.x = 2;
expect(x.value).toBe(2);
a.x = undefined;
expect(x.value).toBe(1);
});
it('toRef getter', () => {
const x = toRef(() => 1);
expect(x.value).toBe(1);
expect(isRef(x)).toBe(true);
expect(unref(x)).toBe(1);
//@ts-expect-error
expect(() => (x.value = 123)).toThrow();
expect(isReadonly(x)).toBe(true);
});
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);
// source -> proxy
a.x = 2;
a.y = 3;
expect(x.value).toBe(2);
expect(y.value).toBe(3);
// proxy -> source
x.value = 3;
y.value = 4;
expect(a.x).toBe(3);
expect(a.y).toBe(4);
// reactivity
let dummyX, dummyY;
watchEffect(() => {
dummyX = x.value;
dummyY = y.value;
});
expect(dummyX).toBe(x.value);
expect(dummyY).toBe(y.value);
// mutating source should trigger effect using the proxy refs
a.x = 4;
a.y = 5;
expect(dummyX).toBe(4);
expect(dummyY).toBe(5);
});
it('toRefs reactive array', () => {
const arr = reactive(['a', 'b', 'c']);
const refs = toRefs(arr);
expect(Array.isArray(refs)).toBe(true);
refs[0].value = '1';
expect(arr[0]).toBe('1');
arr[1] = '2';
expect(refs[1].value).toBe('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

@ -42,7 +42,7 @@ describe('Dollar store access', () => {
function App() {
const logStore = useLogStore();
return <div id={RESULT_ID}>{logStore.$c.length()}</div>;
return <div id={RESULT_ID}>{logStore.$c.length}</div>;
}
Inula.render(<App />, container);
@ -64,7 +64,7 @@ describe('Dollar store access', () => {
>
add
</button>
<p id={RESULT_ID}>{logStore.$c.length()}</p>
<p id={RESULT_ID}>{logStore.$c.length}</p>
</div>
);
}

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

@ -8,7 +8,7 @@
"homepage": "",
"bugs": "",
"license": "MulanPSL2",
"main": "./build/index.js",
"main": "./src/index.ts",
"repository": {},
"engines": {
"node": ">=0.10.0"

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, toRef, toRefs } 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,28 @@ import {
} from './dom/DOMExternal';
import { syncUpdates as flushSync } from './renderer/TreeBuilder';
import { toRaw } from './inulax/proxy/ProxyHandler';
import { isReactive, isShallow, isReadonly } from './inulax/CommonUtils';
const vueReactive = {
ref,
useReference,
isRef,
unref,
shallowRef,
toRef,
toRefs,
reactive,
useReactive,
isReactive,
isShallow,
isReadonly,
computed,
useComputed,
watchEffect,
watch,
useWatch,
toRaw,
};
const Inula = {
Children,
@ -122,9 +146,11 @@ const Inula = {
Profiler,
StrictMode,
Suspense,
// vue reactive api
vueReactive,
};
export const version = __VERSION__;
export const version = '';
export {
Children,
createRef,
@ -161,7 +187,7 @@ export {
clearStore,
reduxAdapter,
watch,
toRaw,
// 兼容ReactIs
isFragment,
isElement,
@ -178,7 +204,15 @@ export {
Profiler,
StrictMode,
Suspense,
// vue reactive api
vueReactive,
};
export * from './types';
export * from './inulax/types/ReactiveTypes';
export * from './inulax/types/ProxyTypes';
export * from './inulax/types/StoreTypes';
export * from './inulax/types/StoreTypes';
export { ComputedImpl } from './inulax/reactive/Computed';
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,14 @@ 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]);
}
export function isReadonly(value: unknown): boolean {
return !!(value && value[ReactiveFlags.IS_READONLY]);
}

View File

@ -15,4 +15,23 @@
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',
IS_READONLY = '_isReadonly',
IS_REF = '_isRef',
}

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,235 @@
### 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, // 不支持
});
```
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,7 +71,7 @@ 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
const vNodeArray = Array.from(vNodes || []);
@ -92,8 +86,28 @@ export class Observer implements IObserver {
this.triggerUpdate(vNode);
});
// 这里需要过滤调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,
});
function setObserver(rawObj: any, observer: IObserver): void {
rawObserverMap.set(rawObj, observer);
}
: (rawObj, observer) => {
rawObj[OBSERVER_KEY] = 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();
setObserverKey(rawObj, observer);
observer = (isDeepProxy ? new Observer() : new HooklessObserver()) as IObserver;
setObserver(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,
{
proxyObj = createCollectionProxy(rawObj, {
current: change => {
listener.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);
});
function get(rawObj: T, key: KeyType, receiver: any) {
return baseGetFun(rawObj, key, receiver, listener, listeners);
}
// 触发属性变化
observer.setProp(key, mutation);
}
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,175 @@
/*
* 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;
}
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;
}
} else {
keyProxy = getValOrProxy('keyChange', false, key, rawObj, listener, listeners);
keyProxies.set(key, keyProxy);
}
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);
const oldValues = [...Array.from(rawObj.entries())];
rawObj.set(keyProxy, value);
const observer = getObserver(rawObj);
const mutation = resolveMutation(
{
_type: 'Map',
entries: oldData,
entries: oldValues,
},
{
_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())];
}
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);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
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 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 baseHasFun(rawObj, key, keyProxies);
}
return false;
function clear(rawObj: T) {
baseClearFun(rawObj, keyProxies, 'Map');
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
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 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 deleteFun(rawObj: T, key: any) {
return baseDeleteFun(rawObj, key, 'Map', keyProxies);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function wrapIterator(rawObj: Record<string, any>, rawIt: { next: () => { value: any; done: boolean } }, type) {
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);
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;
function get(rawObj: T, key: KeyType, receiver: any): any {
return baseGetFun(rawObj, key, receiver, listener, listeners, singleLevel);
}
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;
}
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);
function add(rawObj: T, value: any): Record<string, any> {
return baseAddFunOfSet(rawObj, value, listener, listeners, 'Set', valProxies);
}
return rawObj;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
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));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
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 baseHasFun(rawObj, value, valProxies);
}
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);
function get(rawObj: T, key: any, receiver: any): any {
return baseGetFun(rawObj, key, receiver, listeners, handler, 'Set');
}
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') {
function wrapIterator(rawObj: T, rawIt: IterableIterator<any>) {
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 wrapIterator(rawObj: Record<string, any>, rawIt: { next: () => { value: any; done: boolean } }) {
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);
function get(rawObj: T, key: any, receiver: any): any {
return baseGetFun(rawObj, key, receiver, listeners, handler, 'WeakMap', getFun);
}
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);
}
// 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: { 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);
return true;
function deleteFun(rawObj: T, key: any) {
return baseDeleteFun(rawObj, key, 'WeakMap');
}
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 });
registerListener(rawObj, listener, listeners);
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);
};
function get(rawObj: T, key: any, receiver: any): any {
return baseGetFun(rawObj, key, receiver, listeners, handler, 'WeakSet');
}
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);
};
};
function add(rawObj: T, value: any): Record<string, any> {
return baseAddFunOfSet(rawObj, value, listener, listeners, 'WeakSet', proxies);
}
if (key === RAW_VALUE) {
return rawObj;
}
return Reflect.get(rawObj, key, receiver);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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 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));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
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 baseHasFun(rawObj, value, proxies);
}
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,82 @@
/*
* 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;
readonly _isReadonly = 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,188 @@
/*
* 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 { isArray, isObject, isSame, isShallow } from '../CommonUtils';
import { toRaw } from './Reactive';
import { Observer } from '../proxy/Observer';
import { KeyTypes, OBSERVER_KEY, ReactiveFlags } from '../Constants';
import { IfAny, MaybeRef, RefType, ToRef, ToRefs, UnwrapRef } from '../types/ReactiveTypes';
import { registerDestroyFunction } from '../store/StoreHandler';
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[ReactiveFlags.IS_REF]);
}
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 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);
}
export function toRef<T>(
value: T
): T extends () => infer R ? Readonly<RefType<R>> : T extends RefType ? T : RefType<UnwrapRef<T>>;
export function toRef<T extends Record<string, any>, K extends keyof T>(object: T, key: K): ToRef<T[K]>;
export function toRef<T extends Record<string, any>, K extends keyof T>(
object: T,
key: K,
defaultValue: T[K]
): ToRef<Exclude<T[K], undefined>>;
export function toRef(source: Record<string, any> | MaybeRef, key?: string, defaultValue?: unknown): RefType {
if (isRef(source)) {
return source;
} else if (typeof source === 'function') {
return new GetterRefImpl(source) as any;
} else if (isObject(source) && arguments.length > 1) {
return propertyToRef(source, key!, defaultValue);
} else {
return ref(source);
}
}
class GetterRefImpl<T> {
public readonly _isRef = true;
public readonly _isReadonly = true;
private readonly _getter: () => T;
constructor(getter: () => T) {
this._getter = getter;
}
get value() {
return this._getter();
}
}
function propertyToRef(source: Record<string, any>, key: string, defaultValue?: unknown) {
const val = source[key];
return isRef(val) ? val : (new ObjectRefImpl(source, key, defaultValue) as any);
}
class ObjectRefImpl<T extends Record<string, any>, K extends keyof T> {
public readonly _isRef = true;
private readonly _object: T;
private readonly _key: K;
private readonly _defaultValue?: T[K];
constructor(object: T, key: K, defaultValue?: T[K]) {
this._object = object;
this._key = key;
this._defaultValue = defaultValue;
}
get value() {
const val = this._object[this._key];
return val === undefined ? this._defaultValue! : val;
}
set value(newVal) {
this._object[this._key] = newVal;
}
}
export function toRefs<T extends Record<string, any>>(object: T): ToRefs<T> {
const ret: any = isArray(object) ? new Array(object.length) : {};
for (const key in object) {
ret[key] = propertyToRef(object, key);
}
return ret;
}

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

@ -0,0 +1,18 @@
/*
* 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 './reactive/Reactive';
export * from './reactive/Reactive';
export * from './reactive/Reactive';

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,
@ -52,14 +53,15 @@ const idGenerator = {
};
const storeMap = new Map<string, StoreObj<any, any, any>>();
const pendingMap = new WeakMap<any, boolean | number>();
// 通过该方法执行store.$queue中的action
function tryNextAction(storeObj, proxyObj, config, plannedActions) {
if (!plannedActions.length) {
if (proxyObj.$pending) {
if (pendingMap.get(proxyObj)) {
const timestamp = Date.now();
const duration = timestamp - proxyObj.$pending;
proxyObj.$pending = false;
const duration = timestamp - (pendingMap.get(proxyObj) as number);
pendingMap.set(proxyObj, false);
devtools.emit(QUEUE_FINISHED, {
store: storeObj,
endedAt: timestamp,
@ -99,7 +101,7 @@ export function clearVNodeObservers(vNode: VNode) {
}
// 注册VNode销毁时的清理动作
function registerDestroyFunction() {
export function registerDestroyFunction() {
const processingVNode = getProcessingVNode();
// 获取不到当前运行的VNode说明不在组件中运行属于非法场景
@ -157,25 +159,28 @@ 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) {
pendingMap.set(proxyObj, false);
}
const $a: Partial<StoreActions<S, A>> = {};
const $queue: Partial<StoreActions<S, A>> = {};
const $c: Partial<ComputedValues<S, C>> = {};
const storeObj = {
id,
$state: proxyObj,
$s: proxyObj,
$a: $a as StoreActions<S, A>,
$c: $c as ComputedValues<S, C>,
$queue: $queue as QueuedStoreActions<S, A>,
$config: config,
$listeners: [
$subscriptions: [
change => {
devtools.emit(STATE_CHANGE, {
store: storeObj,
@ -185,16 +190,19 @@ export function createStore<S extends Record<string, any>, A extends UserActions
],
$subscribe: listener => {
devtools.emit(SUBSCRIBED, { store: storeObj, listener });
storeObj.$listeners.push(listener);
storeObj.$subscriptions.push(listener);
return () => {
storeObj.$unsubscribe(listener);
};
},
$unsubscribe: listener => {
devtools.emit(UNSUBSCRIBED, { store: storeObj });
storeObj.$listeners = storeObj.$listeners.filter(item => item != listener);
storeObj.$subscriptions = storeObj.$subscriptions.filter(item => item != listener);
},
} as unknown as StoreObj<S, A, C>;
listener.current = (...args) => {
storeObj.$listeners.forEach(listener => listener(...args));
storeObj.$subscriptions.forEach(listener => listener(...args));
};
const plannedActions: PlannedAction<S, ActionFunction<S>>[] = [];
@ -214,11 +222,11 @@ export function createStore<S extends Record<string, any>, A extends UserActions
fromQueue: true,
});
return new Promise(resolve => {
if (!proxyObj.$pending) {
proxyObj.$pending = Date.now();
if (!pendingMap.get(proxyObj)) {
pendingMap.set(proxyObj, Date.now());
devtools.emit(QUEUE_PENDING, {
store: storeObj,
startedAt: proxyObj.$pending,
startedAt: pendingMap.get(proxyObj),
});
const result = config.actions![action].bind(storeObj, proxyObj)(...payload);
@ -276,12 +284,15 @@ export function createStore<S extends Record<string, any>, A extends UserActions
if (config.computed) {
Object.keys(config.computed).forEach(computeKey => {
// 让store.$c[computeKey]可以访问到computed方法
($c as any)[computeKey] = config.computed![computeKey].bind(storeObj, readonlyProxy(proxyObj));
const computeFn = config.computed![computeKey].bind(storeObj, readonlyProxy(proxyObj));
// 让store.$c[computeKey]可以访问到computed的值
Object.defineProperty($c, computeKey, {
get: computeFn as () => any,
});
// 让store[computeKey]可以访问到computed的值
Object.defineProperty(storeObj, computeKey, {
get: $c[computeKey] as () => any,
get: computeFn as () => any,
});
});
}

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,53 @@
/*
* 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;
export type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
export type ToRef<T> = IfAny<T, RefType<T>, [T] extends [RefType] ? T : RefType<T>>;
export type ToRefs<T = any> = {
[K in keyof T]: ToRef<T[K]>;
};

View File

@ -13,25 +13,13 @@
* See the Mulan PSL v2 for more details.
*/
export interface IObserver {
useProp: (key: string | symbol) => void;
import { AddWatchProp, Listener } 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>;
@ -56,15 +48,26 @@ type Action<T extends ActionFunction<any>, S extends Record<string, unknown>> =
...args: RemoveFirstFromTuple<Parameters<T>>
) => ReturnType<T>;
export type StoreObj<S extends Record<string, unknown>, A extends UserActions<S>, C extends UserComputedValues<S>> = {
$s: S;
export type StoreComputed<S extends Record<string, unknown>, C extends UserComputedValues<S>> = {
[K in keyof C]: ReturnType<C[K]>;
};
export type StoreObj<
S extends Record<string, unknown> = Record<string, unknown>,
A extends UserActions<S> = UserActions<S>,
C extends UserComputedValues<S> = UserComputedValues<S>,
> = {
$s: AddWatchProp<S>;
$state: AddWatchProp<S>;
$a: StoreActions<S, A>;
$c: UserComputedValues<S>;
$c: StoreComputed<S, C>;
$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]> };
$subscriptions: Array<Listener>;
$subscribe: (listener: Listener) => void;
$unsubscribe: (listener: Listener) => void;
} & { [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;
@ -79,11 +82,9 @@ type RemoveFirstFromTuple<T extends any[]> = T['length'] extends 0
: [];
export type UserComputedValues<S extends Record<string, unknown>> = {
[K: string]: ComputedFunction<S>;
[K: string]: (state: S) => any;
};
type ComputedFunction<S extends Record<string, unknown>> = (state: S) => any;
export type AsyncAction<T extends ActionFunction<any>, S extends Record<string, unknown>> = (
this: StoreObj<S, any, any>,
...args: RemoveFirstFromTuple<Parameters<T>>

View File

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