Compare commits
5 Commits
master
...
vue-adapte
Author | SHA1 | Date |
---|---|---|
|
96c4f0c337 | |
|
0f75e48f79 | |
|
b725b0d98c | |
|
d7101ff5e3 | |
|
05f0610c99 |
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
printWidth: 120, // 一行120字符数,如果超过会进行换行
|
||||||
|
tabWidth: 2, // tab等2个空格
|
||||||
|
useTabs: false, // 用空格缩进行
|
||||||
|
semi: true, // 行尾使用分号
|
||||||
|
singleQuote: true, // 字符串使用单引号
|
||||||
|
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
|
||||||
|
jsxSingleQuote: false, // 在JSX中使用双引号
|
||||||
|
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
|
||||||
|
bracketSpacing: true, // 对象的括号间增加空格
|
||||||
|
bracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
|
||||||
|
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
|
||||||
|
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
|
||||||
|
endOfLine: 'lf', // 仅限换行(\n)
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
## 适配Vue
|
||||||
|
|
||||||
|
### 生命周期
|
||||||
|
|
||||||
|
### pinia
|
||||||
|
|
||||||
|
### vuex
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
presets: ['@babel/preset-typescript', ['@babel/preset-env', { targets: { node: 'current' } }]],
|
||||||
|
plugins: [
|
||||||
|
'@babel/plugin-syntax-jsx',
|
||||||
|
[
|
||||||
|
'@babel/plugin-transform-react-jsx',
|
||||||
|
{
|
||||||
|
runtime: 'automatic',
|
||||||
|
importSource: 'openinula',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['@babel/plugin-proposal-class-properties', { loose: true }],
|
||||||
|
['@babel/plugin-proposal-private-methods', { loose: true }],
|
||||||
|
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
|
||||||
|
'@babel/plugin-transform-object-assign',
|
||||||
|
'@babel/plugin-transform-object-super',
|
||||||
|
['@babel/plugin-proposal-object-rest-spread', { loose: true, useBuiltIns: true }],
|
||||||
|
['@babel/plugin-transform-template-literals', { loose: true }],
|
||||||
|
'@babel/plugin-transform-arrow-functions',
|
||||||
|
'@babel/plugin-transform-literals',
|
||||||
|
'@babel/plugin-transform-for-of',
|
||||||
|
'@babel/plugin-transform-block-scoped-functions',
|
||||||
|
'@babel/plugin-transform-classes',
|
||||||
|
'@babel/plugin-transform-shorthand-properties',
|
||||||
|
'@babel/plugin-transform-computed-properties',
|
||||||
|
'@babel/plugin-transform-parameters',
|
||||||
|
['@babel/plugin-transform-spread', { loose: true, useBuiltIns: true }],
|
||||||
|
['@babel/plugin-transform-block-scoping', { throwIfClosureRequired: false }],
|
||||||
|
['@babel/plugin-transform-destructuring', { loose: true, useBuiltIns: true }],
|
||||||
|
'@babel/plugin-transform-runtime',
|
||||||
|
'@babel/plugin-proposal-nullish-coalescing-operator',
|
||||||
|
'@babel/plugin-proposal-optional-chaining',
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,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);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
## 适配Vue
|
||||||
|
|
||||||
|
### 生命周期
|
||||||
|
onBeforeMount
|
||||||
|
onMounted
|
||||||
|
onBeforeUpdate
|
||||||
|
onUpdated
|
||||||
|
onBeforeUnmount
|
||||||
|
onUnmounted
|
||||||
|
|
|
@ -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);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"module": "./esm/pinia-adapter.js",
|
||||||
|
"main": "./cjs/pinia-adapter.js",
|
||||||
|
"types": "./@types/index.d.ts"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"module": "./esm/vuex-adapter.js",
|
||||||
|
"main": "./cjs/vuex-adapter.js",
|
||||||
|
"types": "./@types/index.d.ts"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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')];
|
|
@ -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'),
|
||||||
|
];
|
|
@ -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.
|
* openInula is licensed under Mulan PSL v2.
|
||||||
* You can use this software according to the terms and conditions of the 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.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function watch(stateVariable: any, listener: (state: any) => void) {
|
export * from './pinia';
|
||||||
listener = listener.bind(null, stateVariable);
|
|
||||||
stateVariable.addListener(listener);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
stateVariable.removeListener(listener);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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增加第一个参数store,pinia不需要,所以需要去掉
|
||||||
|
* @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;
|
||||||
|
}
|
|
@ -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]>>>;
|
||||||
|
};
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './lifecycle';
|
|
@ -0,0 +1,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?.();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
}
|
|
@ -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;
|
|
@ -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';
|
|
@ -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;
|
||||||
|
};
|
|
@ -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}`];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 }));
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./build",
|
||||||
|
"incremental": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"allowJs": true, // allowJs=true => tsc compile js as module, no type check
|
||||||
|
"checkJs": false, // Disable ts error checking in js
|
||||||
|
"strict": true, // js-ts mixed setting
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noUnusedLocals": false, // 等大部分js代码改成ts之后再启用.
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"module": "CommonJS",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"target": "es5",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"allowUnreachableCode": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"declaration": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"types": ["jest"], // 赋值为空数组使@types/node不会起作用
|
||||||
|
"lib": ["dom", "esnext", "ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strictNullChecks": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"src/pinia/index.ts"
|
||||||
|
],
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"declarationDir": "./build/pinia/@types"
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"src/vue/index.ts"
|
||||||
|
],
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"declarationDir": "./build/vue/@types"
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"src/vuex/index.ts"
|
||||||
|
],
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"declarationDir": "./build/vuex/@types"
|
||||||
|
},
|
||||||
|
}
|
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,256 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { vueReactive } from '../../../src';
|
||||||
|
const { ref, reactive, watchEffect, computed } = vueReactive;
|
||||||
|
|
||||||
|
describe('test computed', () => {
|
||||||
|
it('should correctly update the computed value', () => {
|
||||||
|
const data = reactive<{ bar?: string }>({});
|
||||||
|
const computedData = computed(() => {
|
||||||
|
return data.bar;
|
||||||
|
});
|
||||||
|
expect(computedData.value).toBe(undefined);
|
||||||
|
data.bar = 'test';
|
||||||
|
expect(computedData.value).toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate the effect trigger', () => {
|
||||||
|
const data = reactive<{ key?: number }>({});
|
||||||
|
const computedData = computed(() => {
|
||||||
|
return data.key;
|
||||||
|
});
|
||||||
|
let result;
|
||||||
|
watchEffect(() => {
|
||||||
|
result = computedData.value;
|
||||||
|
});
|
||||||
|
expect(result).toBe(undefined);
|
||||||
|
data.key = 2;
|
||||||
|
expect(result).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate the computation chain', () => {
|
||||||
|
const data = reactive({ bar: 0 });
|
||||||
|
const c1 = computed(() => data.bar);
|
||||||
|
const c2 = computed(() => c1.value + 2);
|
||||||
|
expect(c2.value).toBe(2);
|
||||||
|
expect(c1.value).toBe(0);
|
||||||
|
data.bar += 2;
|
||||||
|
expect(c2.value).toBe(4);
|
||||||
|
expect(c1.value).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate the computation sequence', () => {
|
||||||
|
const data = reactive({ key: 0 });
|
||||||
|
const getter1 = jest.fn(() => data.key);
|
||||||
|
const getter2 = jest.fn(() => {
|
||||||
|
return c1.value + 3;
|
||||||
|
});
|
||||||
|
const c1 = computed(getter1);
|
||||||
|
const c2 = computed(getter2);
|
||||||
|
|
||||||
|
let result;
|
||||||
|
watchEffect(() => {
|
||||||
|
result = c2.value;
|
||||||
|
});
|
||||||
|
expect(result).toBe(3);
|
||||||
|
expect(getter1).toHaveBeenCalledTimes(1);
|
||||||
|
expect(getter2).toHaveBeenCalledTimes(1);
|
||||||
|
data.key += 2;
|
||||||
|
expect(result).toBe(5);
|
||||||
|
|
||||||
|
expect(getter1).toHaveBeenCalledTimes(2);
|
||||||
|
expect(getter2).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate the computation process', () => {
|
||||||
|
const reactiveData = reactive({ key: 0 });
|
||||||
|
const getterFunc1 = jest.fn(() => reactiveData.key);
|
||||||
|
const getterFunc2 = jest.fn(function () {
|
||||||
|
return computedValue1.value + 2;
|
||||||
|
});
|
||||||
|
const computedValue1 = computed(getterFunc1);
|
||||||
|
const computedValue2 = computed(getterFunc2);
|
||||||
|
|
||||||
|
let computedResult;
|
||||||
|
watchEffect(() => {
|
||||||
|
computedResult = computedValue1.value + computedValue2.value;
|
||||||
|
});
|
||||||
|
expect(computedResult).toBe(2);
|
||||||
|
|
||||||
|
expect(getterFunc1).toHaveBeenCalledTimes(1);
|
||||||
|
expect(getterFunc2).toHaveBeenCalledTimes(1);
|
||||||
|
reactiveData.key++;
|
||||||
|
expect(computedResult).toBe(4);
|
||||||
|
|
||||||
|
expect(getterFunc1).toHaveBeenCalledTimes(2);
|
||||||
|
expect(getterFunc2).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate the computation halt', function () {
|
||||||
|
const reactiveObj = reactive<{ key1?: number }>({});
|
||||||
|
const computedObj = computed(() => {
|
||||||
|
return reactiveObj.key1;
|
||||||
|
});
|
||||||
|
let resultValue;
|
||||||
|
watchEffect(() => {
|
||||||
|
resultValue = computedObj.value;
|
||||||
|
});
|
||||||
|
expect(resultValue).toBe(undefined);
|
||||||
|
reactiveObj.key1 = 3;
|
||||||
|
expect(resultValue).toBe(3);
|
||||||
|
computedObj.stop();
|
||||||
|
reactiveObj.key1 = 4;
|
||||||
|
expect(resultValue).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate the computation changes', () => {
|
||||||
|
const numRef = ref(0);
|
||||||
|
const increment = computed(() => numRef.value + 2);
|
||||||
|
const testFn = jest.fn(() => {
|
||||||
|
numRef.value;
|
||||||
|
increment.value;
|
||||||
|
});
|
||||||
|
watchEffect(testFn);
|
||||||
|
numRef.value += 3;
|
||||||
|
// should call testFn 3 times, 1 for init, 1 for numRef, 1 for increment
|
||||||
|
expect(testFn).toBeCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate the computation stop', () => {
|
||||||
|
const reactiveObj = reactive<{ key1?: number }>({ key1: 1 });
|
||||||
|
const computedObj = computed(() => reactiveObj.key1);
|
||||||
|
computedObj.stop();
|
||||||
|
expect(computedObj.value).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate data changes in a non-lazy manner', () => {
|
||||||
|
const spyFunction = jest.fn();
|
||||||
|
|
||||||
|
const refData = ref<null | { num: number }>({
|
||||||
|
num: 3,
|
||||||
|
});
|
||||||
|
const computedData1 = computed(() => {
|
||||||
|
return refData.value;
|
||||||
|
});
|
||||||
|
const computedData2 = computed(() => {
|
||||||
|
spyFunction();
|
||||||
|
return computedData1.value?.num;
|
||||||
|
});
|
||||||
|
const computedData3 = computed(() => {
|
||||||
|
if (computedData1.value) {
|
||||||
|
return computedData2.value;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
computedData3.value;
|
||||||
|
refData.value!.num = 4;
|
||||||
|
refData.value = null;
|
||||||
|
computedData3.value;
|
||||||
|
expect(spyFunction).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate the computation of item status', () => {
|
||||||
|
let statusMessage: string | undefined;
|
||||||
|
|
||||||
|
const itemList = ref<number[]>();
|
||||||
|
const isNotEmpty = computed(() => {
|
||||||
|
return !!itemList.value;
|
||||||
|
});
|
||||||
|
const status = computed(() => {
|
||||||
|
if (isNotEmpty.value) {
|
||||||
|
return 'Items are available';
|
||||||
|
} else {
|
||||||
|
return 'No items available';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
statusMessage = status.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
itemList.value = [4, 5, 6];
|
||||||
|
itemList.value = [7, 8, 9];
|
||||||
|
itemList.value = undefined;
|
||||||
|
|
||||||
|
expect(statusMessage).toBe('No items available');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('chained computed dirty reallocation after trigger computed getter', () => {
|
||||||
|
let _msg: string | undefined;
|
||||||
|
|
||||||
|
const items = ref<number[]>();
|
||||||
|
const isLoaded = computed(() => {
|
||||||
|
return !!items.value;
|
||||||
|
});
|
||||||
|
const msg = computed(() => {
|
||||||
|
if (isLoaded.value) {
|
||||||
|
return 'The items are loaded';
|
||||||
|
} else {
|
||||||
|
return 'The items are not loaded';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_msg = msg.value;
|
||||||
|
items.value = [1, 2, 3];
|
||||||
|
isLoaded.value; // <- trigger computed getter
|
||||||
|
_msg = msg.value;
|
||||||
|
items.value = undefined;
|
||||||
|
_msg = msg.value;
|
||||||
|
|
||||||
|
expect(_msg).toBe('The items are not loaded');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger by the second computed that maybe dirty', () => {
|
||||||
|
const cSpy = jest.fn();
|
||||||
|
|
||||||
|
const src1 = ref(0);
|
||||||
|
const src2 = ref(0);
|
||||||
|
const c1 = computed(() => src1.value);
|
||||||
|
const c2 = computed(() => (src1.value % 2) + src2.value);
|
||||||
|
const c3 = computed(() => {
|
||||||
|
cSpy();
|
||||||
|
c1.value;
|
||||||
|
c2.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
c3.value;
|
||||||
|
src1.value = 2;
|
||||||
|
c3.value;
|
||||||
|
expect(cSpy).toHaveBeenCalledTimes(2);
|
||||||
|
src2.value = 1;
|
||||||
|
c3.value;
|
||||||
|
expect(cSpy).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger the second effect', () => {
|
||||||
|
const fnSpy = jest.fn();
|
||||||
|
const v = ref(1);
|
||||||
|
const c = computed(() => v.value);
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
c.value;
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
c.value;
|
||||||
|
fnSpy();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fnSpy).toBeCalledTimes(1);
|
||||||
|
v.value = 2;
|
||||||
|
expect(fnSpy).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,215 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { vueReactive } from '../../../src';
|
||||||
|
const { reactive, isReactive, toRaw, ref, isRef, computed, watchEffect } = vueReactive;
|
||||||
|
|
||||||
|
describe('test reactive', () => {
|
||||||
|
it('should validate the reactivity of an object', () => {
|
||||||
|
const original = { key1: 10 };
|
||||||
|
const observed = reactive(original);
|
||||||
|
expect(observed).not.toBe(original);
|
||||||
|
expect(isReactive(observed)).toBe(true);
|
||||||
|
expect(isReactive(original)).toBe(false);
|
||||||
|
expect(observed.key1).toBe(10);
|
||||||
|
expect('key1' in observed).toBe(true);
|
||||||
|
expect(Object.keys(observed)).toEqual(['key1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate the prototype reactivity', () => {
|
||||||
|
const obj = {};
|
||||||
|
const reactiveObj = reactive(obj);
|
||||||
|
expect(isReactive(reactiveObj)).toBe(true);
|
||||||
|
|
||||||
|
const otherObj = { data: ['b'] };
|
||||||
|
expect(isReactive(otherObj)).toBe(false);
|
||||||
|
const reactiveOther = reactive(otherObj);
|
||||||
|
expect(isReactive(reactiveOther)).toBe(true);
|
||||||
|
expect(reactiveOther.data[0]).toBe('b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate nested reactivity', () => {
|
||||||
|
const original = {
|
||||||
|
nested: {
|
||||||
|
key2: 10,
|
||||||
|
},
|
||||||
|
array: [{ key3: 20 }],
|
||||||
|
};
|
||||||
|
const observed = reactive(original);
|
||||||
|
expect(isReactive(observed.nested)).toBe(true);
|
||||||
|
expect(isReactive(observed.array)).toBe(true);
|
||||||
|
expect(isReactive(observed.array[0])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe subtypes of IterableCollections (MyMap, MySet)', () => {
|
||||||
|
class MyMap extends Map {}
|
||||||
|
|
||||||
|
const myMap = reactive(new MyMap());
|
||||||
|
|
||||||
|
expect(myMap).toBeInstanceOf(Map);
|
||||||
|
expect(isReactive(myMap)).toBe(true);
|
||||||
|
|
||||||
|
myMap.set('newKey', {});
|
||||||
|
expect(isReactive(myMap.get('newKey'))).toBe(true);
|
||||||
|
|
||||||
|
class MySet extends Set {}
|
||||||
|
|
||||||
|
const mySet = reactive(new MySet());
|
||||||
|
|
||||||
|
expect(mySet).toBeInstanceOf(Set);
|
||||||
|
expect(isReactive(mySet)).toBe(true);
|
||||||
|
|
||||||
|
let testValue;
|
||||||
|
watchEffect(() => (testValue = mySet.has('newValue')));
|
||||||
|
expect(testValue).toBe(false);
|
||||||
|
mySet.add('newValue');
|
||||||
|
expect(testValue).toBe(true);
|
||||||
|
mySet.delete('newValue');
|
||||||
|
expect(testValue).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe subtypes of WeakCollections (CustomWeakMap, CustomWeakSet)', () => {
|
||||||
|
class CustomWeakMap extends WeakMap {}
|
||||||
|
|
||||||
|
const wmap = reactive(new CustomWeakMap());
|
||||||
|
|
||||||
|
expect(wmap).toBeInstanceOf(WeakMap);
|
||||||
|
expect(isReactive(wmap)).toBe(true);
|
||||||
|
|
||||||
|
const customKey = {};
|
||||||
|
wmap.set(customKey, {});
|
||||||
|
expect(isReactive(wmap.get(customKey))).toBe(true);
|
||||||
|
|
||||||
|
class CustomWeakSet extends WeakSet {}
|
||||||
|
|
||||||
|
const wset = reactive(new CustomWeakSet());
|
||||||
|
|
||||||
|
expect(wset).toBeInstanceOf(WeakSet);
|
||||||
|
expect(isReactive(wset)).toBe(true);
|
||||||
|
|
||||||
|
let testValue;
|
||||||
|
watchEffect(() => (testValue = wset.has(customKey)));
|
||||||
|
expect(testValue).toBe(false);
|
||||||
|
wset.add(customKey);
|
||||||
|
expect(testValue).toBe(true);
|
||||||
|
wset.delete(customKey);
|
||||||
|
expect(testValue).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate that changes in the observed value are reflected in the original (Object)', () => {
|
||||||
|
const original: any = { baz: 5 };
|
||||||
|
const observed = reactive(original);
|
||||||
|
|
||||||
|
observed.qux = 7;
|
||||||
|
expect(observed.qux).toBe(7);
|
||||||
|
expect(original.qux).toBe(7);
|
||||||
|
|
||||||
|
delete observed.baz;
|
||||||
|
expect('baz' in observed).toBe(false);
|
||||||
|
expect('baz' in original).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate that changes in the original value are reflected in the observed value (Object)', () => {
|
||||||
|
const initialData: any = { key1: 2 };
|
||||||
|
const reactiveData = reactive(initialData);
|
||||||
|
|
||||||
|
initialData.key2 = 3;
|
||||||
|
expect(initialData.key2).toBe(3);
|
||||||
|
expect(reactiveData.key2).toBe(3);
|
||||||
|
|
||||||
|
delete initialData.key1;
|
||||||
|
expect('key1' in initialData).toBe(false);
|
||||||
|
expect('key1' in reactiveData).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should verify that assigning an unobserved value to a property results in a reactive wrap', () => {
|
||||||
|
const reactiveObj = reactive<{ key?: object }>({});
|
||||||
|
const rawObj = {};
|
||||||
|
reactiveObj.key = rawObj;
|
||||||
|
expect(reactiveObj.key).not.toBe(rawObj);
|
||||||
|
expect(isReactive(reactiveObj.key)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should affirm that reactivity checks on an already reactive object yield the same Proxy', () => {
|
||||||
|
const initialData = { key: 3 };
|
||||||
|
const reactiveData1 = reactive(initialData);
|
||||||
|
const reactiveData2 = reactive(reactiveData1);
|
||||||
|
expect(reactiveData2).toBe(reactiveData1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should confirm that multiple observations of the same value return identical Proxies', () => {
|
||||||
|
const initialData = { key: 2 };
|
||||||
|
const reactiveData1 = reactive(initialData);
|
||||||
|
const reactiveData2 = reactive(initialData);
|
||||||
|
expect(reactiveData2).toBe(reactiveData1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ensure original object remains unaffected by Proxies', () => {
|
||||||
|
const initialObject: any = { key: 3 };
|
||||||
|
const secondaryObject = { key2: 4 };
|
||||||
|
const reactiveObject1 = reactive(initialObject);
|
||||||
|
const reactiveObject2 = reactive(secondaryObject);
|
||||||
|
reactiveObject1.key2 = reactiveObject2;
|
||||||
|
expect(reactiveObject1.key2).toBe(reactiveObject2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ensure that mutations on objects using reactive as prototype do not trigger', () => {
|
||||||
|
const reactiveObject = reactive({ key: 1 });
|
||||||
|
const originalObject = Object.create(reactiveObject);
|
||||||
|
let testValue;
|
||||||
|
watchEffect(() => (testValue = originalObject.key));
|
||||||
|
expect(testValue).toBe(1);
|
||||||
|
reactiveObject.key = 3;
|
||||||
|
expect(testValue).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate the identity of the original object after toRaw operation', () => {
|
||||||
|
const initialObject = { key: 2 };
|
||||||
|
const reactiveObject = reactive(initialObject);
|
||||||
|
expect(toRaw(reactiveObject)).toBe(initialObject);
|
||||||
|
expect(toRaw(initialObject)).toBe(initialObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate the non-mutability of original object when wrapped by user Proxy', () => {
|
||||||
|
const initialObject = {};
|
||||||
|
const reactiveObject = reactive(initialObject);
|
||||||
|
const proxyObject = new Proxy(reactiveObject, {});
|
||||||
|
const rawObject = toRaw(proxyObject);
|
||||||
|
expect(rawObject).toBe(initialObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should confirm the non-unwrapping of Ref<T>', () => {
|
||||||
|
const alphaRef = reactive(ref(2));
|
||||||
|
const betaRef = reactive(ref({ key: 2 }));
|
||||||
|
|
||||||
|
expect(isRef(alphaRef)).toBe(true);
|
||||||
|
expect(isRef(betaRef)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate the property reassignment from one ref to another', () => {
|
||||||
|
const alpha = ref(2);
|
||||||
|
const beta = ref(3);
|
||||||
|
const observedObject = reactive({ key: alpha });
|
||||||
|
const computedValue = computed(() => observedObject.key);
|
||||||
|
expect(computedValue.value).toBe(2);
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
observedObject.key = beta;
|
||||||
|
expect(computedValue.value).toBe(3);
|
||||||
|
|
||||||
|
beta.value += 2;
|
||||||
|
expect(computedValue.value).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,207 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { vueReactive, RefType } from '../../../src';
|
||||||
|
|
||||||
|
const { ref, isRef, isReactive, reactive, watchEffect, unref, shallowRef, isShallow, computed } = vueReactive;
|
||||||
|
|
||||||
|
describe('test ref', () => {
|
||||||
|
it('should validate the value holding capability', () => {
|
||||||
|
const testRef = ref(3);
|
||||||
|
expect(testRef.value).toBe(3);
|
||||||
|
testRef.value = 4;
|
||||||
|
expect(testRef.value).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should maintain reactivity', () => {
|
||||||
|
const testRef = ref(3);
|
||||||
|
let testVar;
|
||||||
|
const testFn = jest.fn(() => {
|
||||||
|
testVar = testRef.value;
|
||||||
|
});
|
||||||
|
watchEffect(testFn);
|
||||||
|
expect(testFn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(testVar).toBe(3);
|
||||||
|
testRef.value = 4;
|
||||||
|
expect(testFn).toHaveBeenCalledTimes(2);
|
||||||
|
expect(testVar).toBe(4);
|
||||||
|
testRef.value = 4;
|
||||||
|
expect(testFn).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should verify the reactivity of nested properties', () => {
|
||||||
|
const testRef = ref({
|
||||||
|
num: 3,
|
||||||
|
});
|
||||||
|
let testValue;
|
||||||
|
watchEffect(() => {
|
||||||
|
testValue = testRef.value.num;
|
||||||
|
});
|
||||||
|
expect(testValue).toBe(3);
|
||||||
|
testRef.value.num = 4;
|
||||||
|
expect(testValue).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should function correctly without an initial value', () => {
|
||||||
|
const testRef = ref();
|
||||||
|
let testValue;
|
||||||
|
watchEffect(() => {
|
||||||
|
testValue = testRef.value;
|
||||||
|
});
|
||||||
|
expect(testValue).toBe(undefined);
|
||||||
|
testRef.value = 3;
|
||||||
|
expect(testValue).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should operate as a standard property when nested within a reactive structure', () => {
|
||||||
|
const initialRef = ref(2);
|
||||||
|
const reactiveObj = reactive({
|
||||||
|
initialRef,
|
||||||
|
nested: {
|
||||||
|
innerRef: initialRef,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let first: number;
|
||||||
|
let second: number;
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
first = reactiveObj.initialRef;
|
||||||
|
second = reactiveObj.nested.innerRef;
|
||||||
|
});
|
||||||
|
|
||||||
|
const validateDummies = (val: number) => [first, second].forEach(dummy => expect(dummy).toBe(val));
|
||||||
|
|
||||||
|
validateDummies(2);
|
||||||
|
initialRef.value += 2;
|
||||||
|
validateDummies(4);
|
||||||
|
reactiveObj.initialRef += 2;
|
||||||
|
validateDummies(6);
|
||||||
|
reactiveObj.nested.innerRef += 2;
|
||||||
|
validateDummies(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should confirm nested ref types', () => {
|
||||||
|
const primaryRef = ref(2);
|
||||||
|
const secondaryRef = ref(primaryRef);
|
||||||
|
|
||||||
|
expect(typeof (secondaryRef.value + 3)).toBe('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate nested values in ref types', () => {
|
||||||
|
const data = {
|
||||||
|
key: ref(2),
|
||||||
|
};
|
||||||
|
|
||||||
|
const refData = ref(data);
|
||||||
|
|
||||||
|
expect(typeof (refData.value.key + 3)).toBe('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate ref types within array structures', () => {
|
||||||
|
const arrayData = ref([2, ref(4)]).value;
|
||||||
|
expect(isRef(arrayData[0])).toBe(false);
|
||||||
|
expect(isRef(arrayData[1])).toBe(true);
|
||||||
|
expect((arrayData[1] as RefType).value).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve tuple data types', () => {
|
||||||
|
const tupleData: [number, string, { a: number }, () => number, RefType<number>] = [
|
||||||
|
0,
|
||||||
|
'1',
|
||||||
|
{ a: 1 },
|
||||||
|
() => 0,
|
||||||
|
ref(0),
|
||||||
|
];
|
||||||
|
const refTuple = ref(tupleData);
|
||||||
|
|
||||||
|
refTuple.value[0] += 1;
|
||||||
|
expect(refTuple.value[0]).toEqual(1);
|
||||||
|
refTuple.value[1] = refTuple.value[1].concat('1');
|
||||||
|
expect(refTuple.value[1]).toEqual('11');
|
||||||
|
refTuple.value[2].a += 1;
|
||||||
|
expect(refTuple.value[2].a).toEqual(2);
|
||||||
|
expect(refTuple.value[3]()).toEqual(0);
|
||||||
|
refTuple.value[4].value += 1;
|
||||||
|
expect(refTuple.value[4].value).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly unref values', () => {
|
||||||
|
expect(unref(1)).toEqual(1);
|
||||||
|
expect(unref(ref(1))).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should verify the reactivity of a shallowRef', () => {
|
||||||
|
const shallowReference = shallowRef({ key: 1 });
|
||||||
|
expect(isReactive(shallowReference.value)).toBe(false);
|
||||||
|
|
||||||
|
let result;
|
||||||
|
watchEffect(() => {
|
||||||
|
result = shallowReference.value.key;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(1);
|
||||||
|
|
||||||
|
shallowReference.value = { key: 2 };
|
||||||
|
expect(isReactive(shallowReference.value)).toBe(false);
|
||||||
|
expect(result).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be isShallow', () => {
|
||||||
|
const shallowReference = shallowRef({ key: 1 });
|
||||||
|
expect(isShallow(shallowReference)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when isRef is called with a ref', () => {
|
||||||
|
const testRef = ref(1);
|
||||||
|
expect(isRef(testRef)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when isRef is called with a computed ref', () => {
|
||||||
|
const computedRef = computed(() => 1);
|
||||||
|
expect(isRef(computedRef)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when isRef is called with non-ref values', () => {
|
||||||
|
expect(isRef(0)).toBe(false);
|
||||||
|
expect(isRef(1)).toBe(false);
|
||||||
|
const obj = { value: 0 };
|
||||||
|
expect(isRef(obj)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ref not react when assigned the same proxy', () => {
|
||||||
|
const reactiveObj = reactive({ num: 0 });
|
||||||
|
|
||||||
|
const refInstance = ref(reactiveObj);
|
||||||
|
const watchFn1 = jest.fn(() => refInstance.value);
|
||||||
|
|
||||||
|
watchEffect(watchFn1);
|
||||||
|
|
||||||
|
refInstance.value = reactiveObj;
|
||||||
|
expect(watchFn1).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should shallowRef not react when assigned the same proxy', () => {
|
||||||
|
const reactiveObj = reactive({ num: 0 });
|
||||||
|
|
||||||
|
const shallowRefInstance = shallowRef(reactiveObj);
|
||||||
|
const watchFn2 = jest.fn(() => shallowRefInstance.value);
|
||||||
|
|
||||||
|
watchEffect(watchFn2);
|
||||||
|
|
||||||
|
shallowRefInstance.value = reactiveObj;
|
||||||
|
expect(watchFn2).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,247 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render, act, vueReactive, RefType, unmountComponentAtNode } from '../../../src';
|
||||||
|
import { Text, triggerClickEvent } from '../../jest/commonComponents';
|
||||||
|
import * as Inula from '../../../src';
|
||||||
|
|
||||||
|
const { useReactive, useReference, useComputed, useWatch } = vueReactive;
|
||||||
|
|
||||||
|
describe('test reactive in FunctionComponent', () => {
|
||||||
|
const { unmountComponentAtNode } = Inula;
|
||||||
|
let container: HTMLElement | null = null;
|
||||||
|
beforeEach(() => {
|
||||||
|
// 创建一个 DOM 元素作为渲染目标
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// 退出时进行清理
|
||||||
|
unmountComponentAtNode(container);
|
||||||
|
container?.remove();
|
||||||
|
container = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support useReactive in FunctionComponent', () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
|
||||||
|
function App(props) {
|
||||||
|
fn();
|
||||||
|
|
||||||
|
const reactiveObj = useReactive({
|
||||||
|
persons: [
|
||||||
|
{ name: 'p1', age: 1 },
|
||||||
|
{ name: 'p2', age: 2 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const newPerson = { name: 'p3', age: 3 };
|
||||||
|
const addOnePerson = function () {
|
||||||
|
reactiveObj.persons.push(newPerson);
|
||||||
|
};
|
||||||
|
const delOnePerson = function () {
|
||||||
|
reactiveObj.persons.pop();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'hasPerson'} text={`has new person: ${reactiveObj.persons.length}`} />
|
||||||
|
<button id={'addBtn'} onClick={addOnePerson}>
|
||||||
|
add person
|
||||||
|
</button>
|
||||||
|
<button id={'delBtn'} onClick={delOnePerson}>
|
||||||
|
delete person
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<App />, container);
|
||||||
|
|
||||||
|
expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
|
||||||
|
act(() => {
|
||||||
|
triggerClickEvent(container, 'addBtn');
|
||||||
|
});
|
||||||
|
expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
|
||||||
|
|
||||||
|
expect(fn).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support ref object in FunctionComponent', () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
function App(props) {
|
||||||
|
fn();
|
||||||
|
const refObj = useReference({
|
||||||
|
persons: [
|
||||||
|
{ name: 'p1', age: 1 },
|
||||||
|
{ name: 'p2', age: 2 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const newPerson = { name: 'p3', age: 3 };
|
||||||
|
const addOnePerson = function () {
|
||||||
|
refObj.value.persons.push(newPerson);
|
||||||
|
};
|
||||||
|
const delOnePerson = function () {
|
||||||
|
refObj.value.persons.pop();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'hasPerson'} text={`has new person: ${refObj.value.persons.length}`} />
|
||||||
|
<button id={'addBtn'} onClick={addOnePerson}>
|
||||||
|
add person
|
||||||
|
</button>
|
||||||
|
<button id={'delBtn'} onClick={delOnePerson}>
|
||||||
|
delete person
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<App />, container);
|
||||||
|
|
||||||
|
expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
|
||||||
|
act(() => {
|
||||||
|
triggerClickEvent(container, 'addBtn');
|
||||||
|
});
|
||||||
|
expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
|
||||||
|
|
||||||
|
expect(fn).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support ref primitive in FunctionComponent', () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
function App(props) {
|
||||||
|
fn();
|
||||||
|
const refObj = useReference(2);
|
||||||
|
|
||||||
|
const add = function () {
|
||||||
|
refObj.value++;
|
||||||
|
};
|
||||||
|
const del = function () {
|
||||||
|
refObj.value--;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'hasPerson'} text={`has new person: ${refObj.value}`} />
|
||||||
|
<button id={'addBtn'} onClick={add}>
|
||||||
|
add person
|
||||||
|
</button>
|
||||||
|
<button id={'delBtn'} onClick={del}>
|
||||||
|
delete person
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<App />, container);
|
||||||
|
|
||||||
|
expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
|
||||||
|
// 在Array中增加一个对象
|
||||||
|
act(() => {
|
||||||
|
triggerClickEvent(container, 'addBtn');
|
||||||
|
});
|
||||||
|
expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 3');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
triggerClickEvent(container, 'delBtn');
|
||||||
|
});
|
||||||
|
expect(container?.querySelector('#hasPerson')?.innerHTML).toBe('has new person: 2');
|
||||||
|
|
||||||
|
expect(fn).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support useComputed in FunctionComponent', () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
function App(props) {
|
||||||
|
const data = useReactive<{ bar?: string }>({});
|
||||||
|
const computedData = useComputed(() => {
|
||||||
|
fn();
|
||||||
|
return data.bar;
|
||||||
|
});
|
||||||
|
|
||||||
|
const setText = function () {
|
||||||
|
data.bar = 'bar';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'text'} text={computedData.value} />
|
||||||
|
<button id={'setText'} onClick={setText}>
|
||||||
|
set text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<App />, container);
|
||||||
|
|
||||||
|
expect(container?.querySelector('#text')?.innerHTML).toBe('');
|
||||||
|
act(() => {
|
||||||
|
triggerClickEvent(container, 'setText');
|
||||||
|
});
|
||||||
|
expect(container?.querySelector('#text')?.innerHTML).toBe('bar');
|
||||||
|
|
||||||
|
expect(fn).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support useWatch in FunctionComponent', () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
function App(props) {
|
||||||
|
let dummy;
|
||||||
|
const counter = useReactive({ num: 0 });
|
||||||
|
useWatch(() => {
|
||||||
|
fn();
|
||||||
|
dummy = counter.num;
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateCounter = function () {
|
||||||
|
counter.num++;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text id={'text'} text={counter.num} />
|
||||||
|
<button id={'updateCounter'} onClick={updateCounter}>
|
||||||
|
set text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<App />, container);
|
||||||
|
|
||||||
|
expect(container?.querySelector('#text')?.innerHTML).toBe('0');
|
||||||
|
act(() => {
|
||||||
|
triggerClickEvent(container, 'updateCounter');
|
||||||
|
});
|
||||||
|
expect(container?.querySelector('#text')?.innerHTML).toBe('1');
|
||||||
|
|
||||||
|
expect(fn).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,338 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { vueReactive } from '../../../src';
|
||||||
|
|
||||||
|
const { ref, reactive, watch, computed } = vueReactive;
|
||||||
|
|
||||||
|
describe('test watch', () => {
|
||||||
|
it('should watch effect', async () => {
|
||||||
|
const state = reactive({ count: 0 });
|
||||||
|
let dummy;
|
||||||
|
watch(() => {
|
||||||
|
dummy = state.count;
|
||||||
|
});
|
||||||
|
expect(dummy).toBe(0);
|
||||||
|
|
||||||
|
state.count++;
|
||||||
|
expect(dummy).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should watching single source: getter', async () => {
|
||||||
|
const state = reactive({ count: 0 });
|
||||||
|
let dummy;
|
||||||
|
watch(
|
||||||
|
() => state.count,
|
||||||
|
(count, prevCount) => {
|
||||||
|
dummy = [count, prevCount];
|
||||||
|
// assert types
|
||||||
|
count + 1;
|
||||||
|
if (prevCount) {
|
||||||
|
prevCount + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
state.count++;
|
||||||
|
expect(dummy).toMatchObject([1, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should watching single source: ref', async () => {
|
||||||
|
const count = ref(0);
|
||||||
|
let dummy;
|
||||||
|
const spy = jest.fn();
|
||||||
|
watch(count, (count, prevCount) => {
|
||||||
|
spy();
|
||||||
|
dummy = [count, prevCount];
|
||||||
|
});
|
||||||
|
count.value++;
|
||||||
|
expect(dummy).toMatchObject([1, 0]);
|
||||||
|
expect(spy).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('watching single source: array', async () => {
|
||||||
|
const array = reactive([]);
|
||||||
|
const spy = jest.fn((val, prevVal) => {
|
||||||
|
let a = 1;
|
||||||
|
});
|
||||||
|
watch(array, spy);
|
||||||
|
array.push(1);
|
||||||
|
|
||||||
|
// push会触发两次spy(一次是push,一次是length)
|
||||||
|
expect(spy).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not fire if watched getter result did not change', async () => {
|
||||||
|
const spy = jest.fn();
|
||||||
|
const n = ref(0);
|
||||||
|
watch(() => n.value % 2, spy);
|
||||||
|
|
||||||
|
n.value++;
|
||||||
|
expect(spy).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
n.value += 2;
|
||||||
|
// should not be called again because getter result did not change
|
||||||
|
expect(spy).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('watching single source: computed ref', async () => {
|
||||||
|
const count = ref(0);
|
||||||
|
const plus = computed(() => count.value + 1);
|
||||||
|
let dummy;
|
||||||
|
watch(plus, (count, prevCount) => {
|
||||||
|
dummy = [count, prevCount];
|
||||||
|
// assert types
|
||||||
|
count + 1;
|
||||||
|
if (prevCount) {
|
||||||
|
prevCount + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
count.value++;
|
||||||
|
expect(dummy).toMatchObject([2, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('watching primitive with deep: true', async () => {
|
||||||
|
const count = ref(0);
|
||||||
|
let dummy;
|
||||||
|
watch(count, (c, prevCount) => {
|
||||||
|
dummy = [c, prevCount];
|
||||||
|
});
|
||||||
|
count.value++;
|
||||||
|
expect(dummy).toMatchObject([1, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('directly watching reactive object (with automatic deep: true)', async () => {
|
||||||
|
const src = reactive({
|
||||||
|
count: 0,
|
||||||
|
});
|
||||||
|
let dummy;
|
||||||
|
watch(src, ({ count }) => {
|
||||||
|
dummy = count;
|
||||||
|
});
|
||||||
|
src.count++;
|
||||||
|
expect(dummy).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('directly watching reactive object with explicit deep: true', async () => {
|
||||||
|
const src = reactive({
|
||||||
|
state: {
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let dummy;
|
||||||
|
watch(src, ({ state }) => {
|
||||||
|
dummy = state?.count;
|
||||||
|
});
|
||||||
|
|
||||||
|
// nested should not trigger
|
||||||
|
src.state.count++;
|
||||||
|
expect(dummy).toBe(1);
|
||||||
|
|
||||||
|
// root level should trigger
|
||||||
|
src.state = { count: 2 };
|
||||||
|
expect(dummy).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('watching multiple sources', async () => {
|
||||||
|
const spy = jest.fn();
|
||||||
|
const state = reactive({ count: 1 });
|
||||||
|
const count = ref(1);
|
||||||
|
const plus = computed(() => count.value + 1);
|
||||||
|
|
||||||
|
let dummy;
|
||||||
|
watch([() => state.count, count, plus], (vals, oldVals) => {
|
||||||
|
spy();
|
||||||
|
dummy = [vals, oldVals];
|
||||||
|
// assert types
|
||||||
|
vals.concat(1);
|
||||||
|
oldVals.concat(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
state.count++;
|
||||||
|
expect(dummy).toMatchObject([
|
||||||
|
[2, 1, 2],
|
||||||
|
[1, 1, 2],
|
||||||
|
]);
|
||||||
|
expect(spy).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
count.value++;
|
||||||
|
// count触发一次,plus触发一次
|
||||||
|
expect(spy).toBeCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('watching multiple sources: readonly array', async () => {
|
||||||
|
const state = reactive({ count: 1 });
|
||||||
|
const status = ref(false);
|
||||||
|
|
||||||
|
let dummy;
|
||||||
|
watch([() => state.count, status] as const, (vals, oldVals) => {
|
||||||
|
dummy = [vals, oldVals];
|
||||||
|
const [count] = vals;
|
||||||
|
const [, oldStatus] = oldVals;
|
||||||
|
// assert types
|
||||||
|
count + 1;
|
||||||
|
oldStatus === true;
|
||||||
|
});
|
||||||
|
|
||||||
|
state.count++;
|
||||||
|
expect(dummy).toMatchObject([
|
||||||
|
[2, false],
|
||||||
|
[1, false],
|
||||||
|
]);
|
||||||
|
status.value = true;
|
||||||
|
expect(dummy).toMatchObject([
|
||||||
|
[2, true],
|
||||||
|
[2, false],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('watching multiple sources: reactive object (with automatic deep: true)', async () => {
|
||||||
|
const src = reactive({ count: 0 });
|
||||||
|
let dummy;
|
||||||
|
watch([src], ([state]) => {
|
||||||
|
dummy = state;
|
||||||
|
// assert types
|
||||||
|
state.count === 1;
|
||||||
|
});
|
||||||
|
src.count++;
|
||||||
|
expect(dummy).toMatchObject({ count: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stopping the watcher (effect)', async () => {
|
||||||
|
const state = reactive({ count: 0 });
|
||||||
|
let dummy;
|
||||||
|
const stop = watch(() => {
|
||||||
|
dummy = state.count;
|
||||||
|
});
|
||||||
|
expect(dummy).toBe(0);
|
||||||
|
|
||||||
|
stop();
|
||||||
|
state.count++;
|
||||||
|
// should not update
|
||||||
|
expect(dummy).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stopping the watcher (with source)', async () => {
|
||||||
|
const state = reactive({ count: 0 });
|
||||||
|
let dummy;
|
||||||
|
const stop = watch(
|
||||||
|
() => state.count,
|
||||||
|
count => {
|
||||||
|
dummy = count;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
state.count++;
|
||||||
|
expect(dummy).toBe(1);
|
||||||
|
|
||||||
|
stop();
|
||||||
|
state.count++;
|
||||||
|
// should not update
|
||||||
|
expect(dummy).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deep watch effect', async () => {
|
||||||
|
const state = reactive({
|
||||||
|
nested: {
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
array: [1, 2, 3],
|
||||||
|
map: new Map([
|
||||||
|
['a', 1],
|
||||||
|
['b', 2],
|
||||||
|
]),
|
||||||
|
set: new Set([1, 2, 3]),
|
||||||
|
});
|
||||||
|
|
||||||
|
let dummy;
|
||||||
|
watch(() => {
|
||||||
|
dummy = [state.nested.count, state.array[0], state.map.get('a'), state.set.has(1)];
|
||||||
|
});
|
||||||
|
|
||||||
|
state.nested.count++;
|
||||||
|
expect(dummy).toEqual([1, 1, 1, true]);
|
||||||
|
|
||||||
|
// nested array mutation
|
||||||
|
state.array[0] = 2;
|
||||||
|
expect(dummy).toEqual([1, 2, 1, true]);
|
||||||
|
|
||||||
|
// nested map mutation
|
||||||
|
state.map.set('a', 2);
|
||||||
|
expect(dummy).toEqual([1, 2, 2, true]);
|
||||||
|
|
||||||
|
// nested set mutation
|
||||||
|
state.set.delete(1);
|
||||||
|
expect(dummy).toEqual([1, 2, 2, false]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('watching deep ref', async () => {
|
||||||
|
const count = ref(0);
|
||||||
|
const double = computed(() => count.value * 2);
|
||||||
|
const state = reactive([count, double]);
|
||||||
|
|
||||||
|
let dummy;
|
||||||
|
watch(() => {
|
||||||
|
dummy = [state[0].value, state[1].value];
|
||||||
|
});
|
||||||
|
|
||||||
|
count.value++;
|
||||||
|
expect(dummy).toEqual([1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('warn and not respect deep option when using effect', async () => {
|
||||||
|
const arr = ref([1, [2]]);
|
||||||
|
const spy = jest.fn();
|
||||||
|
watch(() => {
|
||||||
|
spy();
|
||||||
|
return arr;
|
||||||
|
});
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
(arr.value[1] as Array<number>)[0] = 3;
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
// expect(`"deep" option is only respected`).toHaveBeenWarned()
|
||||||
|
});
|
||||||
|
|
||||||
|
test('watchEffect should not recursively trigger itself', async () => {
|
||||||
|
const spy = jest.fn();
|
||||||
|
const price = ref(10);
|
||||||
|
const history = ref<number[]>([]);
|
||||||
|
watch(() => {
|
||||||
|
history.value.push(price.value);
|
||||||
|
spy();
|
||||||
|
});
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computed refs should not trigger watch if value has no change', async () => {
|
||||||
|
const spy = jest.fn();
|
||||||
|
const source = ref(0);
|
||||||
|
const price = computed(() => source.value === 0);
|
||||||
|
watch(price, spy);
|
||||||
|
source.value++;
|
||||||
|
source.value++;
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('watching multiple sources: computed', async () => {
|
||||||
|
let count = 0;
|
||||||
|
const value = ref('1');
|
||||||
|
const plus = computed(() => !!value.value);
|
||||||
|
watch([plus], () => {
|
||||||
|
count++;
|
||||||
|
});
|
||||||
|
value.value = '2';
|
||||||
|
expect(plus.value).toBe(true);
|
||||||
|
expect(count).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,613 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { vueReactive } from '../../../src';
|
||||||
|
|
||||||
|
const { reactive, toRaw, watchEffect } = vueReactive;
|
||||||
|
|
||||||
|
function stop(stopHandle: () => void) {
|
||||||
|
stopHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('test watchEffect', () => {
|
||||||
|
it('should run the passed function once (wrapped by a effect)', () => {
|
||||||
|
const fnSpy = jest.fn();
|
||||||
|
watchEffect(fnSpy);
|
||||||
|
expect(fnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe basic properties', () => {
|
||||||
|
let dummy;
|
||||||
|
const counter = reactive({ num: 0 });
|
||||||
|
watchEffect(() => {
|
||||||
|
dummy = counter.num;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dummy).toBe(0);
|
||||||
|
counter.num = 7;
|
||||||
|
expect(dummy).toBe(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe multiple properties', () => {
|
||||||
|
let dummy;
|
||||||
|
const counter = reactive({ num1: 0, num2: 0 });
|
||||||
|
watchEffect(() => (dummy = counter.num1 + counter.num1 + counter.num2));
|
||||||
|
|
||||||
|
expect(dummy).toBe(0);
|
||||||
|
counter.num1 = counter.num2 = 7;
|
||||||
|
expect(dummy).toBe(21);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple effects', () => {
|
||||||
|
let dummy1, dummy2;
|
||||||
|
const counter = reactive({ num: 0 });
|
||||||
|
watchEffect(() => (dummy1 = counter.num));
|
||||||
|
watchEffect(() => (dummy2 = counter.num));
|
||||||
|
|
||||||
|
expect(dummy1).toBe(0);
|
||||||
|
expect(dummy2).toBe(0);
|
||||||
|
counter.num++;
|
||||||
|
expect(dummy1).toBe(1);
|
||||||
|
expect(dummy2).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe nested properties', () => {
|
||||||
|
let dummy;
|
||||||
|
const counter = reactive({ nested: { num: 0 } });
|
||||||
|
watchEffect(() => (dummy = counter.nested.num));
|
||||||
|
|
||||||
|
expect(dummy).toBe(0);
|
||||||
|
counter.nested.num = 8;
|
||||||
|
expect(dummy).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe delete operations', () => {
|
||||||
|
let dummy;
|
||||||
|
const obj = reactive<{
|
||||||
|
prop?: string;
|
||||||
|
}>({ prop: 'value' });
|
||||||
|
watchEffect(() => (dummy = obj.prop));
|
||||||
|
|
||||||
|
expect(dummy).toBe('value');
|
||||||
|
delete obj.prop;
|
||||||
|
expect(dummy).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe has operations', () => {
|
||||||
|
let dummy;
|
||||||
|
const obj = reactive<{ prop?: string | number }>({ prop: 'value' });
|
||||||
|
watchEffect(() => {
|
||||||
|
dummy = 'prop' in obj;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dummy).toBe(true);
|
||||||
|
delete obj.prop;
|
||||||
|
expect(dummy).toBe(false);
|
||||||
|
obj.prop = 12;
|
||||||
|
expect(dummy).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe properties on the prototype chain', () => {
|
||||||
|
let dummy;
|
||||||
|
const counter = reactive<{ num?: number }>({ num: 0 });
|
||||||
|
const parentCounter = reactive({ num: 2 });
|
||||||
|
Object.setPrototypeOf(counter, parentCounter);
|
||||||
|
watchEffect(() => (dummy = counter.num));
|
||||||
|
|
||||||
|
expect(dummy).toBe(0);
|
||||||
|
delete counter.num;
|
||||||
|
expect(dummy).toBe(2);
|
||||||
|
parentCounter.num = 4;
|
||||||
|
expect(dummy).toBe(4);
|
||||||
|
counter.num = 3;
|
||||||
|
expect(dummy).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe has operations on the prototype chain', () => {
|
||||||
|
let dummy;
|
||||||
|
const counter = reactive<{ num?: number }>({ num: 0 });
|
||||||
|
const parentCounter = reactive<{ num?: number }>({ num: 2 });
|
||||||
|
Object.setPrototypeOf(counter, parentCounter);
|
||||||
|
watchEffect(() => (dummy = 'num' in counter));
|
||||||
|
|
||||||
|
expect(dummy).toBe(true);
|
||||||
|
delete counter.num;
|
||||||
|
expect(dummy).toBe(true);
|
||||||
|
delete parentCounter.num;
|
||||||
|
expect(dummy).toBe(false);
|
||||||
|
counter.num = 3;
|
||||||
|
expect(dummy).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe inherited property accessors', () => {
|
||||||
|
let dummy, parentDummy, hiddenValue: any;
|
||||||
|
const obj = reactive<{ prop?: number }>({});
|
||||||
|
const parent = reactive({
|
||||||
|
set prop(value) {
|
||||||
|
hiddenValue = value;
|
||||||
|
},
|
||||||
|
get prop() {
|
||||||
|
return hiddenValue;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.setPrototypeOf(obj, parent);
|
||||||
|
watchEffect(() => (dummy = obj.prop));
|
||||||
|
watchEffect(() => (parentDummy = parent.prop));
|
||||||
|
|
||||||
|
expect(dummy).toBe(undefined);
|
||||||
|
expect(parentDummy).toBe(undefined);
|
||||||
|
obj.prop = 4;
|
||||||
|
expect(dummy).toBe(4);
|
||||||
|
// this doesn't work, should it?
|
||||||
|
// expect(parentDummy).toBe(4)
|
||||||
|
parent.prop = 2;
|
||||||
|
expect(dummy).toBe(2);
|
||||||
|
expect(parentDummy).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe function call chains', () => {
|
||||||
|
let dummy;
|
||||||
|
const counter = reactive({ num: 0 });
|
||||||
|
watchEffect(() => (dummy = getNum()));
|
||||||
|
|
||||||
|
function getNum() {
|
||||||
|
return counter.num;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(dummy).toBe(0);
|
||||||
|
counter.num = 2;
|
||||||
|
expect(dummy).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe iteration', () => {
|
||||||
|
let dummy;
|
||||||
|
const list = reactive(['Hello']);
|
||||||
|
watchEffect(() => (dummy = list.join(' ')));
|
||||||
|
|
||||||
|
expect(dummy).toBe('Hello');
|
||||||
|
list.push('World!');
|
||||||
|
expect(dummy).toBe('Hello World!');
|
||||||
|
list.shift();
|
||||||
|
expect(dummy).toBe('World!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe implicit array length changes', () => {
|
||||||
|
let dummy;
|
||||||
|
const list = reactive(['Hello']);
|
||||||
|
watchEffect(() => (dummy = list.join(' ')));
|
||||||
|
|
||||||
|
expect(dummy).toBe('Hello');
|
||||||
|
list[1] = 'World!';
|
||||||
|
expect(dummy).toBe('Hello World!');
|
||||||
|
list[3] = 'Hello!';
|
||||||
|
expect(dummy).toBe('Hello World! Hello!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe sparse array mutations', () => {
|
||||||
|
let dummy;
|
||||||
|
const list = reactive<string[]>([]);
|
||||||
|
list[1] = 'World!';
|
||||||
|
watchEffect(() => (dummy = list.join(' ')));
|
||||||
|
|
||||||
|
expect(dummy).toBe(' World!');
|
||||||
|
list[0] = 'Hello';
|
||||||
|
expect(dummy).toBe('Hello World!');
|
||||||
|
list.pop();
|
||||||
|
expect(dummy).toBe('Hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe enumeration', () => {
|
||||||
|
let dummy = 0;
|
||||||
|
const numbers = reactive<Record<string, number>>({ num1: 3 });
|
||||||
|
watchEffect(() => {
|
||||||
|
dummy = 0;
|
||||||
|
for (const key in numbers) {
|
||||||
|
dummy += numbers[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dummy).toBe(3);
|
||||||
|
numbers.num2 = 4;
|
||||||
|
expect(dummy).toBe(7);
|
||||||
|
delete numbers.num1;
|
||||||
|
expect(dummy).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe symbol keyed properties', () => {
|
||||||
|
const key = Symbol('symbol keyed prop');
|
||||||
|
let dummy, hasDummy;
|
||||||
|
const obj = reactive<{ [key]?: string }>({ [key]: 'value' });
|
||||||
|
watchEffect(() => (dummy = obj[key]));
|
||||||
|
watchEffect(() => (hasDummy = key in obj));
|
||||||
|
|
||||||
|
expect(dummy).toBe('value');
|
||||||
|
expect(hasDummy).toBe(true);
|
||||||
|
obj[key] = 'newValue';
|
||||||
|
expect(dummy).toBe('newValue');
|
||||||
|
delete obj[key];
|
||||||
|
expect(dummy).toBe(undefined);
|
||||||
|
expect(hasDummy).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not observe well-known symbol keyed properties', () => {
|
||||||
|
const key = Symbol.isConcatSpreadable;
|
||||||
|
let dummy;
|
||||||
|
const array: any = reactive([]);
|
||||||
|
watchEffect(() => (dummy = array[key]));
|
||||||
|
|
||||||
|
expect(array[key]).toBe(undefined);
|
||||||
|
expect(dummy).toBe(undefined);
|
||||||
|
array[key] = true;
|
||||||
|
expect(array[key]).toBe(true);
|
||||||
|
expect(dummy).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe function valued properties', () => {
|
||||||
|
const oldFunc = () => {};
|
||||||
|
const newFunc = () => {};
|
||||||
|
|
||||||
|
let dummy;
|
||||||
|
const obj = reactive({ func: oldFunc });
|
||||||
|
watchEffect(() => {
|
||||||
|
dummy = obj.func;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dummy).toBe(oldFunc);
|
||||||
|
obj.func = newFunc;
|
||||||
|
expect(dummy).toBe(newFunc);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe chained getters relying on this', () => {
|
||||||
|
const obj = reactive({
|
||||||
|
a: 1,
|
||||||
|
get b() {
|
||||||
|
return this.a;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let dummy;
|
||||||
|
watchEffect(() => (dummy = obj.b));
|
||||||
|
expect(dummy).toBe(1);
|
||||||
|
obj.a++;
|
||||||
|
expect(dummy).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe methods relying on this', () => {
|
||||||
|
const obj = reactive({
|
||||||
|
a: 1,
|
||||||
|
b() {
|
||||||
|
return this.a;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let dummy;
|
||||||
|
watchEffect(() => (dummy = obj.b()));
|
||||||
|
expect(dummy).toBe(1);
|
||||||
|
obj.a++;
|
||||||
|
expect(dummy).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not observe set operations without a value change', () => {
|
||||||
|
let hasDummy, getDummy;
|
||||||
|
const obj = reactive({ prop: 'value' });
|
||||||
|
|
||||||
|
const getSpy = jest.fn(() => (getDummy = obj.prop));
|
||||||
|
const hasSpy = jest.fn(() => (hasDummy = 'prop' in obj));
|
||||||
|
watchEffect(getSpy);
|
||||||
|
watchEffect(hasSpy);
|
||||||
|
|
||||||
|
expect(getDummy).toBe('value');
|
||||||
|
expect(hasDummy).toBe(true);
|
||||||
|
obj.prop = 'value';
|
||||||
|
expect(getSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(hasSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(getDummy).toBe('value');
|
||||||
|
expect(hasDummy).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not observe raw mutations', () => {
|
||||||
|
let dummy;
|
||||||
|
const obj = reactive<{ prop?: string }>({});
|
||||||
|
watchEffect(() => (dummy = toRaw(obj).prop));
|
||||||
|
|
||||||
|
expect(dummy).toBe(undefined);
|
||||||
|
obj.prop = 'value';
|
||||||
|
expect(dummy).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be triggered by raw mutations', () => {
|
||||||
|
let dummy;
|
||||||
|
const obj = reactive<{ prop?: string }>({});
|
||||||
|
watchEffect(() => (dummy = obj.prop));
|
||||||
|
|
||||||
|
expect(dummy).toBe(undefined);
|
||||||
|
toRaw(obj).prop = 'value';
|
||||||
|
expect(dummy).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be triggered by inherited raw setters', () => {
|
||||||
|
let dummy, parentDummy, hiddenValue: any;
|
||||||
|
const obj = reactive<{ prop?: number }>({});
|
||||||
|
const parent = reactive({
|
||||||
|
set prop(value) {
|
||||||
|
hiddenValue = value;
|
||||||
|
},
|
||||||
|
get prop() {
|
||||||
|
return hiddenValue;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.setPrototypeOf(obj, parent);
|
||||||
|
watchEffect(() => (dummy = obj.prop));
|
||||||
|
watchEffect(() => (parentDummy = parent.prop));
|
||||||
|
|
||||||
|
expect(dummy).toBe(undefined);
|
||||||
|
expect(parentDummy).toBe(undefined);
|
||||||
|
toRaw(obj).prop = 4;
|
||||||
|
expect(dummy).toBe(undefined);
|
||||||
|
expect(parentDummy).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should avoid implicit infinite recursive loops with itself', () => {
|
||||||
|
const counter = reactive({ num: 0 });
|
||||||
|
|
||||||
|
const counterSpy = jest.fn(() => {
|
||||||
|
counter.num++;
|
||||||
|
});
|
||||||
|
watchEffect(counterSpy);
|
||||||
|
expect(counter.num).toBe(1);
|
||||||
|
expect(counterSpy).toHaveBeenCalledTimes(1);
|
||||||
|
counter.num = 4;
|
||||||
|
expect(counter.num).toBe(5);
|
||||||
|
expect(counterSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow explicitly recursive raw function loops', () => {
|
||||||
|
const counter = reactive({ num: 0 });
|
||||||
|
const numSpy = jest.fn(() => {
|
||||||
|
counter.num++;
|
||||||
|
if (counter.num < 10) {
|
||||||
|
numSpy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watchEffect(numSpy);
|
||||||
|
expect(counter.num).toEqual(10);
|
||||||
|
expect(numSpy).toHaveBeenCalledTimes(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should avoid infinite loops with other effects', () => {
|
||||||
|
const nums = reactive({ num1: 0, num2: 1 });
|
||||||
|
|
||||||
|
const spy1 = jest.fn(() => (nums.num1 = nums.num2));
|
||||||
|
const spy2 = jest.fn(() => (nums.num2 = nums.num1));
|
||||||
|
watchEffect(spy1);
|
||||||
|
watchEffect(spy2);
|
||||||
|
expect(nums.num1).toBe(1);
|
||||||
|
expect(nums.num2).toBe(1);
|
||||||
|
expect(spy1).toHaveBeenCalledTimes(1);
|
||||||
|
expect(spy2).toHaveBeenCalledTimes(1);
|
||||||
|
nums.num2 = 4;
|
||||||
|
expect(nums.num1).toBe(4);
|
||||||
|
expect(nums.num2).toBe(4);
|
||||||
|
expect(spy1).toHaveBeenCalledTimes(2);
|
||||||
|
expect(spy2).toHaveBeenCalledTimes(2);
|
||||||
|
nums.num1 = 10;
|
||||||
|
expect(nums.num1).toBe(10);
|
||||||
|
expect(nums.num2).toBe(10);
|
||||||
|
expect(spy1).toHaveBeenCalledTimes(3);
|
||||||
|
expect(spy2).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a new reactive version of the function', () => {
|
||||||
|
function greet() {
|
||||||
|
return 'Hello World';
|
||||||
|
}
|
||||||
|
|
||||||
|
const effect1 = watchEffect(greet);
|
||||||
|
const effect2 = watchEffect(greet);
|
||||||
|
expect(typeof effect1).toBe('function');
|
||||||
|
expect(typeof effect2).toBe('function');
|
||||||
|
expect(effect1).not.toBe(greet);
|
||||||
|
expect(effect1).not.toBe(effect2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should discover new branches while running automatically', () => {
|
||||||
|
let dummy;
|
||||||
|
const obj = reactive({ prop: 'value', run: false });
|
||||||
|
|
||||||
|
const conditionalSpy = jest.fn(() => {
|
||||||
|
dummy = obj.run ? obj.prop : 'other';
|
||||||
|
});
|
||||||
|
watchEffect(conditionalSpy);
|
||||||
|
|
||||||
|
expect(dummy).toBe('other');
|
||||||
|
expect(conditionalSpy).toHaveBeenCalledTimes(1);
|
||||||
|
obj.prop = 'Hi';
|
||||||
|
expect(dummy).toBe('other');
|
||||||
|
expect(conditionalSpy).toHaveBeenCalledTimes(1);
|
||||||
|
obj.run = true;
|
||||||
|
expect(dummy).toBe('Hi');
|
||||||
|
expect(conditionalSpy).toHaveBeenCalledTimes(2);
|
||||||
|
obj.prop = 'World';
|
||||||
|
expect(dummy).toBe('World');
|
||||||
|
expect(conditionalSpy).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be triggered by mutating a property, which is used in an inactive branch', () => {
|
||||||
|
let dummy;
|
||||||
|
const obj = reactive({ prop: 'value', run: true });
|
||||||
|
|
||||||
|
const conditionalSpy = jest.fn(() => {
|
||||||
|
dummy = obj.run ? obj.prop : 'other';
|
||||||
|
});
|
||||||
|
watchEffect(conditionalSpy);
|
||||||
|
|
||||||
|
expect(dummy).toBe('value');
|
||||||
|
expect(conditionalSpy).toHaveBeenCalledTimes(1);
|
||||||
|
obj.run = false;
|
||||||
|
expect(dummy).toBe('other');
|
||||||
|
expect(conditionalSpy).toHaveBeenCalledTimes(2);
|
||||||
|
obj.prop = 'value2';
|
||||||
|
expect(dummy).toBe('other');
|
||||||
|
expect(conditionalSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle deep effect recursion using cleanup fallback', () => {
|
||||||
|
const results = reactive([0]);
|
||||||
|
const effects = [];
|
||||||
|
for (let i = 1; i < 40; i++) {
|
||||||
|
(index => {
|
||||||
|
const fx = watchEffect(() => {
|
||||||
|
results[index] = results[index - 1] * 2;
|
||||||
|
});
|
||||||
|
effects.push({ fx, index });
|
||||||
|
})(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(results[39]).toBe(0);
|
||||||
|
results[0] = 1;
|
||||||
|
expect(results[39]).toBe(Math.pow(2, 39));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run multiple times for a single mutation', () => {
|
||||||
|
let dummy;
|
||||||
|
const obj = reactive<Record<string, number>>({});
|
||||||
|
const fnSpy = jest.fn(() => {
|
||||||
|
for (const key in obj) {
|
||||||
|
dummy = obj[key];
|
||||||
|
}
|
||||||
|
dummy = obj.prop;
|
||||||
|
});
|
||||||
|
watchEffect(fnSpy);
|
||||||
|
|
||||||
|
expect(fnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
obj.prop = 16;
|
||||||
|
expect(dummy).toBe(16);
|
||||||
|
expect(fnSpy).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should observe class method invocations', () => {
|
||||||
|
class Model {
|
||||||
|
count: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
inc() {
|
||||||
|
this.count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = reactive(new Model());
|
||||||
|
let dummy;
|
||||||
|
watchEffect(() => {
|
||||||
|
dummy = model.count;
|
||||||
|
});
|
||||||
|
expect(dummy).toBe(0);
|
||||||
|
model.inc();
|
||||||
|
expect(dummy).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stop', () => {
|
||||||
|
let dummy;
|
||||||
|
const obj = reactive({ prop: 1 });
|
||||||
|
const runner = watchEffect(() => {
|
||||||
|
dummy = obj.prop;
|
||||||
|
});
|
||||||
|
obj.prop = 2;
|
||||||
|
expect(dummy).toBe(2);
|
||||||
|
stop(runner);
|
||||||
|
obj.prop = 3;
|
||||||
|
expect(dummy).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stop: a stopped effect is nested in a normal effect', () => {
|
||||||
|
let dummy;
|
||||||
|
const obj = reactive({ prop: 1 });
|
||||||
|
const runner = watchEffect(() => {
|
||||||
|
dummy = obj.prop;
|
||||||
|
});
|
||||||
|
runner();
|
||||||
|
obj.prop = 2;
|
||||||
|
expect(dummy).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger all effects when array length is set to 0', () => {
|
||||||
|
const observed: any = reactive([1]);
|
||||||
|
let dummy, record;
|
||||||
|
watchEffect(() => {
|
||||||
|
dummy = observed.length;
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
record = observed[0];
|
||||||
|
});
|
||||||
|
expect(dummy).toBe(1);
|
||||||
|
expect(record).toBe(1);
|
||||||
|
|
||||||
|
observed[1] = 2;
|
||||||
|
expect(observed[1]).toBe(2);
|
||||||
|
|
||||||
|
observed.unshift(3);
|
||||||
|
expect(dummy).toBe(3);
|
||||||
|
expect(record).toBe(3);
|
||||||
|
|
||||||
|
observed.length = 0;
|
||||||
|
expect(dummy).toBe(0);
|
||||||
|
expect(record).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be triggered when set length with string', () => {
|
||||||
|
let ret1 = 'idle';
|
||||||
|
let ret2 = 'idle';
|
||||||
|
const arr1 = reactive(new Array(11).fill(0));
|
||||||
|
const arr2 = reactive(new Array(11).fill(0));
|
||||||
|
watchEffect(() => {
|
||||||
|
ret1 = arr1[10] === undefined ? 'arr[10] is set to empty' : 'idle';
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
ret2 = arr2[10] === undefined ? 'arr[10] is set to empty' : 'idle';
|
||||||
|
});
|
||||||
|
arr1.length = 2;
|
||||||
|
arr2.length = '2' as any;
|
||||||
|
expect(ret1).toBe(ret2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should track hasOwnProperty', () => {
|
||||||
|
const obj: any = reactive({});
|
||||||
|
let has = false;
|
||||||
|
const fnSpy = jest.fn();
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
fnSpy();
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
has = obj.hasOwnProperty('foo');
|
||||||
|
});
|
||||||
|
expect(fnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(has).toBe(false);
|
||||||
|
|
||||||
|
obj.foo = 1;
|
||||||
|
expect(fnSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(has).toBe(true);
|
||||||
|
|
||||||
|
delete obj.foo;
|
||||||
|
expect(fnSpy).toHaveBeenCalledTimes(3);
|
||||||
|
expect(has).toBe(false);
|
||||||
|
|
||||||
|
// should not trigger on unrelated key
|
||||||
|
obj.bar = 2;
|
||||||
|
expect(fnSpy).toHaveBeenCalledTimes(3);
|
||||||
|
expect(has).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
|
@ -119,7 +119,7 @@ describe('测试store中的Array', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => {
|
it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => {
|
||||||
let globalStore = useUserStore();
|
const globalStore = useUserStore();
|
||||||
function Child(props) {
|
function Child(props) {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
@ -146,8 +146,7 @@ describe('测试store中的Array', () => {
|
||||||
expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3');
|
expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3');
|
||||||
|
|
||||||
// shift
|
// shift
|
||||||
//@ts-ignore TODO:why is this argument here?
|
globalStore.$s.persons.shift();
|
||||||
globalStore.$s.persons.shift({ name: 'p0', age: 0 });
|
|
||||||
expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3');
|
expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3');
|
||||||
|
|
||||||
// 赋值[2]
|
// 赋值[2]
|
||||||
|
@ -172,7 +171,7 @@ describe('测试store中的Array', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('测试Array方法: forEach()', () => {
|
it('测试Array方法: forEach()', () => {
|
||||||
let globalStore = useUserStore();
|
const globalStore = useUserStore();
|
||||||
function Child(props) {
|
function Child(props) {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
|
|
@ -315,4 +315,60 @@ describe('测试store中的Set', () => {
|
||||||
});
|
});
|
||||||
expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: ');
|
expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: ');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('测试Set方法,add string', () => {
|
||||||
|
const useUserStore = createStore({
|
||||||
|
id: 'user',
|
||||||
|
state: {
|
||||||
|
persons: new Set(),
|
||||||
|
},
|
||||||
|
})();
|
||||||
|
|
||||||
|
useUserStore.persons.add('p1');
|
||||||
|
expect(useUserStore.persons.has('p1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Set方法,add obj', () => {
|
||||||
|
const useUserStore = createStore({
|
||||||
|
id: 'user',
|
||||||
|
state: {
|
||||||
|
persons: new Set(),
|
||||||
|
},
|
||||||
|
})();
|
||||||
|
|
||||||
|
const obj = { a: 1 };
|
||||||
|
useUserStore.persons.add(obj);
|
||||||
|
expect(useUserStore.persons.has(obj)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Set方法,default value', () => {
|
||||||
|
const obj = { b: 1 };
|
||||||
|
const useUserStore = createStore({
|
||||||
|
id: 'user',
|
||||||
|
state: {
|
||||||
|
persons: new Set(['p1', obj]),
|
||||||
|
},
|
||||||
|
})();
|
||||||
|
|
||||||
|
expect(useUserStore.persons.has('p1')).toBe(true);
|
||||||
|
expect(useUserStore.persons.has(obj)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('测试Set方法,watch', () => {
|
||||||
|
const obj = { b: 1 };
|
||||||
|
const useUserStore = createStore({
|
||||||
|
id: 'user',
|
||||||
|
state: {
|
||||||
|
persons: new Set<any>(['p1', obj]),
|
||||||
|
},
|
||||||
|
})();
|
||||||
|
|
||||||
|
let dummy;
|
||||||
|
const key = {};
|
||||||
|
// effect(() => (dummy = cset.has(key)));
|
||||||
|
useUserStore.persons.watch(() => (dummy = useUserStore.persons.has(key)));
|
||||||
|
expect(dummy).toBe(undefined);
|
||||||
|
useUserStore.persons.add(key);
|
||||||
|
expect(dummy).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
import * as Inula from '../../../src/index';
|
import * as Inula from '../../../src/index';
|
||||||
import * as LogUtils from '../../jest/logUtils';
|
import * as LogUtils from '../../jest/logUtils';
|
||||||
import { clearStore, createStore, useStore } from '../../../src/inulax/store/StoreHandler';
|
import { clearStore, createStore, useStore } from '../../../src/inulax/store/StoreHandler';
|
||||||
|
|
|
@ -42,7 +42,7 @@ describe('Dollar store access', () => {
|
||||||
function App() {
|
function App() {
|
||||||
const logStore = useLogStore();
|
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);
|
Inula.render(<App />, container);
|
||||||
|
@ -64,7 +64,7 @@ describe('Dollar store access', () => {
|
||||||
>
|
>
|
||||||
add
|
add
|
||||||
</button>
|
</button>
|
||||||
<p id={RESULT_ID}>{logStore.$c.length()}</p>
|
<p id={RESULT_ID}>{logStore.$c.length}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,11 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* 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', () => {
|
describe('watch', () => {
|
||||||
it('shouhld watch primitive state variable', async () => {
|
it('should watch primitive state variable', async () => {
|
||||||
const useStore = createStore({
|
const useStore = createStore({
|
||||||
state: {
|
state: {
|
||||||
variable: 'x',
|
variable: 'x',
|
||||||
|
@ -38,7 +39,7 @@ describe('watch', () => {
|
||||||
|
|
||||||
expect(counter).toBe(1);
|
expect(counter).toBe(1);
|
||||||
});
|
});
|
||||||
it('shouhld watch object variable', async () => {
|
it('should watch object variable', async () => {
|
||||||
const useStore = createStore({
|
const useStore = createStore({
|
||||||
state: {
|
state: {
|
||||||
variable: 'x',
|
variable: 'x',
|
||||||
|
@ -60,7 +61,7 @@ describe('watch', () => {
|
||||||
expect(counter).toBe(1);
|
expect(counter).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shouhld watch array item', async () => {
|
it('should watch array item', async () => {
|
||||||
const useStore = createStore({
|
const useStore = createStore({
|
||||||
state: {
|
state: {
|
||||||
arr: ['x'],
|
arr: ['x'],
|
||||||
|
@ -73,7 +74,7 @@ describe('watch', () => {
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
store.arr.watch('0', () => {
|
store.$s.arr.watch('0', () => {
|
||||||
counter++;
|
counter++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -82,7 +83,7 @@ describe('watch', () => {
|
||||||
expect(counter).toBe(1);
|
expect(counter).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shouhld watch collection item', async () => {
|
it('should watch collection item', async () => {
|
||||||
const useStore = createStore({
|
const useStore = createStore({
|
||||||
state: {
|
state: {
|
||||||
collection: new Map([['a', 'a']]),
|
collection: new Map([['a', 'a']]),
|
||||||
|
@ -141,4 +142,203 @@ describe('watch', () => {
|
||||||
expect(counter1).toBe(3);
|
expect(counter1).toBe(3);
|
||||||
expect(counterAll).toBe(6);
|
expect(counterAll).toBe(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should watch multiple variables independedntly', async () => {
|
||||||
|
const useStore = createStore({
|
||||||
|
state: {
|
||||||
|
bool1: true,
|
||||||
|
bool2: false,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
toggle1: state => (state.bool1 = !state.bool1),
|
||||||
|
toggle2: state => (state.bool2 = !state.bool2),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let counter1 = 0;
|
||||||
|
let counterAll = 0;
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
watch(store.$s, () => {
|
||||||
|
counterAll++;
|
||||||
|
});
|
||||||
|
|
||||||
|
store.$s.watch('bool1', () => {
|
||||||
|
counter1++;
|
||||||
|
});
|
||||||
|
|
||||||
|
store.toggle1();
|
||||||
|
store.toggle1();
|
||||||
|
|
||||||
|
store.toggle2();
|
||||||
|
|
||||||
|
store.toggle1();
|
||||||
|
|
||||||
|
store.toggle2();
|
||||||
|
store.toggle2();
|
||||||
|
|
||||||
|
expect(counter1).toBe(3);
|
||||||
|
expect(counterAll).toBe(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('watchEffect', () => {
|
||||||
|
it('should watchEffect obj item', async () => {
|
||||||
|
const useStore = createStore({
|
||||||
|
state: {
|
||||||
|
variable1: '1',
|
||||||
|
variable2: '2',
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
change1: state => (state.variable1 = '11'),
|
||||||
|
change2: state => (state.variable2 = '22'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
let counter = 0;
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
store.variable1;
|
||||||
|
counter++;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(counter).toBe(1);
|
||||||
|
|
||||||
|
store.change1();
|
||||||
|
|
||||||
|
expect(counter).toBe(2);
|
||||||
|
|
||||||
|
store.change2();
|
||||||
|
|
||||||
|
expect(counter).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should watchEffect deep obj item', async () => {
|
||||||
|
const useStore = createStore({
|
||||||
|
state: {
|
||||||
|
obj: {
|
||||||
|
a: 'x',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
change: state => (state.obj.a = 'a'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
let counter = 0;
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
store.obj.a;
|
||||||
|
counter++;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(counter).toBe(1);
|
||||||
|
|
||||||
|
store.change();
|
||||||
|
|
||||||
|
expect(counter).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should watchEffect Map item', async () => {
|
||||||
|
const useStore = createStore({
|
||||||
|
state: {
|
||||||
|
collection: new Map([['a', 'a']]),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
change: state => state.collection.set('a', 'x'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
let counter = 0;
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
store.collection.get('a');
|
||||||
|
counter++;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(counter).toBe(1);
|
||||||
|
|
||||||
|
store.change();
|
||||||
|
|
||||||
|
expect(counter).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should watchEffect Set item', async () => {
|
||||||
|
const useStore = createStore({
|
||||||
|
state: {
|
||||||
|
set: new Set(['a']),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
change: state => state.set.delete('a'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
let counter = 0;
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
store.set.has('a');
|
||||||
|
counter++;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(counter).toBe(1);
|
||||||
|
|
||||||
|
store.change();
|
||||||
|
|
||||||
|
expect(counter).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should watchEffect WeakSet item', async () => {
|
||||||
|
const obj = { a: 1 };
|
||||||
|
const useStore = createStore({
|
||||||
|
state: {
|
||||||
|
set: new WeakSet([obj]),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
change: state => state.set.delete(obj),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
let counter = 0;
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
store.$s.set.has(obj);
|
||||||
|
counter++;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(counter).toBe(1);
|
||||||
|
|
||||||
|
store.change();
|
||||||
|
|
||||||
|
expect(counter).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should watchEffect array item', async () => {
|
||||||
|
const useStore = createStore({
|
||||||
|
state: {
|
||||||
|
arr: ['x'],
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
change: state => (state.arr[0] = 'a'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
let counter = 0;
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
store.arr[0];
|
||||||
|
counter++;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(counter).toBe(1);
|
||||||
|
|
||||||
|
store.change();
|
||||||
|
|
||||||
|
expect(counter).toBe(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,8 +13,8 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createStore, useStore } from '../../../src/index';
|
import { createStore } from '../../../src/index';
|
||||||
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
import { describe, it, expect } from '@jest/globals';
|
||||||
|
|
||||||
describe('Using deep variables', () => {
|
describe('Using deep variables', () => {
|
||||||
it('should listen to object variable change', () => {
|
it('should listen to object variable change', () => {
|
||||||
|
@ -100,6 +100,7 @@ describe('Using deep variables', () => {
|
||||||
const key = Array.from(testStore.data.keys())[0];
|
const key = Array.from(testStore.data.keys())[0];
|
||||||
|
|
||||||
expect(testStore.data.has(key)).toBe(true);
|
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);
|
||||||
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);
|
testStore.data.set(data.key, data.value);
|
||||||
|
|
||||||
|
expect(testStore.data.has(data.key)).toBe(true);
|
||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
testStore.$subscribe(mutation => {
|
testStore.$subscribe(mutation => {
|
||||||
counter++;
|
counter++;
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createProxy } from '../../../src/inulax/proxy/ProxyHandler';
|
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';
|
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
||||||
|
|
||||||
describe('Proxy', () => {
|
describe('Proxy', () => {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
"bugs": "",
|
"bugs": "",
|
||||||
"license": "MulanPSL2",
|
"license": "MulanPSL2",
|
||||||
"main": "./build/index.js",
|
"main": "./src/index.ts",
|
||||||
"repository": {},
|
"repository": {},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
|
|
@ -56,8 +56,11 @@ import {
|
||||||
isPortal,
|
isPortal,
|
||||||
} from './external/InulaIs';
|
} from './external/InulaIs';
|
||||||
import { createStore, useStore, clearStore } from './inulax/store/StoreHandler';
|
import { createStore, useStore, clearStore } from './inulax/store/StoreHandler';
|
||||||
|
import { reactive, 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 * 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 { act } from './external/TestUtil';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -70,7 +73,28 @@ import {
|
||||||
} from './dom/DOMExternal';
|
} from './dom/DOMExternal';
|
||||||
|
|
||||||
import { syncUpdates as flushSync } from './renderer/TreeBuilder';
|
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 = {
|
const Inula = {
|
||||||
Children,
|
Children,
|
||||||
|
@ -122,9 +146,11 @@ const Inula = {
|
||||||
Profiler,
|
Profiler,
|
||||||
StrictMode,
|
StrictMode,
|
||||||
Suspense,
|
Suspense,
|
||||||
|
// vue reactive api
|
||||||
|
vueReactive,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const version = __VERSION__;
|
export const version = '';
|
||||||
export {
|
export {
|
||||||
Children,
|
Children,
|
||||||
createRef,
|
createRef,
|
||||||
|
@ -161,7 +187,7 @@ export {
|
||||||
clearStore,
|
clearStore,
|
||||||
reduxAdapter,
|
reduxAdapter,
|
||||||
watch,
|
watch,
|
||||||
toRaw,
|
|
||||||
// 兼容ReactIs
|
// 兼容ReactIs
|
||||||
isFragment,
|
isFragment,
|
||||||
isElement,
|
isElement,
|
||||||
|
@ -178,7 +204,15 @@ export {
|
||||||
Profiler,
|
Profiler,
|
||||||
StrictMode,
|
StrictMode,
|
||||||
Suspense,
|
Suspense,
|
||||||
|
// vue reactive api
|
||||||
|
vueReactive,
|
||||||
};
|
};
|
||||||
|
|
||||||
export * from './types';
|
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;
|
export default Inula;
|
||||||
|
|
|
@ -13,9 +13,17 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { KeyTypes, ReactiveFlags } from './Constants';
|
||||||
|
import { Mutation } from './types/ProxyTypes';
|
||||||
|
|
||||||
export function isObject(obj: any): boolean {
|
export function isObject(obj: any): boolean {
|
||||||
const type = typeof obj;
|
const type = typeof obj;
|
||||||
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 {
|
export function isSet(obj: any): boolean {
|
||||||
|
@ -126,21 +134,24 @@ export function getDetailedType(val: any) {
|
||||||
return typeof val;
|
return typeof val;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveMutation(from, to) {
|
export function resolveMutation<T extends { length?: number; _type?: string; entries?: any; values?: any }>(
|
||||||
|
from: T,
|
||||||
|
to: T
|
||||||
|
): Mutation<T> {
|
||||||
if (getDetailedType(from) !== getDetailedType(to)) {
|
if (getDetailedType(from) !== getDetailedType(to)) {
|
||||||
return { mutation: true, from, to };
|
return { mutation: true, from, to };
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (getDetailedType(from)) {
|
switch (getDetailedType(from)) {
|
||||||
case 'array': {
|
case 'array': {
|
||||||
const len = Math.max(from.length, to.length);
|
const len = Math.max(from.length ?? 0, to.length ?? 0);
|
||||||
const res: any[] = [];
|
const res: any[] = [];
|
||||||
let found = false;
|
let found = false;
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
if (from.length <= i) {
|
if ((from.length ?? 0) <= i) {
|
||||||
res[i] = { mutation: true, to: to[i] };
|
res[i] = { mutation: true, to: to[i] };
|
||||||
found = true;
|
found = true;
|
||||||
} else if (to.length <= i) {
|
} else if ((to.length ?? 0) <= i) {
|
||||||
res[i] = { mutation: true, from: from[i] };
|
res[i] = { mutation: true, from: from[i] };
|
||||||
found = true;
|
found = true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -200,8 +211,14 @@ export function resolveMutation(from, to) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function omit(obj, ...attrs) {
|
export function isShallow(value: unknown): boolean {
|
||||||
const res = { ...obj };
|
return !!(value && value[ReactiveFlags.IS_SHALLOW]);
|
||||||
attrs.forEach(attr => delete res[attr]);
|
}
|
||||||
return res;
|
|
||||||
|
export function isReactive(value: unknown) {
|
||||||
|
return !!(value && !!value[KeyTypes.RAW_VALUE]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isReadonly(value: unknown): boolean {
|
||||||
|
return !!(value && value[ReactiveFlags.IS_READONLY]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,4 +15,23 @@
|
||||||
|
|
||||||
export const OBSERVER_KEY = typeof Symbol === 'function' ? Symbol('_inulaObserver') : '_inulaObserver';
|
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',
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
优化内容:
|
||||||
|
1. 抽取getValOrProxy函数,减少18处重复代码
|
||||||
|
2. 抽取watchProp函数,减少7处重复代码
|
||||||
|
3. 抽取watchEffect函数
|
||||||
|
4. 抽取registerListener函数
|
||||||
|
5. 删除无用代码,hookObserver其实并没有使用
|
||||||
|
```js
|
||||||
|
const boundHandler = {};
|
||||||
|
Object.entries(handler).forEach(([id, val]) => {
|
||||||
|
boundHandler[id] = (...args: any[]) => {
|
||||||
|
return (val as any)(...args, hookObserver);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return new Proxy(rawObj, { ...boundHandler });
|
||||||
|
```
|
||||||
|
6. 统一抽取常量,如:
|
||||||
|
```js
|
||||||
|
```
|
||||||
|
7. 增加watchEffect函数
|
||||||
|
8. 删除WeakMapProxy中handler的add和clear方法,因为WeakMap并不存在这两个方法
|
||||||
|
9. ObjectProxy中的handler增加deleteProperty方法,处理delete操作
|
||||||
|
```js
|
||||||
|
let dummy;
|
||||||
|
const obj = reactive<{
|
||||||
|
prop?: string;
|
||||||
|
}>({ prop: 'value' });
|
||||||
|
effect(() => (dummy = obj.prop));
|
||||||
|
|
||||||
|
expect(dummy).toBe('value');
|
||||||
|
delete obj.prop;
|
||||||
|
expect(dummy).toBe(undefined);
|
||||||
|
```
|
||||||
|
10. ObjectProxy中的handler增加has方法,处理in操作
|
||||||
|
```js
|
||||||
|
let dummy;
|
||||||
|
const obj = reactive<{ prop?: string | number }>({ prop: 'value' });
|
||||||
|
effect(() => {
|
||||||
|
dummy = 'prop' in obj;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dummy).toBe(true);
|
||||||
|
delete obj.prop;
|
||||||
|
expect(dummy).toBe(false);
|
||||||
|
obj.prop = 12;
|
||||||
|
expect(dummy).toBe(true);
|
||||||
|
```
|
||||||
|
11. 当前不支持for (let key in numbers)这种写法
|
||||||
|
```js
|
||||||
|
it('should observe enumeration', () => {
|
||||||
|
let dummy = 0;
|
||||||
|
const numbers = reactive<Record<string, number>>({ num1: 3 });
|
||||||
|
effect(() => {
|
||||||
|
dummy = 0;
|
||||||
|
for (let key in numbers) {
|
||||||
|
dummy += numbers[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dummy).toBe(3);
|
||||||
|
numbers.num2 = 4;
|
||||||
|
expect(dummy).toBe(7);
|
||||||
|
delete numbers.num1;
|
||||||
|
expect(dummy).toBe(4);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
12. watchEffect中的watcher不支持第二个参数options
|
||||||
|
```js
|
||||||
|
const runner = effect(
|
||||||
|
() => {
|
||||||
|
dummy = obj.foo;
|
||||||
|
},
|
||||||
|
{ onTrigger }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
13. 不支持readonly
|
|
@ -0,0 +1,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);
|
||||||
|
})
|
||||||
|
```
|
|
@ -13,7 +13,7 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IObserver } from './Observer';
|
import { IObserver } from '../types/ProxyTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一个对象(对象、数组、集合)对应一个Observer
|
* 一个对象(对象、数组、集合)对应一个Observer
|
||||||
|
@ -49,5 +49,7 @@ export class HooklessObserver implements IObserver {
|
||||||
|
|
||||||
allChange(): void {}
|
allChange(): void {}
|
||||||
|
|
||||||
|
arrayLengthChange(): void {}
|
||||||
|
|
||||||
clearByVNode(vNode): void {}
|
clearByVNode(vNode): void {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,24 +17,10 @@ import { launchUpdateFromVNode } from '../../renderer/TreeBuilder';
|
||||||
import { getProcessingVNode } from '../../renderer/GlobalVar';
|
import { getProcessingVNode } from '../../renderer/GlobalVar';
|
||||||
import { VNode } from '../../renderer/vnode/VNode';
|
import { VNode } from '../../renderer/vnode/VNode';
|
||||||
import { devtools } from '../devtools';
|
import { devtools } from '../devtools';
|
||||||
|
import { KeyTypes } from '../Constants';
|
||||||
|
import { addRContext, RContextSet } from '../reactive/RContext';
|
||||||
|
|
||||||
export interface IObserver {
|
import { IObserver, Listener, Mutation } from '../types/ProxyTypes';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一个对象(对象、数组、集合)对应一个Observer
|
* 一个对象(对象、数组、集合)对应一个Observer
|
||||||
|
@ -44,12 +30,25 @@ export class Observer implements IObserver {
|
||||||
|
|
||||||
keyVNodes = new Map();
|
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 {
|
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();
|
const processingVNode = getProcessingVNode();
|
||||||
if (processingVNode === null || !processingVNode.observers) {
|
if (processingVNode === null || !processingVNode.observers) {
|
||||||
// 异常场景
|
// 异常场景
|
||||||
|
@ -60,11 +59,6 @@ export class Observer implements IObserver {
|
||||||
processingVNode.observers.add(this);
|
processingVNode.observers.add(this);
|
||||||
|
|
||||||
// key -> vNodes,记录这个prop被哪些VNode使用了
|
// key -> vNodes,记录这个prop被哪些VNode使用了
|
||||||
let vNodes = this.keyVNodes.get(key);
|
|
||||||
if (!vNodes) {
|
|
||||||
vNodes = new Set();
|
|
||||||
this.keyVNodes.set(key, vNodes);
|
|
||||||
}
|
|
||||||
vNodes.add(processingVNode);
|
vNodes.add(processingVNode);
|
||||||
|
|
||||||
// vNode -> keys,记录这个VNode使用了哪些props
|
// vNode -> keys,记录这个VNode使用了哪些props
|
||||||
|
@ -77,9 +71,9 @@ export class Observer implements IObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对象的属性被赋值时调用
|
// 对象的属性被赋值时调用
|
||||||
setProp(key: string | symbol, mutation: any): void {
|
setProp(key: string | symbol, mutation: Mutation, oldValue?: any, newValue?: any): void {
|
||||||
const vNodes = this.keyVNodes.get(key);
|
const vNodes = this.keyVNodes.get(key);
|
||||||
//NOTE: using Set directly can lead to deadlock
|
// NOTE: using Set directly can lead to deadlock
|
||||||
const vNodeArray = Array.from(vNodes || []);
|
const vNodeArray = Array.from(vNodes || []);
|
||||||
vNodeArray.forEach((vNode: VNode) => {
|
vNodeArray.forEach((vNode: VNode) => {
|
||||||
if (vNode.isStoreChange) {
|
if (vNode.isStoreChange) {
|
||||||
|
@ -92,8 +86,28 @@ export class Observer implements IObserver {
|
||||||
this.triggerUpdate(vNode);
|
this.triggerUpdate(vNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: mutations are different in dev and production.
|
// 这里需要过滤调COLLECTION_CHANGE,因为这个是集合的变化,不是具体的某个prop的变化,否则会重复触发
|
||||||
this.triggerChangeListeners({ mutation, vNodes });
|
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 {
|
triggerUpdate(vNode: VNode): void {
|
||||||
|
@ -101,11 +115,11 @@ export class Observer implements IObserver {
|
||||||
launchUpdateFromVNode(vNode);
|
launchUpdateFromVNode(vNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
addListener(listener: (mutation) => void): void {
|
addListener(listener: Listener): void {
|
||||||
this.listeners.push(listener);
|
this.listeners.push(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeListener(listener: (mutation) => void): void {
|
removeListener(listener: Listener): void {
|
||||||
this.listeners = this.listeners.filter(item => item != listener);
|
this.listeners = this.listeners.filter(item => item != listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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的关系数据
|
// 删除Observer中保存的这个VNode的关系数据
|
||||||
clearByVNode(vNode: VNode): void {
|
clearByVNode(vNode: VNode): void {
|
||||||
const keys = this.vNodeKeys.get(vNode);
|
const keys = this.vNodeKeys.get(vNode);
|
||||||
|
|
|
@ -19,65 +19,59 @@ import { HooklessObserver } from './HooklessObserver';
|
||||||
import { isArray, isCollection, isObject } from '../CommonUtils';
|
import { isArray, isCollection, isObject } from '../CommonUtils';
|
||||||
import { createArrayProxy } from './handlers/ArrayProxyHandler';
|
import { createArrayProxy } from './handlers/ArrayProxyHandler';
|
||||||
import { createCollectionProxy } from './handlers/CollectionProxyHandler';
|
import { createCollectionProxy } from './handlers/CollectionProxyHandler';
|
||||||
import type { IObserver } from '../types';
|
import { IObserver, CurrentListener } from '../types/ProxyTypes';
|
||||||
import { OBSERVER_KEY, RAW_VALUE } from '../Constants';
|
|
||||||
|
|
||||||
// 保存rawObj -> Proxy
|
// Save rawObj -> Proxy
|
||||||
const proxyMap = new WeakMap();
|
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 {
|
// Use WeakMap to save rawObj -> Observer, without polluting the original object
|
||||||
return rawObj[OBSERVER_KEY];
|
const rawObserverMap = new WeakMap<any, IObserver>();
|
||||||
|
|
||||||
|
export function getObserver(rawObj: any): IObserver {
|
||||||
|
return rawObserverMap.get(rawObj) as IObserver;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setObserverKey =
|
function setObserver(rawObj: any, observer: IObserver): void {
|
||||||
typeof OBSERVER_KEY === 'string'
|
rawObserverMap.set(rawObj, observer);
|
||||||
? (rawObj, observer) => {
|
}
|
||||||
Object.defineProperty(rawObj, OBSERVER_KEY, {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: false,
|
|
||||||
value: 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))) {
|
if (!(rawObj && isObject(rawObj))) {
|
||||||
return rawObj;
|
return rawObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已代理过
|
// Already exists
|
||||||
const existProxy = proxyMap.get(rawObj);
|
const existProxy = proxyMap.get(rawObj);
|
||||||
if (existProxy) {
|
if (existProxy) {
|
||||||
return existProxy;
|
return existProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observer不需要代理
|
// Observer does not need to be approached
|
||||||
if (rawObj instanceof Observer) {
|
if (rawObj instanceof Observer) {
|
||||||
return rawObj;
|
return rawObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建Observer
|
// Create Observer
|
||||||
let observer: IObserver = getObserver(rawObj);
|
let observer = getObserver(rawObj);
|
||||||
if (!observer) {
|
if (!observer) {
|
||||||
observer = isHookObserver ? new Observer() : new HooklessObserver();
|
observer = (isDeepProxy ? new Observer() : new HooklessObserver()) as IObserver;
|
||||||
setObserverKey(rawObj, observer);
|
setObserver(rawObj, observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
hookObserverMap.set(rawObj, isHookObserver);
|
deepProxyMap.set(rawObj, isDeepProxy);
|
||||||
|
|
||||||
// 创建Proxy
|
// 创建Proxy
|
||||||
let proxyObj;
|
let proxyObj: ProxyHandler<any>;
|
||||||
if (!isHookObserver) {
|
if (!isDeepProxy) {
|
||||||
proxyObj = createObjectProxy(
|
proxyObj = createObjectProxy(
|
||||||
rawObj,
|
rawObj,
|
||||||
{
|
{
|
||||||
current: change => {
|
current: change => {
|
||||||
listener.current(change);
|
listener?.current(change);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
|
@ -86,27 +80,23 @@ export function createProxy(rawObj: any, listener: { current: (...args) => any }
|
||||||
// 数组
|
// 数组
|
||||||
proxyObj = createArrayProxy(rawObj as [], {
|
proxyObj = createArrayProxy(rawObj as [], {
|
||||||
current: change => {
|
current: change => {
|
||||||
listener.current(change);
|
listener?.current(change);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (isCollection(rawObj)) {
|
} else if (isCollection(rawObj)) {
|
||||||
// 集合
|
// 集合
|
||||||
proxyObj = createCollectionProxy(
|
proxyObj = createCollectionProxy(rawObj, {
|
||||||
rawObj,
|
current: change => {
|
||||||
{
|
listener?.current(change);
|
||||||
current: change => {
|
|
||||||
listener.current(change);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
true
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// 原生对象 或 函数
|
// 原生对象 或 函数
|
||||||
proxyObj = createObjectProxy(
|
proxyObj = createObjectProxy(
|
||||||
rawObj,
|
rawObj,
|
||||||
{
|
{
|
||||||
current: change => {
|
current: change => {
|
||||||
listener.current(change);
|
listener?.current(change);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
|
@ -118,7 +108,3 @@ export function createProxy(rawObj: any, listener: { current: (...args) => any }
|
||||||
|
|
||||||
return proxyObj;
|
return proxyObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toRaw<T>(observed: T): T {
|
|
||||||
return observed && observed[RAW_VALUE];
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,150 +13,23 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
import { registerListener } from './HandlerUtils';
|
||||||
import { isSame, isValidIntegerKey } from '../../CommonUtils';
|
import { baseSetFun, baseGetFun } from './BaseObjectHandler';
|
||||||
import { resolveMutation } from '../../CommonUtils';
|
import { CurrentListener, Listeners, ObjectType } from '../../types/ProxyTypes';
|
||||||
import { isPanelActive } from '../../devtools';
|
|
||||||
import { OBSERVER_KEY, RAW_VALUE } from '../../Constants';
|
|
||||||
|
|
||||||
function set(rawObj: any[], key: string, value: any, receiver: any) {
|
export function createArrayProxy<T extends any[]>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
|
||||||
const oldValue = rawObj[key];
|
const listeners: Listeners = [];
|
||||||
const oldLength = rawObj.length;
|
|
||||||
const newValue = value;
|
|
||||||
|
|
||||||
const oldArray = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
function get(rawObj: T, key: KeyType, receiver: any) {
|
||||||
|
return baseGetFun(rawObj, key, receiver, listener, listeners);
|
||||||
const ret = Reflect.set(rawObj, key, newValue, receiver);
|
|
||||||
|
|
||||||
const newLength = rawObj.length;
|
|
||||||
const observer = getObserver(rawObj);
|
|
||||||
|
|
||||||
const mutation = isPanelActive() ? resolveMutation(oldArray, rawObj) : resolveMutation(null, rawObj);
|
|
||||||
|
|
||||||
if (!isSame(newValue, oldValue)) {
|
|
||||||
// 值不一样,触发监听器
|
|
||||||
if (observer.watchers?.[key]) {
|
|
||||||
observer.watchers[key].forEach(cb => {
|
|
||||||
cb(key, oldValue, newValue, mutation);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触发属性变化
|
|
||||||
observer.setProp(key, mutation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldLength !== newLength) {
|
const handler = {
|
||||||
// 触发数组的大小变化
|
|
||||||
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 = {
|
|
||||||
get,
|
get,
|
||||||
set,
|
set: baseSetFun,
|
||||||
};
|
};
|
||||||
|
|
||||||
getObserver(rawObj).addListener(change => {
|
registerListener(rawObj, listener, listeners);
|
||||||
if (!change.parents) change.parents = [];
|
|
||||||
change.parents.push(rawObj);
|
|
||||||
listener.current(change);
|
|
||||||
listeners.forEach(lst => lst(change));
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Proxy(rawObj, handle);
|
return new Proxy(rawObj as ObjectType, handler);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,236 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { KeyTypes } from '../../Constants';
|
||||||
|
import { getObserver } from '../ProxyHandler';
|
||||||
|
import { getValOrProxy, getWatchFn, triggerSetWatchers } from './HandlerUtils';
|
||||||
|
import {
|
||||||
|
CollectionStringTypes,
|
||||||
|
CollectionTypes,
|
||||||
|
CurrentListener,
|
||||||
|
IterableTypes,
|
||||||
|
Listener,
|
||||||
|
Listeners,
|
||||||
|
MapTypes,
|
||||||
|
ObjectType,
|
||||||
|
SetTypes,
|
||||||
|
} from '../../types/ProxyTypes';
|
||||||
|
import { resolveMutation } from '../../CommonUtils';
|
||||||
|
|
||||||
|
export function baseGetFun(
|
||||||
|
rawObj: MapTypes | SetTypes,
|
||||||
|
key: any,
|
||||||
|
receiver: any,
|
||||||
|
listeners: Listeners,
|
||||||
|
handler: ObjectType,
|
||||||
|
type: CollectionStringTypes,
|
||||||
|
getFun?: (rawObj: MapTypes, key: any) => any
|
||||||
|
): any {
|
||||||
|
if (key === KeyTypes.VALUE) {
|
||||||
|
return receiver;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((type === 'Map' || type === 'Set') && key === KeyTypes.SIZE) {
|
||||||
|
return baseSizeFun(rawObj as IterableTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((type === 'Map' || type === 'WeakMap') && key === KeyTypes.GET) {
|
||||||
|
return getFun!.bind(null, rawObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
||||||
|
const value = Reflect.get(handler, key, receiver);
|
||||||
|
return value.bind(null, rawObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = getObserver(rawObj);
|
||||||
|
if (key === KeyTypes.WATCH) {
|
||||||
|
return getWatchFn(observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === KeyTypes.ADD_LISTENER) {
|
||||||
|
return (listener: Listener) => {
|
||||||
|
listeners.push(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === KeyTypes.REMOVE_LISTENER) {
|
||||||
|
return (listener: Listener) => {
|
||||||
|
listeners = listeners.filter(item => item != listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === KeyTypes.RAW_VALUE) {
|
||||||
|
return rawObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Reflect.get(rawObj, key, receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseSizeFun(rawObj: IterableTypes) {
|
||||||
|
const observer = getObserver(rawObj);
|
||||||
|
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||||
|
return rawObj.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function baseForEach(
|
||||||
|
rawObj: CollectionTypes,
|
||||||
|
callback: (valProxy: any, keyProxy: any, rawObj: any) => void,
|
||||||
|
listener: CurrentListener,
|
||||||
|
listeners: Listeners
|
||||||
|
) {
|
||||||
|
const observer = getObserver(rawObj);
|
||||||
|
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||||
|
rawObj.forEach((value, key) => {
|
||||||
|
const valProxy = getValOrProxy('valueChange', false, value, rawObj, listener, listeners);
|
||||||
|
const keyProxy = getValOrProxy('keyChange', false, key, rawObj, listener, listeners);
|
||||||
|
// 最后一个参数要返回代理对象
|
||||||
|
return callback(valProxy, keyProxy, rawObj);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function baseClearFun(rawObj: IterableTypes, proxies: Map<any, any>, type: CollectionStringTypes) {
|
||||||
|
const oldSize = rawObj.size;
|
||||||
|
rawObj.clear();
|
||||||
|
proxies.clear();
|
||||||
|
|
||||||
|
if (oldSize > 0) {
|
||||||
|
const observer = getObserver(rawObj);
|
||||||
|
|
||||||
|
if (type === 'Set') {
|
||||||
|
triggerSetWatchers(observer);
|
||||||
|
}
|
||||||
|
observer.allChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function baseDeleteFun(
|
||||||
|
rawObj: MapTypes | SetTypes,
|
||||||
|
value: any,
|
||||||
|
type: CollectionStringTypes,
|
||||||
|
proxies?: MapTypes
|
||||||
|
) {
|
||||||
|
// 通过new Set([{a: 1}])创建的值并没有加入proxies,所以还需要判断一下
|
||||||
|
const val = proxies?.get(value) || value;
|
||||||
|
|
||||||
|
if (baseHasFun(rawObj, value, proxies)) {
|
||||||
|
let oldValues;
|
||||||
|
if (type === 'Set') {
|
||||||
|
oldValues = Array.from((rawObj as Set<any>).values());
|
||||||
|
} else if (type === 'Map') {
|
||||||
|
oldValues = [...Array.from((rawObj as Map<any, any>).entries())];
|
||||||
|
}
|
||||||
|
|
||||||
|
rawObj.delete(val);
|
||||||
|
proxies?.delete(value);
|
||||||
|
|
||||||
|
const observer = getObserver(rawObj);
|
||||||
|
|
||||||
|
if (type === 'Set' || type === 'WeakSet') {
|
||||||
|
triggerSetWatchers(observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mutation;
|
||||||
|
if (type === 'Set') {
|
||||||
|
mutation = resolveMutation(
|
||||||
|
{
|
||||||
|
_type: type,
|
||||||
|
values: oldValues,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_type: type,
|
||||||
|
values: Array.from((rawObj as Set<any>).values()),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if (type === 'Map') {
|
||||||
|
mutation = resolveMutation(
|
||||||
|
{
|
||||||
|
_type: type,
|
||||||
|
entries: oldValues,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_type: type,
|
||||||
|
entries: Array.from((rawObj as Map<any, any>).entries()),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mutation = { mutation: true, from: value, to: rawObj };
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.setProp(value, mutation);
|
||||||
|
|
||||||
|
if (type === 'Set' || type === 'Map') {
|
||||||
|
observer.setProp(KeyTypes.COLLECTION_CHANGE, mutation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function baseAddFunOfSet(
|
||||||
|
rawObj: SetTypes,
|
||||||
|
value: any,
|
||||||
|
listener: CurrentListener,
|
||||||
|
listeners: Listeners,
|
||||||
|
type: CollectionStringTypes,
|
||||||
|
proxies?: MapTypes
|
||||||
|
): Record<string, any> {
|
||||||
|
if (!baseHasFun(rawObj, value, proxies)) {
|
||||||
|
const proxy = getValOrProxy('valueChange', false, value, rawObj, listener, listeners);
|
||||||
|
|
||||||
|
let oldValues;
|
||||||
|
if (type === 'Set') {
|
||||||
|
oldValues = Array.from((rawObj as Set<any>).values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
proxies?.set(value, proxy);
|
||||||
|
rawObj.add(proxy);
|
||||||
|
|
||||||
|
const observer = getObserver(rawObj);
|
||||||
|
|
||||||
|
triggerSetWatchers(observer);
|
||||||
|
|
||||||
|
let mutation;
|
||||||
|
if (type === 'Set') {
|
||||||
|
mutation = resolveMutation(
|
||||||
|
{
|
||||||
|
_type: type,
|
||||||
|
values: oldValues,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_type: type,
|
||||||
|
values: Array.from((rawObj as Set<any>).values()),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mutation = { mutation: true, from: rawObj, to: value };
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.setProp(value, mutation, undefined, value);
|
||||||
|
if (type === 'Set') {
|
||||||
|
observer.setProp(KeyTypes.COLLECTION_CHANGE, mutation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function baseHasFun(rawObj: MapTypes | SetTypes, value: any, proxies?: MapTypes): boolean {
|
||||||
|
// 通过new Set([{a: 1}])创建的值并没有加入proxies,所以还需要判断一下
|
||||||
|
return proxies?.has(value) || rawObj.has(value);
|
||||||
|
}
|
|
@ -0,0 +1,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);
|
||||||
|
}
|
|
@ -19,19 +19,20 @@ import { createSetProxy } from './SetProxy';
|
||||||
import { createWeakMapProxy } from './WeakMapProxy';
|
import { createWeakMapProxy } from './WeakMapProxy';
|
||||||
import { createMapProxy } from './MapProxy';
|
import { createMapProxy } from './MapProxy';
|
||||||
|
|
||||||
export function createCollectionProxy(
|
import { CurrentListener } from '../../types/ProxyTypes';
|
||||||
rawObj: Record<string, unknown>,
|
|
||||||
listener: { current: (...args) => any },
|
export function createCollectionProxy<T extends any>(rawObj: T, listener: CurrentListener) {
|
||||||
hookObserver = true
|
|
||||||
): ProxyHandler<Record<string, unknown>> {
|
|
||||||
if (isWeakSet(rawObj)) {
|
if (isWeakSet(rawObj)) {
|
||||||
return createWeakSetProxy(rawObj, listener, hookObserver);
|
return createWeakSetProxy(rawObj as WeakSet<any>, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSet(rawObj)) {
|
if (isSet(rawObj)) {
|
||||||
return createSetProxy(rawObj, listener, hookObserver);
|
return createSetProxy(rawObj as Set<any>, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isWeakMap(rawObj)) {
|
if (isWeakMap(rawObj)) {
|
||||||
return createWeakMapProxy(rawObj, listener, hookObserver);
|
return createWeakMapProxy(rawObj as WeakMap<any, any>, listener);
|
||||||
}
|
}
|
||||||
return createMapProxy(rawObj, listener, hookObserver);
|
|
||||||
|
return createMapProxy(rawObj as Map<any, any>, listener);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { createProxy, getObserver, deepProxyMap } from '../ProxyHandler';
|
||||||
|
import { isArray, isValidIntegerKey, resolveMutation } from '../../CommonUtils';
|
||||||
|
import { isRef } from '../../reactive/Ref';
|
||||||
|
import { CurrentListener, IObserver, Listeners, WatchFn, WatchHandler } from '../../types/ProxyTypes';
|
||||||
|
|
||||||
|
export const SET_WATCH_KEY = '_setWatchKey';
|
||||||
|
|
||||||
|
// 获取观察者函数
|
||||||
|
export function getWatchFn(observer: IObserver): WatchFn {
|
||||||
|
// 返回一个函数,该函数接受属性和处理程序作为参数
|
||||||
|
return (prop: any, handler?: WatchHandler) => {
|
||||||
|
// Set不需要指定prop
|
||||||
|
if (typeof prop === 'function') {
|
||||||
|
handler = prop;
|
||||||
|
prop = SET_WATCH_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 观察指定的属性
|
||||||
|
watchProp(observer, prop, handler as WatchHandler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 观察属性
|
||||||
|
function watchProp(observer: IObserver, prop: any, handler: WatchHandler) {
|
||||||
|
if (!observer.watchers[prop]) {
|
||||||
|
observer.watchers[prop] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将处理程序添加到观察者数组中
|
||||||
|
if (!observer.watchers[prop].includes(handler)) {
|
||||||
|
observer.watchers[prop].push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// 从观察者数组中移除处理程序
|
||||||
|
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function triggerSetWatchers(observer: IObserver) {
|
||||||
|
if (observer.watchers[SET_WATCH_KEY]) {
|
||||||
|
observer.watchers[SET_WATCH_KEY].forEach(cb => {
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getValOrProxy(
|
||||||
|
key: string | symbol,
|
||||||
|
singleLevel: boolean,
|
||||||
|
value: any,
|
||||||
|
rawObj: Record<string, any>,
|
||||||
|
listener: CurrentListener,
|
||||||
|
listeners: Listeners
|
||||||
|
): any {
|
||||||
|
if (isRef(value)) {
|
||||||
|
// ref unwrapping
|
||||||
|
return isArray(rawObj) && isValidIntegerKey(key) ? value : value.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于value也需要进一步代理
|
||||||
|
return singleLevel
|
||||||
|
? value
|
||||||
|
: createProxy(
|
||||||
|
value,
|
||||||
|
{
|
||||||
|
current: change => {
|
||||||
|
if (!change.parents) change.parents = [];
|
||||||
|
change.parents.push(rawObj);
|
||||||
|
const mutation = resolveMutation(
|
||||||
|
{ ...rawObj, [key]: change.mutation.from },
|
||||||
|
{ ...rawObj, [key]: change.mutation.to }
|
||||||
|
);
|
||||||
|
listener.current({ ...change, mutation });
|
||||||
|
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deepProxyMap.get(rawObj)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerListener(rawObj: any, listener: CurrentListener, listeners: Listeners) {
|
||||||
|
getObserver(rawObj).addListener(change => {
|
||||||
|
if (!change.parents) change.parents = [];
|
||||||
|
change.parents.push(rawObj);
|
||||||
|
listener.current(change);
|
||||||
|
listeners.forEach(lst => lst(change));
|
||||||
|
});
|
||||||
|
}
|
|
@ -13,252 +13,108 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* 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 { isSame } from '../../CommonUtils';
|
||||||
import { resolveMutation } from '../../CommonUtils';
|
import { resolveMutation } from '../../CommonUtils';
|
||||||
import { isPanelActive } from '../../devtools';
|
import { KeyTypes } from '../../Constants';
|
||||||
import { RAW_VALUE } 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(
|
export function createMapProxy<T extends Map<any, any>>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
|
||||||
rawObj: Record<string, any>,
|
const listeners: Listeners = [];
|
||||||
listener: { current: (...args) => any },
|
// 场景:let obj = {}; map.set(obj, val);
|
||||||
hookObserver = true
|
// 满足两个UT:1、map.has(Array.from(map.keys())[0])为true; 2、map.has(obj)为true;
|
||||||
): Record<string, any> {
|
const keyProxies = new Map();
|
||||||
let listeners: ((mutation) => Record<string, any>)[] = [];
|
|
||||||
let oldData: [any, any][] = [];
|
|
||||||
const proxies = new Map();
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
function getFun(rawObj: T, key: any): any {
|
||||||
function getFun(rawObj: { get: (key: any) => any; has: (key: any) => boolean }, key: any): any {
|
const keyProxy = rawObj.has(key) ? key : keyProxies.get(key);
|
||||||
const keyProxy = rawObj.has(key) ? key : proxies.get(key);
|
|
||||||
if (!keyProxy) return;
|
if (!keyProxy) return;
|
||||||
|
|
||||||
const observer = getObserver(rawObj);
|
const observer = getObserver(rawObj);
|
||||||
observer.useProp(key);
|
observer.useProp(key);
|
||||||
|
|
||||||
const value = rawObj.get(keyProxy);
|
const value = rawObj.get(keyProxy);
|
||||||
|
|
||||||
// 对于value也需要进一步代理
|
return getValOrProxy(key, false, value, rawObj, listener, listeners);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
function get(rawObj: T, key: any, receiver: any): any {
|
||||||
|
return baseGetFun(rawObj, key, receiver, listeners, handler, 'Map', getFun);
|
||||||
|
}
|
||||||
|
|
||||||
// Map的set方法
|
// Map的set方法
|
||||||
function set(
|
function set(rawObj: T, key: any, value: any): any {
|
||||||
rawObj: {
|
let keyProxy;
|
||||||
get: (key: any) => any;
|
let oldValue;
|
||||||
set: (key: any, value: any) => any;
|
if (baseHasFun(rawObj, key, keyProxies)) {
|
||||||
has: (key: any) => boolean;
|
keyProxy = keyProxies.has(key) ? keyProxies.get(key) : key;
|
||||||
entries: () => [any, any][];
|
oldValue = rawObj.get(keyProxy);
|
||||||
},
|
if (isSame(value, oldValue)) {
|
||||||
key: any,
|
return;
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
observer.setProp(key, mutation);
|
|
||||||
oldData = [...Array.from(rawObj.entries())];
|
|
||||||
} else {
|
} else {
|
||||||
// NEW VALUE
|
keyProxy = getValOrProxy('keyChange', false, key, rawObj, listener, listeners);
|
||||||
const keyProxy = createProxy(
|
keyProxies.set(key, keyProxy);
|
||||||
key,
|
|
||||||
{
|
|
||||||
current: change => {
|
|
||||||
// KEY CHANGE
|
|
||||||
if (!change.parents) change.parents = [];
|
|
||||||
change.parents.push(rawObj);
|
|
||||||
const mutation = resolveMutation(
|
|
||||||
{ ...rawObj, ['_keyChange']: change.mutation.from },
|
|
||||||
{ ...rawObj, ['_keyChange']: change.mutation.to }
|
|
||||||
);
|
|
||||||
listener.current({ ...change, mutation });
|
|
||||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hookObserverMap.get(rawObj)
|
|
||||||
);
|
|
||||||
proxies.set(key, keyProxy);
|
|
||||||
|
|
||||||
rawObj.set(keyProxy, value);
|
|
||||||
const observer = getObserver(rawObj);
|
|
||||||
const mutation = resolveMutation(
|
|
||||||
{
|
|
||||||
_type: 'Map',
|
|
||||||
entries: oldData,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_type: 'Map',
|
|
||||||
entries: Array.from(rawObj.entries()),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
|
||||||
|
|
||||||
if (observer.watchers?.[key]) {
|
|
||||||
observer.watchers[key].forEach(cb => {
|
|
||||||
cb(key, null, value, mutation);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
observer.setProp(key, mutation);
|
|
||||||
oldData = [...Array.from(rawObj.entries())];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldValues = [...Array.from(rawObj.entries())];
|
||||||
|
|
||||||
|
rawObj.set(keyProxy, value);
|
||||||
|
const observer = getObserver(rawObj);
|
||||||
|
const mutation = resolveMutation(
|
||||||
|
{
|
||||||
|
_type: 'Map',
|
||||||
|
entries: oldValues,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_type: 'Map',
|
||||||
|
entries: Array.from(rawObj.entries()),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
observer.setProp(KeyTypes.COLLECTION_CHANGE, mutation);
|
||||||
|
observer.setProp(key, mutation, oldValue, value);
|
||||||
|
|
||||||
return rawObj;
|
return rawObj;
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
function has(rawObj: { has: (any) => boolean }, key: any): boolean {
|
function has(rawObj: T, key: any): boolean {
|
||||||
const observer = getObserver(rawObj);
|
const observer = getObserver(rawObj);
|
||||||
observer.useProp(key);
|
observer.useProp(key);
|
||||||
if (rawObj.has(key)) {
|
|
||||||
return true;
|
return baseHasFun(rawObj, key, keyProxies);
|
||||||
}
|
|
||||||
return proxies.has(key);
|
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
function clear(rawObj: { size: number; clear: () => void; entries: () => [any, any][] }) {
|
|
||||||
const oldSize = rawObj.size;
|
|
||||||
rawObj.clear();
|
|
||||||
|
|
||||||
if (oldSize > 0) {
|
function clear(rawObj: T) {
|
||||||
const observer = getObserver(rawObj);
|
baseClearFun(rawObj, keyProxies, 'Map');
|
||||||
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);
|
function deleteFun(rawObj: T, key: any) {
|
||||||
const mutation = resolveMutation(
|
return baseDeleteFun(rawObj, key, 'Map', keyProxies);
|
||||||
{
|
|
||||||
_type: 'Map',
|
|
||||||
entries: oldData,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_type: 'Map',
|
|
||||||
entries: Array.from(rawObj.entries()),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
observer.setProp(key, mutation);
|
|
||||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
|
||||||
|
|
||||||
oldData = [...Array.from(rawObj.entries())];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
function forEach(
|
function forEach(rawObj: T, callback: (valProxy: any, keyProxy: any, rawObj: any) => void) {
|
||||||
rawObj: { forEach: (callback: (value: any, key: any) => void) => void },
|
baseForEach(rawObj, callback, listener, listeners);
|
||||||
callback: (valProxy: any, keyProxy: any, rawObj: any) => void
|
}
|
||||||
) {
|
|
||||||
|
function wrapIterator(rawObj: T, rawIt: IterableIterator<any>, type: IteratorTypes) {
|
||||||
const observer = getObserver(rawObj);
|
const observer = getObserver(rawObj);
|
||||||
observer.useProp(COLLECTION_CHANGE);
|
const hookObserver = deepProxyMap.get(rawObj);
|
||||||
rawObj.forEach((value, key) => {
|
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||||
const keyProxy = createProxy(
|
|
||||||
value,
|
|
||||||
{
|
|
||||||
current: change => {
|
|
||||||
//KEY ATTRIBUTES CHANGED
|
|
||||||
if (!change.parents) change.parents = [];
|
|
||||||
change.parents.push(rawObj);
|
|
||||||
const mutation = resolveMutation(
|
|
||||||
{ ...rawObj, ['_keyChange']: change.mutation.from },
|
|
||||||
{ ...rawObj, ['_keyChange']: change.mutation.to }
|
|
||||||
);
|
|
||||||
listener.current({ ...change, mutation });
|
|
||||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hookObserverMap.get(rawObj)
|
|
||||||
);
|
|
||||||
const valProxy = createProxy(
|
|
||||||
key,
|
|
||||||
{
|
|
||||||
current: change => {
|
|
||||||
// VALUE ATTRIBUTE CHANGED
|
|
||||||
if (!change.parents) change.parents = [];
|
|
||||||
change.parents.push(rawObj);
|
|
||||||
const mutation = resolveMutation(
|
|
||||||
{ ...rawObj, key: change.mutation.from },
|
|
||||||
{ ...rawObj, key: change.mutation.to }
|
|
||||||
);
|
|
||||||
listener.current({ ...change, mutation });
|
|
||||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hookObserverMap.get(rawObj)
|
|
||||||
);
|
|
||||||
// 最后一个参数要返回代理对象
|
|
||||||
return callback(keyProxy, valProxy, rawObj);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
function wrapIterator(rawObj: Record<string, any>, rawIt: { next: () => { value: any; done: boolean } }, type) {
|
|
||||||
const observer = getObserver(rawObj);
|
|
||||||
const hookObserver = hookObserverMap.get(rawObj);
|
|
||||||
observer.useProp(COLLECTION_CHANGE);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
next() {
|
next() {
|
||||||
const { value, done } = rawIt.next();
|
const { value, done } = rawIt.next();
|
||||||
if (done) {
|
if (done) {
|
||||||
return {
|
return {
|
||||||
value: createProxy(
|
value: getValOrProxy(value, false, value, rawObj, listener, listeners),
|
||||||
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
|
|
||||||
),
|
|
||||||
done,
|
done,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
observer.useProp(COLLECTION_CHANGE);
|
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||||
let newVal;
|
let newVal;
|
||||||
if (type === 'entries') {
|
if (type === 'entries') {
|
||||||
//ENTRY CHANGED
|
//ENTRY CHANGED
|
||||||
|
@ -298,22 +154,7 @@ export function createMapProxy(
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
// SINGLE VALUE CHANGED
|
// SINGLE VALUE CHANGED
|
||||||
newVal = createProxy(
|
newVal = getValOrProxy(type === 'keys' ? 'key' : 'value', false, value, rawObj, listener, listeners);
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { value: newVal, done };
|
return { value: newVal, done };
|
||||||
|
@ -325,29 +166,19 @@ export function createMapProxy(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
function keys(rawObj: T) {
|
||||||
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 } } }) {
|
|
||||||
return wrapIterator(rawObj, rawObj.keys(), 'keys');
|
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');
|
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');
|
return wrapIterator(rawObj, rawObj.entries(), 'entries');
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
function forOf(rawObj: {
|
function forOf(rawObj: T) {
|
||||||
entries: () => { next: () => { value: any; done: boolean } };
|
|
||||||
values: () => { next: () => { value: any; done: boolean } };
|
|
||||||
}) {
|
|
||||||
return wrapIterator(rawObj, rawObj.entries(), 'entries');
|
return wrapIterator(rawObj, rawObj.entries(), 'entries');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,65 +196,7 @@ export function createMapProxy(
|
||||||
[typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf,
|
[typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf,
|
||||||
};
|
};
|
||||||
|
|
||||||
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
registerListener(rawObj, listener, listeners);
|
||||||
if (key === 'size') {
|
|
||||||
return size(rawObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'get') {
|
return new Proxy(rawObj as ObjectType, handler);
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,118 +13,30 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isSame, resolveMutation } from '../../CommonUtils';
|
import { registerListener } from './HandlerUtils';
|
||||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
import { baseSetFun, baseGetFun, has, deleteProperty, ownKeys } from './BaseObjectHandler';
|
||||||
import { OBSERVER_KEY, RAW_VALUE } from '../../Constants';
|
import { CurrentListener, KeyType, Listeners, ObjectType } from '../../types/ProxyTypes';
|
||||||
import { isPanelActive } from '../../devtools';
|
|
||||||
|
|
||||||
function set(rawObj: Record<string, any>, key: string, value: any, receiver: any): boolean {
|
export function createObjectProxy<T extends ObjectType>(
|
||||||
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>>(
|
|
||||||
rawObj: T,
|
rawObj: T,
|
||||||
listener: { current: (...args) => any },
|
listener: CurrentListener,
|
||||||
singleLevel = false
|
singleLevel = false
|
||||||
): ProxyHandler<T> {
|
): ProxyHandler<T> {
|
||||||
let listeners = [] as ((...args) => void)[];
|
const listeners: Listeners = [];
|
||||||
|
|
||||||
function get(rawObj: Record<string, any>, key: string | symbol, receiver: any): any {
|
function get(rawObj: T, key: KeyType, receiver: any): any {
|
||||||
// The observer object of symbol ('_inulaObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep.
|
return baseGetFun(rawObj, key, receiver, listener, listeners, singleLevel);
|
||||||
if (key === OBSERVER_KEY) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = getObserver(rawObj);
|
|
||||||
|
|
||||||
if (key === 'watch') {
|
|
||||||
return (prop, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
|
||||||
if (!observer.watchers[prop]) {
|
|
||||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
|
||||||
}
|
|
||||||
observer.watchers[prop].push(handler);
|
|
||||||
return () => {
|
|
||||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'addListener') {
|
|
||||||
return listener => {
|
|
||||||
listeners.push(listener);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'removeListener') {
|
|
||||||
return listener => {
|
|
||||||
listeners = listeners.filter(item => item != listener);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === RAW_VALUE) {
|
|
||||||
return rawObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
observer.useProp(key);
|
|
||||||
|
|
||||||
const value = Reflect.get(rawObj, key, receiver);
|
|
||||||
|
|
||||||
// 对于prototype不做代理
|
|
||||||
if (key !== 'prototype') {
|
|
||||||
// 对于value也需要进一步代理
|
|
||||||
const valProxy = singleLevel
|
|
||||||
? value
|
|
||||||
: createProxy(
|
|
||||||
value,
|
|
||||||
{
|
|
||||||
current: change => {
|
|
||||||
if (!change.parents) change.parents = [];
|
|
||||||
change.parents.push(rawObj);
|
|
||||||
const mutation = resolveMutation(
|
|
||||||
{ ...rawObj, [key]: change.mutation.from },
|
|
||||||
{ ...rawObj, [key]: change.mutation.to }
|
|
||||||
);
|
|
||||||
listener.current({ ...change, mutation });
|
|
||||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hookObserverMap.get(rawObj)
|
|
||||||
);
|
|
||||||
|
|
||||||
return valProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxy = new Proxy(rawObj, {
|
const handler = {
|
||||||
get,
|
get,
|
||||||
set,
|
set: baseSetFun,
|
||||||
});
|
deleteProperty,
|
||||||
|
has,
|
||||||
|
ownKeys,
|
||||||
|
};
|
||||||
|
|
||||||
getObserver(rawObj).addListener(change => {
|
registerListener(rawObj, listener, listeners);
|
||||||
if (!change.parents) change.parents = [];
|
|
||||||
change.parents.push(rawObj);
|
|
||||||
listener.current(change);
|
|
||||||
listeners.forEach(lst => lst(change));
|
|
||||||
});
|
|
||||||
|
|
||||||
return proxy;
|
return new Proxy(rawObj, handler);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,130 +13,43 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { resolveMutation } from '../../CommonUtils';
|
import { getObserver } from '../ProxyHandler';
|
||||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
import { KeyTypes } from '../../Constants';
|
||||||
import { RAW_VALUE } 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>>(
|
function add(rawObj: T, value: any): Record<string, any> {
|
||||||
rawObj: T,
|
return baseAddFunOfSet(rawObj, value, listener, listeners, 'Set', valProxies);
|
||||||
listener: { current: (...args) => any },
|
|
||||||
hookObserver = true
|
|
||||||
): ProxyHandler<T> {
|
|
||||||
let listeners: ((mutation) => Record<string, any>)[] = [];
|
|
||||||
const proxies = new WeakMap();
|
|
||||||
|
|
||||||
// Set的add方法
|
|
||||||
function add(
|
|
||||||
rawObj: { add: (any) => void; has: (any) => boolean; values: () => any[] },
|
|
||||||
value: any
|
|
||||||
): Record<string, any> {
|
|
||||||
if (!rawObj.has(proxies.get(value))) {
|
|
||||||
const proxy = createProxy(
|
|
||||||
value,
|
|
||||||
{
|
|
||||||
current: change => {
|
|
||||||
if (!change.parents) change.parents = [];
|
|
||||||
change.parents.push(rawObj);
|
|
||||||
const mutation = resolveMutation(
|
|
||||||
{ ...rawObj, valueChange: change.mutation.from },
|
|
||||||
{ ...rawObj, valueChange: change.mutation.to }
|
|
||||||
);
|
|
||||||
listener.current({
|
|
||||||
...change,
|
|
||||||
mutation,
|
|
||||||
});
|
|
||||||
listeners.forEach(lst =>
|
|
||||||
lst({
|
|
||||||
...change,
|
|
||||||
mutation,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hookObserverMap.get(rawObj)
|
|
||||||
);
|
|
||||||
const oldValues = Array.from(rawObj.values());
|
|
||||||
|
|
||||||
proxies.set(value, proxy);
|
|
||||||
|
|
||||||
rawObj.add(proxies.get(value));
|
|
||||||
|
|
||||||
const observer = getObserver(rawObj);
|
|
||||||
const mutation = resolveMutation(
|
|
||||||
{
|
|
||||||
_type: 'Set',
|
|
||||||
values: oldValues,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_type: 'Set',
|
|
||||||
values: Array.from(rawObj.values()),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
observer.setProp(value, mutation);
|
|
||||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawObj;
|
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
function has(rawObj: { has: (string) => boolean }, value: any): boolean {
|
function has(rawObj: T, value: any): boolean {
|
||||||
const observer = getObserver(rawObj);
|
const observer = getObserver(rawObj);
|
||||||
observer.useProp(value);
|
observer.useProp(value);
|
||||||
|
|
||||||
return rawObj.has(proxies.get(value));
|
return baseHasFun(rawObj, value, valProxies);
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
function deleteFun(
|
|
||||||
rawObj: { has: (key: any) => boolean; delete: (value: any) => void; values: () => any[] },
|
|
||||||
value: any
|
|
||||||
) {
|
|
||||||
const val = rawObj.has(proxies.get(value)) ? proxies.get(value) : value;
|
|
||||||
if (rawObj.has(val)) {
|
|
||||||
const oldValues = Array.from(rawObj.values());
|
|
||||||
rawObj.delete(val);
|
|
||||||
|
|
||||||
proxies.delete(value);
|
function deleteFun(rawObj: T, value: any) {
|
||||||
|
return baseDeleteFun(rawObj, value, 'Set', valProxies);
|
||||||
const observer = getObserver(rawObj);
|
|
||||||
const mutation = resolveMutation(
|
|
||||||
{
|
|
||||||
_type: 'Set',
|
|
||||||
values: oldValues,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_type: 'Set',
|
|
||||||
values: Array.from(rawObj.values()),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
observer.setProp(value, mutation);
|
|
||||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
function clear(rawObj: { size: number; clear: () => void }) {
|
|
||||||
const oldSize = rawObj.size;
|
|
||||||
rawObj.clear();
|
|
||||||
|
|
||||||
if (oldSize > 0) {
|
function clear(rawObj: T) {
|
||||||
const observer = getObserver(rawObj);
|
baseClearFun(rawObj, valProxies, 'Set');
|
||||||
observer.allChange();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
function size(rawObj: { size: number }) {
|
|
||||||
const observer = getObserver(rawObj);
|
|
||||||
observer.useProp(COLLECTION_CHANGE);
|
|
||||||
return rawObj.size;
|
|
||||||
}
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
const handler = {
|
const handler = {
|
||||||
get,
|
get,
|
||||||
add,
|
add,
|
||||||
|
@ -151,86 +64,21 @@ export function createSetProxy<T extends Record<string, any>>(
|
||||||
[typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf,
|
[typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf,
|
||||||
};
|
};
|
||||||
|
|
||||||
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
function get(rawObj: T, key: any, receiver: any): any {
|
||||||
if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
return baseGetFun(rawObj, key, receiver, listeners, handler, 'Set');
|
||||||
const value = Reflect.get(handler, key, receiver);
|
|
||||||
return value.bind(null, rawObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'size') {
|
|
||||||
return size(rawObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'addListener') {
|
|
||||||
return listener => {
|
|
||||||
listeners.push(listener);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'removeListener') {
|
|
||||||
return listener => {
|
|
||||||
listeners = listeners.filter(item => item != listener);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (key === 'watch') {
|
|
||||||
const observer = getObserver(rawObj);
|
|
||||||
|
|
||||||
return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
|
||||||
if (!observer.watchers[prop]) {
|
|
||||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
|
||||||
}
|
|
||||||
observer.watchers[prop].push(handler);
|
|
||||||
return () => {
|
|
||||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === RAW_VALUE) {
|
|
||||||
return rawObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Reflect.get(rawObj, key, receiver);
|
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
function wrapIterator(rawObj: Record<string, any>, rawIt: { next: () => { value: any; done: boolean } }) {
|
function wrapIterator(rawObj: T, rawIt: IterableIterator<any>) {
|
||||||
const observer = getObserver(rawObj);
|
const observer = getObserver(rawObj);
|
||||||
const hookObserver = hookObserverMap.get(rawObj);
|
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||||
observer.useProp(COLLECTION_CHANGE);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
next() {
|
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();
|
const { value, done } = rawIt.next();
|
||||||
if (done) {
|
if (!done) {
|
||||||
return { value: createProxy(value, currentListener, hookObserver), done };
|
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||||
}
|
}
|
||||||
|
return { value: getValOrProxy('valueChange', false, value, rawObj, listener, listeners), done };
|
||||||
observer.useProp(COLLECTION_CHANGE);
|
|
||||||
|
|
||||||
const newVal = createProxy(value, currentListener, hookObserver);
|
|
||||||
|
|
||||||
return { value: newVal, done };
|
|
||||||
},
|
},
|
||||||
// 判断Symbol类型,兼容IE
|
// 判断Symbol类型,兼容IE
|
||||||
[typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']() {
|
[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());
|
return wrapIterator(rawObj, rawObj.keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) {
|
function values(rawObj: T) {
|
||||||
return wrapIterator(rawObj, rawObj.values());
|
return wrapIterator(rawObj, rawObj.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) {
|
function entries(rawObj: T) {
|
||||||
return wrapIterator(rawObj, rawObj.entries());
|
return wrapIterator(rawObj, rawObj.entries());
|
||||||
}
|
}
|
||||||
|
|
||||||
function forOf(rawObj: {
|
function forOf(rawObj: T) {
|
||||||
entries: () => { next: () => { value: any; done: boolean } };
|
return wrapIterator(rawObj, rawObj.values());
|
||||||
values: () => { next: () => { value: any; done: boolean } };
|
|
||||||
}) {
|
|
||||||
const iterator = rawObj.values();
|
|
||||||
return wrapIterator(rawObj, iterator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function forEach(
|
function forEach(rawObj: T, callback: (valProxy: any, keyProxy: any, rawObj: any) => void) {
|
||||||
rawObj: { forEach: (callback: (value: any, key: any) => void) => void },
|
baseForEach(rawObj, callback, listener, listeners);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
getObserver(rawObj).addListener(change => {
|
registerListener(rawObj, listener, listeners);
|
||||||
if (!change.parents) change.parents = [];
|
|
||||||
change.parents.push(rawObj);
|
return new Proxy(rawObj, handler);
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,197 +13,64 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
import { getObserver } from '../ProxyHandler';
|
||||||
import { isSame } from '../../CommonUtils';
|
import { isSame } from '../../CommonUtils';
|
||||||
import { resolveMutation } from '../../CommonUtils';
|
import { resolveMutation } from '../../CommonUtils';
|
||||||
import { isPanelActive } from '../../devtools';
|
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<T extends WeakMap<any, any>>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
|
||||||
|
const listeners: Listeners = [];
|
||||||
export function createWeakMapProxy(
|
|
||||||
rawObj: Record<string, any>,
|
|
||||||
listener: { current: (...args) => any },
|
|
||||||
hookObserver = true
|
|
||||||
): Record<string, any> {
|
|
||||||
let listeners: ((mutation) => Record<string, any>)[] = [];
|
|
||||||
|
|
||||||
const handler = {
|
const handler = {
|
||||||
get,
|
get,
|
||||||
set,
|
set,
|
||||||
add,
|
|
||||||
delete: deleteFun,
|
delete: deleteFun,
|
||||||
clear,
|
|
||||||
has,
|
has,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getFun(rawObj: { get: (key: any) => any }, key: any) {
|
function getFun(rawObj: T, key: any) {
|
||||||
const observer = getObserver(rawObj);
|
const observer = getObserver(rawObj);
|
||||||
observer.useProp(key);
|
observer.useProp(key);
|
||||||
|
|
||||||
const value = rawObj.get(key);
|
const value = rawObj.get(key);
|
||||||
// 对于value也需要进一步代理
|
// 对于value也需要进一步代理
|
||||||
const valProxy = createProxy(
|
return getValOrProxy(key, false, value, rawObj, listener, listeners);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
function get(rawObj: T, key: any, receiver: any): any {
|
||||||
if (key === 'get') {
|
return baseGetFun(rawObj, key, receiver, listeners, handler, 'WeakMap', getFun);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map的set方法
|
function set(rawObj: T, key: any, value: any) {
|
||||||
function set(
|
|
||||||
rawObj: { get: (key: any) => any; set: (key: any, value: any) => any; has: (key: any) => boolean },
|
|
||||||
key: any,
|
|
||||||
value: any
|
|
||||||
) {
|
|
||||||
const oldValue = rawObj.get(key);
|
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);
|
const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : resolveMutation(null, rawObj);
|
||||||
|
|
||||||
if (valChange || !rawObj.has(key)) {
|
if (!isSame(value, oldValue)) {
|
||||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
observer.setProp(key, mutation, oldValue, value);
|
||||||
}
|
|
||||||
|
|
||||||
if (valChange) {
|
|
||||||
if (observer.watchers?.[key]) {
|
|
||||||
observer.watchers[key].forEach(cb => {
|
|
||||||
cb(key, oldValue, newValue, mutation);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
observer.setProp(key, mutation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rawObj;
|
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);
|
function has(rawObj: T, key: any): boolean {
|
||||||
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 {
|
|
||||||
const observer = getObserver(rawObj);
|
const observer = getObserver(rawObj);
|
||||||
observer.useProp(key);
|
observer.useProp(key);
|
||||||
|
|
||||||
return rawObj.has(key);
|
return rawObj.has(key);
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
function clear(rawObj: { size: number; clear: () => void }) {
|
|
||||||
const oldSize = rawObj.size;
|
|
||||||
rawObj.clear();
|
|
||||||
|
|
||||||
if (oldSize > 0) {
|
function deleteFun(rawObj: T, key: any) {
|
||||||
const observer = getObserver(rawObj);
|
return baseDeleteFun(rawObj, key, 'WeakMap');
|
||||||
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);
|
registerListener(rawObj, listener, listeners);
|
||||||
const mutation = isPanelActive()
|
|
||||||
? resolveMutation(oldCollection, rawObj)
|
|
||||||
: { mutation: true, from: null, to: rawObj };
|
|
||||||
observer.setProp(key, mutation);
|
|
||||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
|
||||||
|
|
||||||
return true;
|
return new Proxy(rawObj as ObjectType, handler as any);
|
||||||
}
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,16 +13,14 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { resolveMutation } from '../../CommonUtils';
|
import { registerListener } from './HandlerUtils';
|
||||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
import { CurrentListener, Listeners } from '../../types/ProxyTypes';
|
||||||
import { RAW_VALUE } from '../../Constants';
|
import { baseGetFun, baseAddFunOfSet, baseHasFun, baseDeleteFun } from './BaseCollectionHandler';
|
||||||
|
import { getObserver } from '../ProxyHandler';
|
||||||
|
|
||||||
export function createWeakSetProxy<T extends Record<string, any>>(
|
export function createWeakSetProxy<T extends WeakSet<any>>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
|
||||||
rawObj: T,
|
const listeners: Listeners = [];
|
||||||
listener: { current: (...args) => any },
|
// 因为rawObj是WeakSet类型,里面存放的是proxy对象,所以需要一个map来存放真实的对象和proxy对象的映射关系
|
||||||
hookObserver = true
|
|
||||||
): ProxyHandler<T> {
|
|
||||||
let listeners: ((mutation) => Record<string, any>)[] = [];
|
|
||||||
const proxies = new WeakMap();
|
const proxies = new WeakMap();
|
||||||
|
|
||||||
const handler = {
|
const handler = {
|
||||||
|
@ -32,112 +30,26 @@ export function createWeakSetProxy<T extends Record<string, any>>(
|
||||||
has,
|
has,
|
||||||
};
|
};
|
||||||
|
|
||||||
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
function get(rawObj: T, key: any, receiver: any): any {
|
||||||
if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
return baseGetFun(rawObj, key, receiver, listeners, handler, 'WeakSet');
|
||||||
const value = Reflect.get(handler, key, receiver);
|
|
||||||
return value.bind(null, rawObj);
|
|
||||||
}
|
|
||||||
if (key === 'addListener') {
|
|
||||||
return listener => {
|
|
||||||
listeners.push(listener);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'removeListener') {
|
|
||||||
return listener => {
|
|
||||||
listeners = listeners.filter(item => item != listener);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (key === 'watch') {
|
|
||||||
const observer = getObserver(rawObj);
|
|
||||||
|
|
||||||
return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
|
||||||
if (!observer.watchers[prop]) {
|
|
||||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
|
||||||
}
|
|
||||||
observer.watchers[prop].push(handler);
|
|
||||||
return () => {
|
|
||||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === RAW_VALUE) {
|
|
||||||
return rawObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Reflect.get(rawObj, key, receiver);
|
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// 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);
|
function add(rawObj: T, value: any): Record<string, any> {
|
||||||
|
return baseAddFunOfSet(rawObj, value, listener, listeners, 'WeakSet', proxies);
|
||||||
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);
|
const observer = getObserver(rawObj);
|
||||||
observer.useProp(value);
|
observer.useProp(value);
|
||||||
|
|
||||||
return rawObj.has(proxies.get(value));
|
return baseHasFun(rawObj, value, proxies);
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
function deleteFun(rawObj: { has: (key: any) => boolean; delete: (value: any) => void }, value: any) {
|
|
||||||
if (rawObj.has(proxies.get(value))) {
|
|
||||||
rawObj.delete(proxies.get(value));
|
|
||||||
|
|
||||||
proxies.delete(value);
|
function deleteFun(rawObj: T, value: any) {
|
||||||
|
return baseDeleteFun(rawObj, value, 'WeakSet', proxies);
|
||||||
const observer = getObserver(rawObj);
|
|
||||||
const mutation = { mutation: true, from: value, to: rawObj };
|
|
||||||
|
|
||||||
observer.setProp(value, mutation);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
getObserver(rawObj).addListener(change => {
|
registerListener(rawObj, listener, listeners);
|
||||||
if (!change.parents) change.parents = [];
|
|
||||||
change.parents.push(rawObj);
|
return new Proxy(rawObj, handler);
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Observer } from '../proxy/Observer';
|
||||||
|
import { FnType } from '../types/ProxyTypes';
|
||||||
|
|
||||||
|
export type RContextFn = FnType;
|
||||||
|
export type RContextSet = Set<RContext>;
|
||||||
|
let currentRContext: RContext | null = null;
|
||||||
|
|
||||||
|
const reactiveContextStack: RContext[] = [];
|
||||||
|
|
||||||
|
export class RContext {
|
||||||
|
// 记录当前RContext的运行次数,用于解决在watchEffect中的数据发生变化导致RContext重新运行的问题
|
||||||
|
runs = 0;
|
||||||
|
|
||||||
|
fn: RContextFn;
|
||||||
|
|
||||||
|
// 记录该RContext中使用到的Reactive中的RContextSet
|
||||||
|
reactiveDependents: Set<RContextSet> | null = null;
|
||||||
|
|
||||||
|
constructor(fn: RContextFn) {
|
||||||
|
this.fn = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
cleanupRContext(this);
|
||||||
|
currentRContext = this;
|
||||||
|
reactiveContextStack.push(this);
|
||||||
|
|
||||||
|
return endRContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
const end = this.start();
|
||||||
|
try {
|
||||||
|
this.runs++;
|
||||||
|
this.fn();
|
||||||
|
} finally {
|
||||||
|
this.runs--;
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
cleanupRContext(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function endRContext() {
|
||||||
|
reactiveContextStack.pop();
|
||||||
|
currentRContext = reactiveContextStack[reactiveContextStack.length - 1] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除 RContext和响应式数据的绑定,双向清除
|
||||||
|
function cleanupRContext(rContext: RContext) {
|
||||||
|
if (rContext.reactiveDependents !== null) {
|
||||||
|
for (const usedRContexts of rContext.reactiveDependents) {
|
||||||
|
usedRContexts.delete(rContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
rContext.reactiveDependents.clear();
|
||||||
|
rContext.reactiveDependents = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addRContext(observer: Observer, prop: string | symbol) {
|
||||||
|
if (currentRContext !== null) {
|
||||||
|
if (!observer.rContexts[prop]) {
|
||||||
|
observer.rContexts[prop] = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!observer.rContexts[prop].has(currentRContext)) {
|
||||||
|
observer.rContexts[prop].add(currentRContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentRContext.reactiveDependents === null) {
|
||||||
|
currentRContext.reactiveDependents = new Set<RContextSet>();
|
||||||
|
}
|
||||||
|
currentRContext.reactiveDependents.add(observer.rContexts[prop]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createProxy } from '../proxy/ProxyHandler';
|
||||||
|
import { KeyTypes } from '../Constants';
|
||||||
|
import { ReactiveRet } from '../types/ReactiveTypes';
|
||||||
|
import { ObjectType } from '../types/ProxyTypes';
|
||||||
|
import { registerDestroyFunction } from '../store/StoreHandler';
|
||||||
|
import { useRef } from '../../renderer/hooks/HookExternal';
|
||||||
|
|
||||||
|
export function reactive<T extends ObjectType>(rawObj: T): ReactiveRet<T>;
|
||||||
|
export function reactive<T extends ObjectType>(rawObj: T) {
|
||||||
|
return createProxy(rawObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useReactive<T extends ObjectType>(rawObj: T): ReactiveRet<T>;
|
||||||
|
export function useReactive<T extends ObjectType>(rawObj: T) {
|
||||||
|
registerDestroyFunction();
|
||||||
|
const objRef = useRef(rawObj);
|
||||||
|
return createProxy(objRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRaw<T>(observed: T): T {
|
||||||
|
const raw = observed && observed[KeyTypes.RAW_VALUE];
|
||||||
|
return raw ? toRaw(raw) : observed;
|
||||||
|
}
|
|
@ -0,0 +1,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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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';
|
|
@ -16,7 +16,7 @@
|
||||||
import { useEffect, useRef } from '../../renderer/hooks/HookExternal';
|
import { useEffect, useRef } from '../../renderer/hooks/HookExternal';
|
||||||
import { getProcessingVNode } from '../../renderer/GlobalVar';
|
import { getProcessingVNode } from '../../renderer/GlobalVar';
|
||||||
import { createProxy } from '../proxy/ProxyHandler';
|
import { createProxy } from '../proxy/ProxyHandler';
|
||||||
import readonlyProxy from '../proxy/readonlyProxy';
|
import readonlyProxy from '../proxy/ReadonlyProxy';
|
||||||
import { Observer } from '../proxy/Observer';
|
import { Observer } from '../proxy/Observer';
|
||||||
import { FunctionComponent, ClassComponent } from '../../renderer/vnode/VNodeTags';
|
import { FunctionComponent, ClassComponent } from '../../renderer/vnode/VNodeTags';
|
||||||
import { isPromise } from '../CommonUtils';
|
import { isPromise } from '../CommonUtils';
|
||||||
|
@ -30,7 +30,7 @@ import type {
|
||||||
StoreObj,
|
StoreObj,
|
||||||
UserActions,
|
UserActions,
|
||||||
UserComputedValues,
|
UserComputedValues,
|
||||||
} from '../types';
|
} from '../types/StoreTypes';
|
||||||
import { VNode } from '../../renderer/vnode/VNode';
|
import { VNode } from '../../renderer/vnode/VNode';
|
||||||
import { devtools } from '../devtools';
|
import { devtools } from '../devtools';
|
||||||
import {
|
import {
|
||||||
|
@ -43,6 +43,7 @@ import {
|
||||||
SUBSCRIBED,
|
SUBSCRIBED,
|
||||||
UNSUBSCRIBED,
|
UNSUBSCRIBED,
|
||||||
} from '../devtools/constants';
|
} from '../devtools/constants';
|
||||||
|
import { CurrentListener } from '../types/ProxyTypes';
|
||||||
|
|
||||||
const idGenerator = {
|
const idGenerator = {
|
||||||
id: 0,
|
id: 0,
|
||||||
|
@ -52,14 +53,15 @@ const idGenerator = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const storeMap = new Map<string, StoreObj<any, any, any>>();
|
const storeMap = new Map<string, StoreObj<any, any, any>>();
|
||||||
|
const pendingMap = new WeakMap<any, boolean | number>();
|
||||||
|
|
||||||
// 通过该方法执行store.$queue中的action
|
// 通过该方法执行store.$queue中的action
|
||||||
function tryNextAction(storeObj, proxyObj, config, plannedActions) {
|
function tryNextAction(storeObj, proxyObj, config, plannedActions) {
|
||||||
if (!plannedActions.length) {
|
if (!plannedActions.length) {
|
||||||
if (proxyObj.$pending) {
|
if (pendingMap.get(proxyObj)) {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const duration = timestamp - proxyObj.$pending;
|
const duration = timestamp - (pendingMap.get(proxyObj) as number);
|
||||||
proxyObj.$pending = false;
|
pendingMap.set(proxyObj, false);
|
||||||
devtools.emit(QUEUE_FINISHED, {
|
devtools.emit(QUEUE_FINISHED, {
|
||||||
store: storeObj,
|
store: storeObj,
|
||||||
endedAt: timestamp,
|
endedAt: timestamp,
|
||||||
|
@ -99,7 +101,7 @@ export function clearVNodeObservers(vNode: VNode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册VNode销毁时的清理动作
|
// 注册VNode销毁时的清理动作
|
||||||
function registerDestroyFunction() {
|
export function registerDestroyFunction() {
|
||||||
const processingVNode = getProcessingVNode();
|
const processingVNode = getProcessingVNode();
|
||||||
|
|
||||||
// 获取不到当前运行的VNode,说明不在组件中运行,属于非法场景
|
// 获取不到当前运行的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 id = config.id || idGenerator.get('UNNAMED_STORE');
|
||||||
|
|
||||||
const listener = {
|
const listener: CurrentListener = {
|
||||||
current: listener => {},
|
current: listener => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const proxyObj = createProxy(config.state, listener, !config.options?.isReduxAdapter);
|
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 $a: Partial<StoreActions<S, A>> = {};
|
||||||
const $queue: Partial<StoreActions<S, A>> = {};
|
const $queue: Partial<StoreActions<S, A>> = {};
|
||||||
const $c: Partial<ComputedValues<S, C>> = {};
|
const $c: Partial<ComputedValues<S, C>> = {};
|
||||||
const storeObj = {
|
const storeObj = {
|
||||||
id,
|
id,
|
||||||
|
$state: proxyObj,
|
||||||
$s: proxyObj,
|
$s: proxyObj,
|
||||||
$a: $a as StoreActions<S, A>,
|
$a: $a as StoreActions<S, A>,
|
||||||
$c: $c as ComputedValues<S, C>,
|
$c: $c as ComputedValues<S, C>,
|
||||||
$queue: $queue as QueuedStoreActions<S, A>,
|
$queue: $queue as QueuedStoreActions<S, A>,
|
||||||
$config: config,
|
$config: config,
|
||||||
$listeners: [
|
$subscriptions: [
|
||||||
change => {
|
change => {
|
||||||
devtools.emit(STATE_CHANGE, {
|
devtools.emit(STATE_CHANGE, {
|
||||||
store: storeObj,
|
store: storeObj,
|
||||||
|
@ -185,16 +190,19 @@ export function createStore<S extends Record<string, any>, A extends UserActions
|
||||||
],
|
],
|
||||||
$subscribe: listener => {
|
$subscribe: listener => {
|
||||||
devtools.emit(SUBSCRIBED, { store: storeObj, listener });
|
devtools.emit(SUBSCRIBED, { store: storeObj, listener });
|
||||||
storeObj.$listeners.push(listener);
|
storeObj.$subscriptions.push(listener);
|
||||||
|
return () => {
|
||||||
|
storeObj.$unsubscribe(listener);
|
||||||
|
};
|
||||||
},
|
},
|
||||||
$unsubscribe: listener => {
|
$unsubscribe: listener => {
|
||||||
devtools.emit(UNSUBSCRIBED, { store: storeObj });
|
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>;
|
} as unknown as StoreObj<S, A, C>;
|
||||||
|
|
||||||
listener.current = (...args) => {
|
listener.current = (...args) => {
|
||||||
storeObj.$listeners.forEach(listener => listener(...args));
|
storeObj.$subscriptions.forEach(listener => listener(...args));
|
||||||
};
|
};
|
||||||
|
|
||||||
const plannedActions: PlannedAction<S, ActionFunction<S>>[] = [];
|
const plannedActions: PlannedAction<S, ActionFunction<S>>[] = [];
|
||||||
|
@ -214,11 +222,11 @@ export function createStore<S extends Record<string, any>, A extends UserActions
|
||||||
fromQueue: true,
|
fromQueue: true,
|
||||||
});
|
});
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (!proxyObj.$pending) {
|
if (!pendingMap.get(proxyObj)) {
|
||||||
proxyObj.$pending = Date.now();
|
pendingMap.set(proxyObj, Date.now());
|
||||||
devtools.emit(QUEUE_PENDING, {
|
devtools.emit(QUEUE_PENDING, {
|
||||||
store: storeObj,
|
store: storeObj,
|
||||||
startedAt: proxyObj.$pending,
|
startedAt: pendingMap.get(proxyObj),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = config.actions![action].bind(storeObj, proxyObj)(...payload);
|
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) {
|
if (config.computed) {
|
||||||
Object.keys(config.computed).forEach(computeKey => {
|
Object.keys(config.computed).forEach(computeKey => {
|
||||||
// 让store.$c[computeKey]可以访问到computed方法
|
const computeFn = config.computed![computeKey].bind(storeObj, readonlyProxy(proxyObj));
|
||||||
($c as any)[computeKey] = config.computed![computeKey].bind(storeObj, readonlyProxy(proxyObj));
|
// 让store.$c[computeKey]可以访问到computed的值
|
||||||
|
Object.defineProperty($c, computeKey, {
|
||||||
|
get: computeFn as () => any,
|
||||||
|
});
|
||||||
|
|
||||||
// 让store[computeKey]可以访问到computed的值
|
// 让store[computeKey]可以访问到computed的值
|
||||||
Object.defineProperty(storeObj, computeKey, {
|
Object.defineProperty(storeObj, computeKey, {
|
||||||
get: $c[computeKey] as () => any,
|
get: computeFn as () => any,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type KeyType = string | symbol;
|
||||||
|
export type ObjectType = Record<KeyType, any>;
|
||||||
|
export type FnType = () => any;
|
||||||
|
|
||||||
|
// Collection types
|
||||||
|
export type MapTypes = Map<any, any> | WeakMap<any, any>;
|
||||||
|
export type SetTypes = Set<any> | WeakSet<any>;
|
||||||
|
export type IterableTypes = Map<any, any> | Set<any>;
|
||||||
|
export type CollectionTypes = Map<any, any> | Set<any>;
|
||||||
|
export type CollectionStringTypes = 'Map' | 'WeakMap' | 'Set' | 'WeakSet';
|
||||||
|
|
||||||
|
export type Listener = (change: any) => void;
|
||||||
|
export type Listeners = Listener[];
|
||||||
|
export type CurrentListener = { current: Listener };
|
||||||
|
export type WatchHandler = (key?: KeyType, oldValue?: any, newValue?: any, mutation?: any) => void;
|
||||||
|
export type WatchFn = (prop: KeyType, handler?: WatchHandler) => void;
|
||||||
|
export type WatchCallback = (val: any, prevVal: any) => void;
|
||||||
|
|
||||||
|
type WatchProp<T> = T & { watch?: WatchFn };
|
||||||
|
export type AddWatchProp<T> =
|
||||||
|
T extends Map<infer K, infer V>
|
||||||
|
? WatchProp<Map<K, AddWatchProp<V>>>
|
||||||
|
: T extends WeakMap<infer K, infer V>
|
||||||
|
? WatchProp<WeakMap<K, AddWatchProp<V>>>
|
||||||
|
: T extends Set<infer U>
|
||||||
|
? WatchProp<Set<AddWatchProp<U>>>
|
||||||
|
: T extends WeakSet<infer U>
|
||||||
|
? WatchProp<WeakSet<AddWatchProp<U>>>
|
||||||
|
: T extends ObjectType
|
||||||
|
? WatchProp<{ [K in keyof T]: AddWatchProp<T[K]> }>
|
||||||
|
: T;
|
||||||
|
|
||||||
|
export interface IObserver {
|
||||||
|
watchers: {
|
||||||
|
[key: KeyType]: WatchHandler[];
|
||||||
|
};
|
||||||
|
|
||||||
|
useProp: (key: KeyType) => void;
|
||||||
|
|
||||||
|
addListener: (listener: Listener) => void;
|
||||||
|
|
||||||
|
removeListener: (listener: () => void) => void;
|
||||||
|
|
||||||
|
setProp: (key: KeyType, mutation: any, oldValue?: any, newValue?: any) => void;
|
||||||
|
|
||||||
|
triggerChangeListeners: (mutation: any) => void;
|
||||||
|
|
||||||
|
triggerUpdate: (vNode: any) => void;
|
||||||
|
|
||||||
|
allChange: () => void;
|
||||||
|
|
||||||
|
arrayLengthChange: (length: number) => void;
|
||||||
|
|
||||||
|
clearByVNode: (vNode: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Mutation<T = any> = {
|
||||||
|
mutation: boolean;
|
||||||
|
from: T;
|
||||||
|
to: T;
|
||||||
|
};
|
|
@ -0,0 +1,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]>;
|
||||||
|
};
|
|
@ -13,25 +13,13 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface IObserver {
|
import { AddWatchProp, Listener } from './ProxyTypes';
|
||||||
useProp: (key: string | symbol) => void;
|
|
||||||
|
|
||||||
addListener: (listener: (mutation: any) => void) => void;
|
export type StoreConfig<
|
||||||
|
S extends Record<string, unknown>,
|
||||||
removeListener: (listener: (mutation: any) => void) => void;
|
A extends UserActions<S>,
|
||||||
|
C extends UserComputedValues<S>,
|
||||||
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>> = {
|
|
||||||
id?: string;
|
id?: string;
|
||||||
state?: S;
|
state?: S;
|
||||||
actions?: A;
|
actions?: A;
|
||||||
|
@ -45,7 +33,11 @@ export type UserActions<S extends Record<string, unknown>> = {
|
||||||
[K: string]: ActionFunction<S>;
|
[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>> = {
|
export type StoreActions<S extends Record<string, unknown>, A extends UserActions<S>> = {
|
||||||
[K in keyof A]: Action<A[K], 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>>
|
...args: RemoveFirstFromTuple<Parameters<T>>
|
||||||
) => ReturnType<T>;
|
) => ReturnType<T>;
|
||||||
|
|
||||||
export type StoreObj<S extends Record<string, unknown>, A extends UserActions<S>, C extends UserComputedValues<S>> = {
|
export type StoreComputed<S extends Record<string, unknown>, C extends UserComputedValues<S>> = {
|
||||||
$s: 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>;
|
$a: StoreActions<S, A>;
|
||||||
$c: UserComputedValues<S>;
|
$c: StoreComputed<S, C>;
|
||||||
$queue: QueuedStoreActions<S, A>;
|
$queue: QueuedStoreActions<S, A>;
|
||||||
$listeners;
|
$subscriptions: Array<Listener>;
|
||||||
$subscribe: (listener: (mutation) => void) => void;
|
$subscribe: (listener: Listener) => void;
|
||||||
$unsubscribe: (listener: (mutation) => void) => void;
|
$unsubscribe: (listener: Listener) => void;
|
||||||
} & { [K in keyof S]: S[K] } & { [K in keyof A]: Action<A[K], S> } & { [K in keyof C]: ReturnType<C[K]> };
|
} & { [K in keyof AddWatchProp<S>]: AddWatchProp<S>[K] } & { [K in keyof A]: Action<A[K], S> } & {
|
||||||
|
[K in keyof C]: ReturnType<C[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
export type PlannedAction<S extends Record<string, unknown>, F extends ActionFunction<S>> = {
|
export type PlannedAction<S extends Record<string, unknown>, F extends ActionFunction<S>> = {
|
||||||
action: string;
|
action: string;
|
||||||
|
@ -79,11 +82,9 @@ type RemoveFirstFromTuple<T extends any[]> = T['length'] extends 0
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
export type UserComputedValues<S extends Record<string, unknown>> = {
|
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>> = (
|
export type AsyncAction<T extends ActionFunction<any>, S extends Record<string, unknown>> = (
|
||||||
this: StoreObj<S, any, any>,
|
this: StoreObj<S, any, any>,
|
||||||
...args: RemoveFirstFromTuple<Parameters<T>>
|
...args: RemoveFirstFromTuple<Parameters<T>>
|
|
@ -1,3 +1,4 @@
|
||||||
packages:
|
packages:
|
||||||
# all packages in direct subdirs of packages/
|
# all packages in direct subdirs of packages/
|
||||||
- 'packages/*'
|
- 'packages/*'
|
||||||
|
- 'packages/**/*'
|
||||||
|
|
Loading…
Reference in New Issue