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.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
|
@ -13,11 +13,4 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
export function watch(stateVariable: any, listener: (state: any) => void) {
|
||||
listener = listener.bind(null, stateVariable);
|
||||
stateVariable.addListener(listener);
|
||||
|
||||
return () => {
|
||||
stateVariable.removeListener(listener);
|
||||
};
|
||||
}
|
||||
export * from './pinia';
|
|
@ -0,0 +1,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、直接赋值', () => {
|
||||
let globalStore = useUserStore();
|
||||
const globalStore = useUserStore();
|
||||
function Child(props) {
|
||||
const userStore = useUserStore();
|
||||
|
||||
|
@ -146,8 +146,7 @@ describe('测试store中的Array', () => {
|
|||
expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// shift
|
||||
//@ts-ignore TODO:why is this argument here?
|
||||
globalStore.$s.persons.shift({ name: 'p0', age: 0 });
|
||||
globalStore.$s.persons.shift();
|
||||
expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: p2 p3');
|
||||
|
||||
// 赋值[2]
|
||||
|
@ -172,7 +171,7 @@ describe('测试store中的Array', () => {
|
|||
});
|
||||
|
||||
it('测试Array方法: forEach()', () => {
|
||||
let globalStore = useUserStore();
|
||||
const globalStore = useUserStore();
|
||||
function Child(props) {
|
||||
const userStore = useUserStore();
|
||||
|
||||
|
|
|
@ -315,4 +315,60 @@ describe('测试store中的Set', () => {
|
|||
});
|
||||
expect(container?.querySelector('#nameList')?.innerHTML).toBe('name list: ');
|
||||
});
|
||||
|
||||
it('测试Set方法,add string', () => {
|
||||
const useUserStore = createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
persons: new Set(),
|
||||
},
|
||||
})();
|
||||
|
||||
useUserStore.persons.add('p1');
|
||||
expect(useUserStore.persons.has('p1')).toBe(true);
|
||||
});
|
||||
|
||||
it('测试Set方法,add obj', () => {
|
||||
const useUserStore = createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
persons: new Set(),
|
||||
},
|
||||
})();
|
||||
|
||||
const obj = { a: 1 };
|
||||
useUserStore.persons.add(obj);
|
||||
expect(useUserStore.persons.has(obj)).toBe(true);
|
||||
});
|
||||
|
||||
it('测试Set方法,default value', () => {
|
||||
const obj = { b: 1 };
|
||||
const useUserStore = createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
persons: new Set(['p1', obj]),
|
||||
},
|
||||
})();
|
||||
|
||||
expect(useUserStore.persons.has('p1')).toBe(true);
|
||||
expect(useUserStore.persons.has(obj)).toBe(true);
|
||||
});
|
||||
|
||||
it('测试Set方法,watch', () => {
|
||||
const obj = { b: 1 };
|
||||
const useUserStore = createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
persons: new Set<any>(['p1', obj]),
|
||||
},
|
||||
})();
|
||||
|
||||
let dummy;
|
||||
const key = {};
|
||||
// effect(() => (dummy = cset.has(key)));
|
||||
useUserStore.persons.watch(() => (dummy = useUserStore.persons.has(key)));
|
||||
expect(dummy).toBe(undefined);
|
||||
useUserStore.persons.add(key);
|
||||
expect(dummy).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
//@ts-ignore
|
||||
import * as Inula from '../../../src/index';
|
||||
import * as LogUtils from '../../jest/logUtils';
|
||||
import { clearStore, createStore, useStore } from '../../../src/inulax/store/StoreHandler';
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('Dollar store access', () => {
|
|||
function App() {
|
||||
const logStore = useLogStore();
|
||||
|
||||
return <div id={RESULT_ID}>{logStore.$c.length()}</div>;
|
||||
return <div id={RESULT_ID}>{logStore.$c.length}</div>;
|
||||
}
|
||||
|
||||
Inula.render(<App />, container);
|
||||
|
@ -64,7 +64,7 @@ describe('Dollar store access', () => {
|
|||
>
|
||||
add
|
||||
</button>
|
||||
<p id={RESULT_ID}>{logStore.$c.length()}</p>
|
||||
<p id={RESULT_ID}>{logStore.$c.length}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,10 +13,11 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { createStore, watch } from '../../../src/index';
|
||||
import { createStore, watch, vueReactive } from '../../../src/index';
|
||||
const { watchEffect } = vueReactive;
|
||||
|
||||
describe('watch', () => {
|
||||
it('shouhld watch primitive state variable', async () => {
|
||||
it('should watch primitive state variable', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
variable: 'x',
|
||||
|
@ -38,7 +39,7 @@ describe('watch', () => {
|
|||
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
it('shouhld watch object variable', async () => {
|
||||
it('should watch object variable', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
variable: 'x',
|
||||
|
@ -60,7 +61,7 @@ describe('watch', () => {
|
|||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('shouhld watch array item', async () => {
|
||||
it('should watch array item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
arr: ['x'],
|
||||
|
@ -73,7 +74,7 @@ describe('watch', () => {
|
|||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
store.arr.watch('0', () => {
|
||||
store.$s.arr.watch('0', () => {
|
||||
counter++;
|
||||
});
|
||||
|
||||
|
@ -82,7 +83,7 @@ describe('watch', () => {
|
|||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('shouhld watch collection item', async () => {
|
||||
it('should watch collection item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
collection: new Map([['a', 'a']]),
|
||||
|
@ -141,4 +142,203 @@ describe('watch', () => {
|
|||
expect(counter1).toBe(3);
|
||||
expect(counterAll).toBe(6);
|
||||
});
|
||||
|
||||
it('should watch multiple variables independedntly', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
bool1: true,
|
||||
bool2: false,
|
||||
},
|
||||
actions: {
|
||||
toggle1: state => (state.bool1 = !state.bool1),
|
||||
toggle2: state => (state.bool2 = !state.bool2),
|
||||
},
|
||||
});
|
||||
|
||||
let counter1 = 0;
|
||||
let counterAll = 0;
|
||||
const store = useStore();
|
||||
|
||||
watch(store.$s, () => {
|
||||
counterAll++;
|
||||
});
|
||||
|
||||
store.$s.watch('bool1', () => {
|
||||
counter1++;
|
||||
});
|
||||
|
||||
store.toggle1();
|
||||
store.toggle1();
|
||||
|
||||
store.toggle2();
|
||||
|
||||
store.toggle1();
|
||||
|
||||
store.toggle2();
|
||||
store.toggle2();
|
||||
|
||||
expect(counter1).toBe(3);
|
||||
expect(counterAll).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('watchEffect', () => {
|
||||
it('should watchEffect obj item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
variable1: '1',
|
||||
variable2: '2',
|
||||
},
|
||||
actions: {
|
||||
change1: state => (state.variable1 = '11'),
|
||||
change2: state => (state.variable2 = '22'),
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
watchEffect(() => {
|
||||
store.variable1;
|
||||
counter++;
|
||||
});
|
||||
|
||||
expect(counter).toBe(1);
|
||||
|
||||
store.change1();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
|
||||
store.change2();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
|
||||
it('should watchEffect deep obj item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
obj: {
|
||||
a: 'x',
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
change: state => (state.obj.a = 'a'),
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
watchEffect(() => {
|
||||
store.obj.a;
|
||||
counter++;
|
||||
});
|
||||
|
||||
expect(counter).toBe(1);
|
||||
|
||||
store.change();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
|
||||
it('should watchEffect Map item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
collection: new Map([['a', 'a']]),
|
||||
},
|
||||
actions: {
|
||||
change: state => state.collection.set('a', 'x'),
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
watchEffect(() => {
|
||||
store.collection.get('a');
|
||||
counter++;
|
||||
});
|
||||
|
||||
expect(counter).toBe(1);
|
||||
|
||||
store.change();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
|
||||
it('should watchEffect Set item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
set: new Set(['a']),
|
||||
},
|
||||
actions: {
|
||||
change: state => state.set.delete('a'),
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
watchEffect(() => {
|
||||
store.set.has('a');
|
||||
counter++;
|
||||
});
|
||||
|
||||
expect(counter).toBe(1);
|
||||
|
||||
store.change();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
|
||||
it('should watchEffect WeakSet item', async () => {
|
||||
const obj = { a: 1 };
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
set: new WeakSet([obj]),
|
||||
},
|
||||
actions: {
|
||||
change: state => state.set.delete(obj),
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
watchEffect(() => {
|
||||
store.$s.set.has(obj);
|
||||
counter++;
|
||||
});
|
||||
|
||||
expect(counter).toBe(1);
|
||||
|
||||
store.change();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
|
||||
it('should watchEffect array item', async () => {
|
||||
const useStore = createStore({
|
||||
state: {
|
||||
arr: ['x'],
|
||||
},
|
||||
actions: {
|
||||
change: state => (state.arr[0] = 'a'),
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
let counter = 0;
|
||||
|
||||
watchEffect(() => {
|
||||
store.arr[0];
|
||||
counter++;
|
||||
});
|
||||
|
||||
expect(counter).toBe(1);
|
||||
|
||||
store.change();
|
||||
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { createStore, useStore } from '../../../src/index';
|
||||
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
||||
import { createStore } from '../../../src/index';
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
describe('Using deep variables', () => {
|
||||
it('should listen to object variable change', () => {
|
||||
|
@ -100,6 +100,7 @@ describe('Using deep variables', () => {
|
|||
const key = Array.from(testStore.data.keys())[0];
|
||||
|
||||
expect(testStore.data.has(key)).toBe(true);
|
||||
expect(testStore.data.has(data.key)).toBe(true);
|
||||
|
||||
testStore.data.set(data.key, data.value);
|
||||
testStore.data.set(data.key, data.value);
|
||||
|
@ -158,6 +159,8 @@ describe('Using deep variables', () => {
|
|||
|
||||
testStore.data.set(data.key, data.value);
|
||||
|
||||
expect(testStore.data.has(data.key)).toBe(true);
|
||||
|
||||
let counter = 0;
|
||||
testStore.$subscribe(mutation => {
|
||||
counter++;
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
*/
|
||||
|
||||
import { createProxy } from '../../../src/inulax/proxy/ProxyHandler';
|
||||
import { readonlyProxy } from '../../../src/inulax/proxy/readonlyProxy';
|
||||
import { readonlyProxy } from '../../../src/inulax/proxy/ReadonlyProxy';
|
||||
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
||||
|
||||
describe('Proxy', () => {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"homepage": "",
|
||||
"bugs": "",
|
||||
"license": "MulanPSL2",
|
||||
"main": "./build/index.js",
|
||||
"main": "./src/index.ts",
|
||||
"repository": {},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
|
@ -56,8 +56,11 @@ import {
|
|||
isPortal,
|
||||
} from './external/InulaIs';
|
||||
import { createStore, useStore, clearStore } from './inulax/store/StoreHandler';
|
||||
import { reactive, useReactive, toRaw } from './inulax/reactive/Reactive';
|
||||
import { ref, useReference, isRef, unref, shallowRef, toRef, toRefs } from './inulax/reactive/Ref';
|
||||
import * as reduxAdapter from './inulax/adapters/redux';
|
||||
import { watch } from './inulax/proxy/watch';
|
||||
import { watch, watchEffect, useWatch } from './inulax/reactive/Watch';
|
||||
import { computed, useComputed } from './inulax/reactive/Computed';
|
||||
import { act } from './external/TestUtil';
|
||||
|
||||
import {
|
||||
|
@ -70,7 +73,28 @@ import {
|
|||
} from './dom/DOMExternal';
|
||||
|
||||
import { syncUpdates as flushSync } from './renderer/TreeBuilder';
|
||||
import { toRaw } from './inulax/proxy/ProxyHandler';
|
||||
import { isReactive, isShallow, isReadonly } from './inulax/CommonUtils';
|
||||
|
||||
const vueReactive = {
|
||||
ref,
|
||||
useReference,
|
||||
isRef,
|
||||
unref,
|
||||
shallowRef,
|
||||
toRef,
|
||||
toRefs,
|
||||
reactive,
|
||||
useReactive,
|
||||
isReactive,
|
||||
isShallow,
|
||||
isReadonly,
|
||||
computed,
|
||||
useComputed,
|
||||
watchEffect,
|
||||
watch,
|
||||
useWatch,
|
||||
toRaw,
|
||||
};
|
||||
|
||||
const Inula = {
|
||||
Children,
|
||||
|
@ -122,9 +146,11 @@ const Inula = {
|
|||
Profiler,
|
||||
StrictMode,
|
||||
Suspense,
|
||||
// vue reactive api
|
||||
vueReactive,
|
||||
};
|
||||
|
||||
export const version = __VERSION__;
|
||||
export const version = '';
|
||||
export {
|
||||
Children,
|
||||
createRef,
|
||||
|
@ -161,7 +187,7 @@ export {
|
|||
clearStore,
|
||||
reduxAdapter,
|
||||
watch,
|
||||
toRaw,
|
||||
|
||||
// 兼容ReactIs
|
||||
isFragment,
|
||||
isElement,
|
||||
|
@ -178,7 +204,15 @@ export {
|
|||
Profiler,
|
||||
StrictMode,
|
||||
Suspense,
|
||||
// vue reactive api
|
||||
vueReactive,
|
||||
};
|
||||
|
||||
export * from './types';
|
||||
export * from './inulax/types/ReactiveTypes';
|
||||
export * from './inulax/types/ProxyTypes';
|
||||
export * from './inulax/types/StoreTypes';
|
||||
export * from './inulax/types/StoreTypes';
|
||||
export { ComputedImpl } from './inulax/reactive/Computed';
|
||||
|
||||
export default Inula;
|
||||
|
|
|
@ -13,9 +13,17 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { KeyTypes, ReactiveFlags } from './Constants';
|
||||
import { Mutation } from './types/ProxyTypes';
|
||||
|
||||
export function isObject(obj: any): boolean {
|
||||
const type = typeof obj;
|
||||
return (obj !== null || obj !== undefined) && (type === 'object' || type === 'function');
|
||||
return (obj !== null || obj !== undefined) && type === 'object';
|
||||
}
|
||||
|
||||
export function isPrimitive(obj: unknown): boolean {
|
||||
const type = typeof obj;
|
||||
return obj != null && type !== 'object' && type !== 'function';
|
||||
}
|
||||
|
||||
export function isSet(obj: any): boolean {
|
||||
|
@ -126,21 +134,24 @@ export function getDetailedType(val: any) {
|
|||
return typeof val;
|
||||
}
|
||||
|
||||
export function resolveMutation(from, to) {
|
||||
export function resolveMutation<T extends { length?: number; _type?: string; entries?: any; values?: any }>(
|
||||
from: T,
|
||||
to: T
|
||||
): Mutation<T> {
|
||||
if (getDetailedType(from) !== getDetailedType(to)) {
|
||||
return { mutation: true, from, to };
|
||||
}
|
||||
|
||||
switch (getDetailedType(from)) {
|
||||
case 'array': {
|
||||
const len = Math.max(from.length, to.length);
|
||||
const len = Math.max(from.length ?? 0, to.length ?? 0);
|
||||
const res: any[] = [];
|
||||
let found = false;
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (from.length <= i) {
|
||||
if ((from.length ?? 0) <= i) {
|
||||
res[i] = { mutation: true, to: to[i] };
|
||||
found = true;
|
||||
} else if (to.length <= i) {
|
||||
} else if ((to.length ?? 0) <= i) {
|
||||
res[i] = { mutation: true, from: from[i] };
|
||||
found = true;
|
||||
} else {
|
||||
|
@ -200,8 +211,14 @@ export function resolveMutation(from, to) {
|
|||
}
|
||||
}
|
||||
|
||||
export function omit(obj, ...attrs) {
|
||||
const res = { ...obj };
|
||||
attrs.forEach(attr => delete res[attr]);
|
||||
return res;
|
||||
export function isShallow(value: unknown): boolean {
|
||||
return !!(value && value[ReactiveFlags.IS_SHALLOW]);
|
||||
}
|
||||
|
||||
export function isReactive(value: unknown) {
|
||||
return !!(value && !!value[KeyTypes.RAW_VALUE]);
|
||||
}
|
||||
|
||||
export function isReadonly(value: unknown): boolean {
|
||||
return !!(value && value[ReactiveFlags.IS_READONLY]);
|
||||
}
|
||||
|
|
|
@ -15,4 +15,23 @@
|
|||
|
||||
export const OBSERVER_KEY = typeof Symbol === 'function' ? Symbol('_inulaObserver') : '_inulaObserver';
|
||||
|
||||
export const RAW_VALUE = '_rawValue';
|
||||
// 特殊处理的keys
|
||||
export enum KeyTypes {
|
||||
RAW_VALUE = '_rawValue',
|
||||
COLLECTION_CHANGE = '_collectionChange',
|
||||
GET = 'get',
|
||||
SIZE = 'size',
|
||||
VALUE = 'value',
|
||||
WATCH = 'watch',
|
||||
LENGTH = 'length',
|
||||
PROTOTYPE = 'prototype',
|
||||
HAS_OWN_PROPERTY = 'hasOwnProperty',
|
||||
ADD_LISTENER = 'addListener',
|
||||
REMOVE_LISTENER = 'removeListener',
|
||||
}
|
||||
|
||||
export enum ReactiveFlags {
|
||||
IS_SHALLOW = '_isShallow',
|
||||
IS_READONLY = '_isReadonly',
|
||||
IS_REF = '_isRef',
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import type { IObserver } from './Observer';
|
||||
import { IObserver } from '../types/ProxyTypes';
|
||||
|
||||
/**
|
||||
* 一个对象(对象、数组、集合)对应一个Observer
|
||||
|
@ -49,5 +49,7 @@ export class HooklessObserver implements IObserver {
|
|||
|
||||
allChange(): void {}
|
||||
|
||||
arrayLengthChange(): void {}
|
||||
|
||||
clearByVNode(vNode): void {}
|
||||
}
|
||||
|
|
|
@ -17,24 +17,10 @@ import { launchUpdateFromVNode } from '../../renderer/TreeBuilder';
|
|||
import { getProcessingVNode } from '../../renderer/GlobalVar';
|
||||
import { VNode } from '../../renderer/vnode/VNode';
|
||||
import { devtools } from '../devtools';
|
||||
import { KeyTypes } from '../Constants';
|
||||
import { addRContext, RContextSet } from '../reactive/RContext';
|
||||
|
||||
export interface IObserver {
|
||||
useProp: (key: string) => void;
|
||||
|
||||
addListener: (listener: () => void) => void;
|
||||
|
||||
removeListener: (listener: () => void) => void;
|
||||
|
||||
setProp: (key: string, mutation: any) => void;
|
||||
|
||||
triggerChangeListeners: (mutation: any) => void;
|
||||
|
||||
triggerUpdate: (vNode: any) => void;
|
||||
|
||||
allChange: () => void;
|
||||
|
||||
clearByVNode: (vNode: any) => void;
|
||||
}
|
||||
import { IObserver, Listener, Mutation } from '../types/ProxyTypes';
|
||||
|
||||
/**
|
||||
* 一个对象(对象、数组、集合)对应一个Observer
|
||||
|
@ -44,12 +30,25 @@ export class Observer implements IObserver {
|
|||
|
||||
keyVNodes = new Map();
|
||||
|
||||
listeners: ((mutation) => void)[] = [];
|
||||
listeners: Listener[] = [];
|
||||
|
||||
watchers = {} as { [key: string]: ((key: string, oldValue: any, newValue: any, mutation: any) => void)[] };
|
||||
watchers = {};
|
||||
|
||||
rContexts: {
|
||||
[key: string | symbol]: RContextSet;
|
||||
} = {};
|
||||
|
||||
// 对象的属性被使用时调用
|
||||
useProp(key: string | symbol): void {
|
||||
// 用于watchEffect的监听
|
||||
addRContext(this, key);
|
||||
|
||||
let vNodes = this.keyVNodes.get(key);
|
||||
if (!vNodes) {
|
||||
vNodes = new Set();
|
||||
this.keyVNodes.set(key, vNodes);
|
||||
}
|
||||
|
||||
const processingVNode = getProcessingVNode();
|
||||
if (processingVNode === null || !processingVNode.observers) {
|
||||
// 异常场景
|
||||
|
@ -60,11 +59,6 @@ export class Observer implements IObserver {
|
|||
processingVNode.observers.add(this);
|
||||
|
||||
// key -> vNodes,记录这个prop被哪些VNode使用了
|
||||
let vNodes = this.keyVNodes.get(key);
|
||||
if (!vNodes) {
|
||||
vNodes = new Set();
|
||||
this.keyVNodes.set(key, vNodes);
|
||||
}
|
||||
vNodes.add(processingVNode);
|
||||
|
||||
// vNode -> keys,记录这个VNode使用了哪些props
|
||||
|
@ -77,9 +71,9 @@ export class Observer implements IObserver {
|
|||
}
|
||||
|
||||
// 对象的属性被赋值时调用
|
||||
setProp(key: string | symbol, mutation: any): void {
|
||||
setProp(key: string | symbol, mutation: Mutation, oldValue?: any, newValue?: any): void {
|
||||
const vNodes = this.keyVNodes.get(key);
|
||||
//NOTE: using Set directly can lead to deadlock
|
||||
// NOTE: using Set directly can lead to deadlock
|
||||
const vNodeArray = Array.from(vNodes || []);
|
||||
vNodeArray.forEach((vNode: VNode) => {
|
||||
if (vNode.isStoreChange) {
|
||||
|
@ -92,8 +86,28 @@ export class Observer implements IObserver {
|
|||
this.triggerUpdate(vNode);
|
||||
});
|
||||
|
||||
// 这里需要过滤调COLLECTION_CHANGE,因为这个是集合的变化,不是具体的某个prop的变化,否则会重复触发
|
||||
if (key !== KeyTypes.COLLECTION_CHANGE) {
|
||||
// NOTE: mutations are different in dev and production.
|
||||
this.triggerChangeListeners({ mutation, vNodes });
|
||||
|
||||
// 值不一样,触发监听器
|
||||
if (this.watchers[key]) {
|
||||
this.watchers[key].forEach(cb => {
|
||||
cb(key, oldValue, newValue, mutation);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.rContexts[key]) {
|
||||
// clone this.rContexts[key] to avoid concurrent modification
|
||||
const rContexts = Array.from(this.rContexts[key]);
|
||||
rContexts.forEach(rContext => {
|
||||
if (!rContext.runs) {
|
||||
rContext.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
triggerUpdate(vNode: VNode): void {
|
||||
|
@ -101,11 +115,11 @@ export class Observer implements IObserver {
|
|||
launchUpdateFromVNode(vNode);
|
||||
}
|
||||
|
||||
addListener(listener: (mutation) => void): void {
|
||||
addListener(listener: Listener): void {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
removeListener(listener: (mutation) => void): void {
|
||||
removeListener(listener: Listener): void {
|
||||
this.listeners = this.listeners.filter(item => item != listener);
|
||||
}
|
||||
|
||||
|
@ -142,6 +156,17 @@ export class Observer implements IObserver {
|
|||
}
|
||||
}
|
||||
|
||||
arrayLengthChange(length: number): void {
|
||||
const keyIt = this.keyVNodes.keys();
|
||||
let keyItem = keyIt.next();
|
||||
while (!keyItem.done) {
|
||||
if (keyItem.value >= length) {
|
||||
this.setProp(keyItem.value, {});
|
||||
}
|
||||
keyItem = keyIt.next();
|
||||
}
|
||||
}
|
||||
|
||||
// 删除Observer中保存的这个VNode的关系数据
|
||||
clearByVNode(vNode: VNode): void {
|
||||
const keys = this.vNodeKeys.get(vNode);
|
||||
|
|
|
@ -19,65 +19,59 @@ import { HooklessObserver } from './HooklessObserver';
|
|||
import { isArray, isCollection, isObject } from '../CommonUtils';
|
||||
import { createArrayProxy } from './handlers/ArrayProxyHandler';
|
||||
import { createCollectionProxy } from './handlers/CollectionProxyHandler';
|
||||
import type { IObserver } from '../types';
|
||||
import { OBSERVER_KEY, RAW_VALUE } from '../Constants';
|
||||
import { IObserver, CurrentListener } from '../types/ProxyTypes';
|
||||
|
||||
// 保存rawObj -> Proxy
|
||||
const proxyMap = new WeakMap();
|
||||
// Save rawObj -> Proxy
|
||||
const proxyMap = new WeakMap<any, ProxyHandler<any>>();
|
||||
|
||||
export const hookObserverMap = new WeakMap();
|
||||
// Record whether rawObj has been deeply proxied
|
||||
export const deepProxyMap = new WeakMap<any, boolean>();
|
||||
|
||||
export function getObserver(rawObj: any): Observer {
|
||||
return rawObj[OBSERVER_KEY];
|
||||
// Use WeakMap to save rawObj -> Observer, without polluting the original object
|
||||
const rawObserverMap = new WeakMap<any, IObserver>();
|
||||
|
||||
export function getObserver(rawObj: any): IObserver {
|
||||
return rawObserverMap.get(rawObj) as IObserver;
|
||||
}
|
||||
|
||||
const setObserverKey =
|
||||
typeof OBSERVER_KEY === 'string'
|
||||
? (rawObj, observer) => {
|
||||
Object.defineProperty(rawObj, OBSERVER_KEY, {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
value: observer,
|
||||
});
|
||||
}
|
||||
: (rawObj, observer) => {
|
||||
rawObj[OBSERVER_KEY] = observer;
|
||||
};
|
||||
function setObserver(rawObj: any, observer: IObserver): void {
|
||||
rawObserverMap.set(rawObj, observer);
|
||||
}
|
||||
|
||||
export function createProxy(rawObj: any, listener: { current: (...args) => any }, isHookObserver = true): any {
|
||||
// 不是对象(是原始数据类型)不用代理
|
||||
export function createProxy(rawObj: any, listener?: CurrentListener, isDeepProxy = true): any {
|
||||
// No need to proxy if it's not an object (i.e., it's a primitive data type)
|
||||
if (!(rawObj && isObject(rawObj))) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
// 已代理过
|
||||
// Already exists
|
||||
const existProxy = proxyMap.get(rawObj);
|
||||
if (existProxy) {
|
||||
return existProxy;
|
||||
}
|
||||
|
||||
// Observer不需要代理
|
||||
// Observer does not need to be approached
|
||||
if (rawObj instanceof Observer) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
// 创建Observer
|
||||
let observer: IObserver = getObserver(rawObj);
|
||||
// Create Observer
|
||||
let observer = getObserver(rawObj);
|
||||
if (!observer) {
|
||||
observer = isHookObserver ? new Observer() : new HooklessObserver();
|
||||
setObserverKey(rawObj, observer);
|
||||
observer = (isDeepProxy ? new Observer() : new HooklessObserver()) as IObserver;
|
||||
setObserver(rawObj, observer);
|
||||
}
|
||||
|
||||
hookObserverMap.set(rawObj, isHookObserver);
|
||||
deepProxyMap.set(rawObj, isDeepProxy);
|
||||
|
||||
// 创建Proxy
|
||||
let proxyObj;
|
||||
if (!isHookObserver) {
|
||||
let proxyObj: ProxyHandler<any>;
|
||||
if (!isDeepProxy) {
|
||||
proxyObj = createObjectProxy(
|
||||
rawObj,
|
||||
{
|
||||
current: change => {
|
||||
listener.current(change);
|
||||
listener?.current(change);
|
||||
},
|
||||
},
|
||||
true
|
||||
|
@ -86,27 +80,23 @@ export function createProxy(rawObj: any, listener: { current: (...args) => any }
|
|||
// 数组
|
||||
proxyObj = createArrayProxy(rawObj as [], {
|
||||
current: change => {
|
||||
listener.current(change);
|
||||
listener?.current(change);
|
||||
},
|
||||
});
|
||||
} else if (isCollection(rawObj)) {
|
||||
// 集合
|
||||
proxyObj = createCollectionProxy(
|
||||
rawObj,
|
||||
{
|
||||
proxyObj = createCollectionProxy(rawObj, {
|
||||
current: change => {
|
||||
listener.current(change);
|
||||
listener?.current(change);
|
||||
},
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// 原生对象 或 函数
|
||||
proxyObj = createObjectProxy(
|
||||
rawObj,
|
||||
{
|
||||
current: change => {
|
||||
listener.current(change);
|
||||
listener?.current(change);
|
||||
},
|
||||
},
|
||||
false
|
||||
|
@ -118,7 +108,3 @@ export function createProxy(rawObj: any, listener: { current: (...args) => any }
|
|||
|
||||
return proxyObj;
|
||||
}
|
||||
|
||||
export function toRaw<T>(observed: T): T {
|
||||
return observed && observed[RAW_VALUE];
|
||||
}
|
||||
|
|
|
@ -13,150 +13,23 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { isSame, isValidIntegerKey } from '../../CommonUtils';
|
||||
import { resolveMutation } from '../../CommonUtils';
|
||||
import { isPanelActive } from '../../devtools';
|
||||
import { OBSERVER_KEY, RAW_VALUE } from '../../Constants';
|
||||
import { registerListener } from './HandlerUtils';
|
||||
import { baseSetFun, baseGetFun } from './BaseObjectHandler';
|
||||
import { CurrentListener, Listeners, ObjectType } from '../../types/ProxyTypes';
|
||||
|
||||
function set(rawObj: any[], key: string, value: any, receiver: any) {
|
||||
const oldValue = rawObj[key];
|
||||
const oldLength = rawObj.length;
|
||||
const newValue = value;
|
||||
export function createArrayProxy<T extends any[]>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
|
||||
const listeners: Listeners = [];
|
||||
|
||||
const oldArray = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
||||
|
||||
const ret = Reflect.set(rawObj, key, newValue, receiver);
|
||||
|
||||
const newLength = rawObj.length;
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
const mutation = isPanelActive() ? resolveMutation(oldArray, rawObj) : resolveMutation(null, rawObj);
|
||||
|
||||
if (!isSame(newValue, oldValue)) {
|
||||
// 值不一样,触发监听器
|
||||
if (observer.watchers?.[key]) {
|
||||
observer.watchers[key].forEach(cb => {
|
||||
cb(key, oldValue, newValue, mutation);
|
||||
});
|
||||
function get(rawObj: T, key: KeyType, receiver: any) {
|
||||
return baseGetFun(rawObj, key, receiver, listener, listeners);
|
||||
}
|
||||
|
||||
// 触发属性变化
|
||||
observer.setProp(key, mutation);
|
||||
}
|
||||
|
||||
if (oldLength !== newLength) {
|
||||
// 触发数组的大小变化
|
||||
observer.setProp('length', mutation);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function createArrayProxy(rawObj: any[], listener: { current: (...args) => any }): any[] {
|
||||
let listeners = [] as ((...args) => void)[];
|
||||
|
||||
function objectGet(rawObj: Record<string, any>, key: string | symbol, receiver: any, singleLevel = false): any {
|
||||
// The observer object of symbol ('_inulaObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep.
|
||||
if (key === OBSERVER_KEY) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
if (key === 'watch') {
|
||||
return (prop, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'addListener') {
|
||||
return listener => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'removeListener') {
|
||||
return listener => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
|
||||
observer.useProp(key);
|
||||
|
||||
const value = Reflect.get(rawObj, key, receiver);
|
||||
|
||||
// 对于prototype不做代理
|
||||
if (key !== 'prototype') {
|
||||
// 对于value也需要进一步代理
|
||||
const valProxy = singleLevel
|
||||
? value
|
||||
: createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [key]: change.mutation.from },
|
||||
{ ...rawObj, [key]: change.mutation.to }
|
||||
);
|
||||
listener.current(mutation);
|
||||
listeners.forEach(lst => lst(mutation));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
|
||||
return valProxy;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function get(rawObj: any[], key: string, receiver: any) {
|
||||
if (key === 'watch') {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (isValidIntegerKey(key) || key === 'length') {
|
||||
return objectGet(rawObj, key, receiver);
|
||||
}
|
||||
|
||||
if (key === RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
return Reflect.get(rawObj, key, receiver);
|
||||
}
|
||||
|
||||
const handle = {
|
||||
const handler = {
|
||||
get,
|
||||
set,
|
||||
set: baseSetFun,
|
||||
};
|
||||
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
registerListener(rawObj, listener, listeners);
|
||||
|
||||
return new Proxy(rawObj, handle);
|
||||
return new Proxy(rawObj as ObjectType, handler);
|
||||
}
|
||||
|
|
|
@ -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 { createMapProxy } from './MapProxy';
|
||||
|
||||
export function createCollectionProxy(
|
||||
rawObj: Record<string, unknown>,
|
||||
listener: { current: (...args) => any },
|
||||
hookObserver = true
|
||||
): ProxyHandler<Record<string, unknown>> {
|
||||
import { CurrentListener } from '../../types/ProxyTypes';
|
||||
|
||||
export function createCollectionProxy<T extends any>(rawObj: T, listener: CurrentListener) {
|
||||
if (isWeakSet(rawObj)) {
|
||||
return createWeakSetProxy(rawObj, listener, hookObserver);
|
||||
return createWeakSetProxy(rawObj as WeakSet<any>, listener);
|
||||
}
|
||||
|
||||
if (isSet(rawObj)) {
|
||||
return createSetProxy(rawObj, listener, hookObserver);
|
||||
return createSetProxy(rawObj as Set<any>, listener);
|
||||
}
|
||||
|
||||
if (isWeakMap(rawObj)) {
|
||||
return createWeakMapProxy(rawObj, listener, hookObserver);
|
||||
return createWeakMapProxy(rawObj as WeakMap<any, any>, listener);
|
||||
}
|
||||
return createMapProxy(rawObj, listener, hookObserver);
|
||||
|
||||
return createMapProxy(rawObj as Map<any, any>, listener);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
import { createProxy, getObserver, deepProxyMap } from '../ProxyHandler';
|
||||
import { isArray, isValidIntegerKey, resolveMutation } from '../../CommonUtils';
|
||||
import { isRef } from '../../reactive/Ref';
|
||||
import { CurrentListener, IObserver, Listeners, WatchFn, WatchHandler } from '../../types/ProxyTypes';
|
||||
|
||||
export const SET_WATCH_KEY = '_setWatchKey';
|
||||
|
||||
// 获取观察者函数
|
||||
export function getWatchFn(observer: IObserver): WatchFn {
|
||||
// 返回一个函数,该函数接受属性和处理程序作为参数
|
||||
return (prop: any, handler?: WatchHandler) => {
|
||||
// Set不需要指定prop
|
||||
if (typeof prop === 'function') {
|
||||
handler = prop;
|
||||
prop = SET_WATCH_KEY;
|
||||
}
|
||||
|
||||
// 观察指定的属性
|
||||
watchProp(observer, prop, handler as WatchHandler);
|
||||
};
|
||||
}
|
||||
|
||||
// 观察属性
|
||||
function watchProp(observer: IObserver, prop: any, handler: WatchHandler) {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [];
|
||||
}
|
||||
|
||||
// 将处理程序添加到观察者数组中
|
||||
if (!observer.watchers[prop].includes(handler)) {
|
||||
observer.watchers[prop].push(handler);
|
||||
}
|
||||
|
||||
return () => {
|
||||
// 从观察者数组中移除处理程序
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
}
|
||||
|
||||
export function triggerSetWatchers(observer: IObserver) {
|
||||
if (observer.watchers[SET_WATCH_KEY]) {
|
||||
observer.watchers[SET_WATCH_KEY].forEach(cb => {
|
||||
cb();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getValOrProxy(
|
||||
key: string | symbol,
|
||||
singleLevel: boolean,
|
||||
value: any,
|
||||
rawObj: Record<string, any>,
|
||||
listener: CurrentListener,
|
||||
listeners: Listeners
|
||||
): any {
|
||||
if (isRef(value)) {
|
||||
// ref unwrapping
|
||||
return isArray(rawObj) && isValidIntegerKey(key) ? value : value.value;
|
||||
}
|
||||
|
||||
// 对于value也需要进一步代理
|
||||
return singleLevel
|
||||
? value
|
||||
: createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [key]: change.mutation.from },
|
||||
{ ...rawObj, [key]: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
deepProxyMap.get(rawObj)
|
||||
);
|
||||
}
|
||||
|
||||
export function registerListener(rawObj: any, listener: CurrentListener, listeners: Listeners) {
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
}
|
|
@ -13,252 +13,108 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { createProxy, getObserver, deepProxyMap } from '../ProxyHandler';
|
||||
import { isSame } from '../../CommonUtils';
|
||||
import { resolveMutation } from '../../CommonUtils';
|
||||
import { isPanelActive } from '../../devtools';
|
||||
import { RAW_VALUE } from '../../Constants';
|
||||
import { KeyTypes } from '../../Constants';
|
||||
import { getValOrProxy, registerListener } from './HandlerUtils';
|
||||
import { baseDeleteFun, baseHasFun, baseForEach, baseGetFun, baseClearFun } from './BaseCollectionHandler';
|
||||
import { CurrentListener, Listeners, ObjectType } from '../../types/ProxyTypes';
|
||||
|
||||
const COLLECTION_CHANGE = '_collectionChange';
|
||||
type IteratorTypes = 'keys' | 'values' | 'entries';
|
||||
|
||||
export function createMapProxy(
|
||||
rawObj: Record<string, any>,
|
||||
listener: { current: (...args) => any },
|
||||
hookObserver = true
|
||||
): Record<string, any> {
|
||||
let listeners: ((mutation) => Record<string, any>)[] = [];
|
||||
let oldData: [any, any][] = [];
|
||||
const proxies = new Map();
|
||||
export function createMapProxy<T extends Map<any, any>>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
|
||||
const listeners: Listeners = [];
|
||||
// 场景:let obj = {}; map.set(obj, val);
|
||||
// 满足两个UT:1、map.has(Array.from(map.keys())[0])为true; 2、map.has(obj)为true;
|
||||
const keyProxies = new Map();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function getFun(rawObj: { get: (key: any) => any; has: (key: any) => boolean }, key: any): any {
|
||||
const keyProxy = rawObj.has(key) ? key : proxies.get(key);
|
||||
function getFun(rawObj: T, key: any): any {
|
||||
const keyProxy = rawObj.has(key) ? key : keyProxies.get(key);
|
||||
if (!keyProxy) return;
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(key);
|
||||
|
||||
const value = rawObj.get(keyProxy);
|
||||
|
||||
// 对于value也需要进一步代理
|
||||
const valProxy = createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [key]: change.mutation.from },
|
||||
{ ...rawObj, [key]: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
|
||||
return valProxy;
|
||||
return getValOrProxy(key, false, value, rawObj, listener, listeners);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function get(rawObj: T, key: any, receiver: any): any {
|
||||
return baseGetFun(rawObj, key, receiver, listeners, handler, 'Map', getFun);
|
||||
}
|
||||
|
||||
// Map的set方法
|
||||
function set(
|
||||
rawObj: {
|
||||
get: (key: any) => any;
|
||||
set: (key: any, value: any) => any;
|
||||
has: (key: any) => boolean;
|
||||
entries: () => [any, any][];
|
||||
},
|
||||
key: any,
|
||||
value: any
|
||||
): any {
|
||||
if (rawObj.has(key) || rawObj.has(proxies.get(key))) {
|
||||
// VALUE CHANGE (whole value for selected key is changed)
|
||||
const oldValue = rawObj.get(proxies.get(key));
|
||||
if (isSame(value, oldValue)) return;
|
||||
rawObj.set(proxies.get(key), value);
|
||||
const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : resolveMutation(null, rawObj);
|
||||
const observer = getObserver(rawObj);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
|
||||
if (observer.watchers[key]) {
|
||||
observer.watchers[key].forEach(cb => {
|
||||
cb(key, oldValue, value, mutation);
|
||||
});
|
||||
function set(rawObj: T, key: any, value: any): any {
|
||||
let keyProxy;
|
||||
let oldValue;
|
||||
if (baseHasFun(rawObj, key, keyProxies)) {
|
||||
keyProxy = keyProxies.has(key) ? keyProxies.get(key) : key;
|
||||
oldValue = rawObj.get(keyProxy);
|
||||
if (isSame(value, oldValue)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
keyProxy = getValOrProxy('keyChange', false, key, rawObj, listener, listeners);
|
||||
keyProxies.set(key, keyProxy);
|
||||
}
|
||||
|
||||
observer.setProp(key, mutation);
|
||||
oldData = [...Array.from(rawObj.entries())];
|
||||
} else {
|
||||
// NEW VALUE
|
||||
const keyProxy = createProxy(
|
||||
key,
|
||||
{
|
||||
current: change => {
|
||||
// KEY CHANGE
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, ['_keyChange']: change.mutation.from },
|
||||
{ ...rawObj, ['_keyChange']: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
proxies.set(key, keyProxy);
|
||||
const oldValues = [...Array.from(rawObj.entries())];
|
||||
|
||||
rawObj.set(keyProxy, value);
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{
|
||||
_type: 'Map',
|
||||
entries: oldData,
|
||||
entries: oldValues,
|
||||
},
|
||||
{
|
||||
_type: 'Map',
|
||||
entries: Array.from(rawObj.entries()),
|
||||
}
|
||||
);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
|
||||
if (observer.watchers?.[key]) {
|
||||
observer.watchers[key].forEach(cb => {
|
||||
cb(key, null, value, mutation);
|
||||
});
|
||||
}
|
||||
observer.setProp(key, mutation);
|
||||
oldData = [...Array.from(rawObj.entries())];
|
||||
}
|
||||
observer.setProp(KeyTypes.COLLECTION_CHANGE, mutation);
|
||||
observer.setProp(key, mutation, oldValue, value);
|
||||
|
||||
return rawObj;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function has(rawObj: { has: (any) => boolean }, key: any): boolean {
|
||||
|
||||
function has(rawObj: T, key: any): boolean {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(key);
|
||||
if (rawObj.has(key)) {
|
||||
return true;
|
||||
}
|
||||
return proxies.has(key);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function clear(rawObj: { size: number; clear: () => void; entries: () => [any, any][] }) {
|
||||
const oldSize = rawObj.size;
|
||||
rawObj.clear();
|
||||
|
||||
if (oldSize > 0) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.allChange();
|
||||
oldData = [...Array.from(rawObj.entries())];
|
||||
}
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function deleteFun(
|
||||
rawObj: { has: (key: any) => boolean; delete: (key: any) => void; entries: () => [any, any][] },
|
||||
key: any
|
||||
) {
|
||||
if (rawObj.has(key) || proxies.has(key)) {
|
||||
rawObj.delete(key || proxies.get(key));
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{
|
||||
_type: 'Map',
|
||||
entries: oldData,
|
||||
},
|
||||
{
|
||||
_type: 'Map',
|
||||
entries: Array.from(rawObj.entries()),
|
||||
}
|
||||
);
|
||||
observer.setProp(key, mutation);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
|
||||
oldData = [...Array.from(rawObj.entries())];
|
||||
return true;
|
||||
return baseHasFun(rawObj, key, keyProxies);
|
||||
}
|
||||
|
||||
return false;
|
||||
function clear(rawObj: T) {
|
||||
baseClearFun(rawObj, keyProxies, 'Map');
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function forEach(
|
||||
rawObj: { forEach: (callback: (value: any, key: any) => void) => void },
|
||||
callback: (valProxy: any, keyProxy: any, rawObj: any) => void
|
||||
) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
rawObj.forEach((value, key) => {
|
||||
const keyProxy = createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
//KEY ATTRIBUTES CHANGED
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, ['_keyChange']: change.mutation.from },
|
||||
{ ...rawObj, ['_keyChange']: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
const valProxy = createProxy(
|
||||
key,
|
||||
{
|
||||
current: change => {
|
||||
// VALUE ATTRIBUTE CHANGED
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, key: change.mutation.from },
|
||||
{ ...rawObj, key: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
// 最后一个参数要返回代理对象
|
||||
return callback(keyProxy, valProxy, rawObj);
|
||||
});
|
||||
|
||||
function deleteFun(rawObj: T, key: any) {
|
||||
return baseDeleteFun(rawObj, key, 'Map', keyProxies);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function wrapIterator(rawObj: Record<string, any>, rawIt: { next: () => { value: any; done: boolean } }, type) {
|
||||
|
||||
function forEach(rawObj: T, callback: (valProxy: any, keyProxy: any, rawObj: any) => void) {
|
||||
baseForEach(rawObj, callback, listener, listeners);
|
||||
}
|
||||
|
||||
function wrapIterator(rawObj: T, rawIt: IterableIterator<any>, type: IteratorTypes) {
|
||||
const observer = getObserver(rawObj);
|
||||
const hookObserver = hookObserverMap.get(rawObj);
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
const hookObserver = deepProxyMap.get(rawObj);
|
||||
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||
|
||||
return {
|
||||
next() {
|
||||
const { value, done } = rawIt.next();
|
||||
if (done) {
|
||||
return {
|
||||
value: createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [value]: change.mutation.from },
|
||||
{ ...rawObj, [value]: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserver
|
||||
),
|
||||
value: getValOrProxy(value, false, value, rawObj, listener, listeners),
|
||||
done,
|
||||
};
|
||||
}
|
||||
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||
let newVal;
|
||||
if (type === 'entries') {
|
||||
//ENTRY CHANGED
|
||||
|
@ -298,22 +154,7 @@ export function createMapProxy(
|
|||
];
|
||||
} else {
|
||||
// SINGLE VALUE CHANGED
|
||||
newVal = createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [type === 'keys' ? 'key' : 'value']: change.mutation.from },
|
||||
{ ...rawObj, [type === 'keys' ? 'key' : 'value']: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserver
|
||||
);
|
||||
newVal = getValOrProxy(type === 'keys' ? 'key' : 'value', false, value, rawObj, listener, listeners);
|
||||
}
|
||||
|
||||
return { value: newVal, done };
|
||||
|
@ -325,29 +166,19 @@ export function createMapProxy(
|
|||
};
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function size(rawObj: { size: number }) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
return rawObj.size;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function keys(rawObj: { keys: () => { next: () => { value: any; done: boolean } } }) {
|
||||
function keys(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.keys(), 'keys');
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) {
|
||||
|
||||
function values(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.values(), 'values');
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) {
|
||||
|
||||
function entries(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.entries(), 'entries');
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function forOf(rawObj: {
|
||||
entries: () => { next: () => { value: any; done: boolean } };
|
||||
values: () => { next: () => { value: any; done: boolean } };
|
||||
}) {
|
||||
|
||||
function forOf(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.entries(), 'entries');
|
||||
}
|
||||
|
||||
|
@ -365,65 +196,7 @@ export function createMapProxy(
|
|||
[typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf,
|
||||
};
|
||||
|
||||
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
||||
if (key === 'size') {
|
||||
return size(rawObj);
|
||||
}
|
||||
registerListener(rawObj, listener, listeners);
|
||||
|
||||
if (key === 'get') {
|
||||
return getFun.bind(null, rawObj);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
||||
const value = Reflect.get(handler, key, receiver);
|
||||
return value.bind(null, rawObj);
|
||||
}
|
||||
|
||||
if (key === 'watch') {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'addListener') {
|
||||
return listener => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'removeListener') {
|
||||
return listener => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
return Reflect.get(rawObj, key, receiver);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const boundHandler = {};
|
||||
Object.entries(handler).forEach(([id, val]) => {
|
||||
boundHandler[id] = (...args: any[]) => {
|
||||
return (val as any)(...args, hookObserver);
|
||||
};
|
||||
});
|
||||
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
return new Proxy(rawObj, { ...boundHandler });
|
||||
return new Proxy(rawObj as ObjectType, handler);
|
||||
}
|
||||
|
|
|
@ -13,118 +13,30 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { isSame, resolveMutation } from '../../CommonUtils';
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { OBSERVER_KEY, RAW_VALUE } from '../../Constants';
|
||||
import { isPanelActive } from '../../devtools';
|
||||
import { registerListener } from './HandlerUtils';
|
||||
import { baseSetFun, baseGetFun, has, deleteProperty, ownKeys } from './BaseObjectHandler';
|
||||
import { CurrentListener, KeyType, Listeners, ObjectType } from '../../types/ProxyTypes';
|
||||
|
||||
function set(rawObj: Record<string, any>, key: string, value: any, receiver: any): boolean {
|
||||
const oldObject = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
const oldValue = rawObj[key];
|
||||
const newValue = value;
|
||||
|
||||
const ret = Reflect.set(rawObj, key, newValue, receiver);
|
||||
const mutation = isPanelActive() ? resolveMutation(oldObject, rawObj) : resolveMutation(null, rawObj);
|
||||
|
||||
if (!isSame(newValue, oldValue)) {
|
||||
if (observer.watchers?.[key]) {
|
||||
observer.watchers[key].forEach(cb => {
|
||||
cb(key, oldValue, newValue, mutation);
|
||||
});
|
||||
}
|
||||
observer.setProp(key, mutation);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function createObjectProxy<T extends Record<string, any>>(
|
||||
export function createObjectProxy<T extends ObjectType>(
|
||||
rawObj: T,
|
||||
listener: { current: (...args) => any },
|
||||
listener: CurrentListener,
|
||||
singleLevel = false
|
||||
): ProxyHandler<T> {
|
||||
let listeners = [] as ((...args) => void)[];
|
||||
const listeners: Listeners = [];
|
||||
|
||||
function get(rawObj: Record<string, any>, key: string | symbol, receiver: any): any {
|
||||
// The observer object of symbol ('_inulaObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep.
|
||||
if (key === OBSERVER_KEY) {
|
||||
return undefined;
|
||||
function get(rawObj: T, key: KeyType, receiver: any): any {
|
||||
return baseGetFun(rawObj, key, receiver, listener, listeners, singleLevel);
|
||||
}
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
if (key === 'watch') {
|
||||
return (prop, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'addListener') {
|
||||
return listener => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'removeListener') {
|
||||
return listener => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
observer.useProp(key);
|
||||
|
||||
const value = Reflect.get(rawObj, key, receiver);
|
||||
|
||||
// 对于prototype不做代理
|
||||
if (key !== 'prototype') {
|
||||
// 对于value也需要进一步代理
|
||||
const valProxy = singleLevel
|
||||
? value
|
||||
: createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [key]: change.mutation.from },
|
||||
{ ...rawObj, [key]: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
|
||||
return valProxy;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
const proxy = new Proxy(rawObj, {
|
||||
const handler = {
|
||||
get,
|
||||
set,
|
||||
});
|
||||
set: baseSetFun,
|
||||
deleteProperty,
|
||||
has,
|
||||
ownKeys,
|
||||
};
|
||||
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
registerListener(rawObj, listener, listeners);
|
||||
|
||||
return proxy;
|
||||
return new Proxy(rawObj, handler);
|
||||
}
|
||||
|
|
|
@ -13,130 +13,43 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { resolveMutation } from '../../CommonUtils';
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { RAW_VALUE } from '../../Constants';
|
||||
import { getObserver } from '../ProxyHandler';
|
||||
import { KeyTypes } from '../../Constants';
|
||||
import { getValOrProxy, registerListener } from './HandlerUtils';
|
||||
import {
|
||||
baseGetFun,
|
||||
baseForEach,
|
||||
baseAddFunOfSet,
|
||||
baseHasFun,
|
||||
baseDeleteFun,
|
||||
baseClearFun,
|
||||
} from './BaseCollectionHandler';
|
||||
import { CurrentListener, Listeners } from '../../types/ProxyTypes';
|
||||
|
||||
const COLLECTION_CHANGE = '_collectionChange';
|
||||
export function createSetProxy<T extends Set<any>>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
|
||||
const listeners: Listeners = [];
|
||||
// 因为rawObj是Set类型,里面存放的是proxy对象,所以需要一个map来存放真实的对象和proxy对象的映射关系
|
||||
const valProxies = new Map();
|
||||
|
||||
export function createSetProxy<T extends Record<string, any>>(
|
||||
rawObj: T,
|
||||
listener: { current: (...args) => any },
|
||||
hookObserver = true
|
||||
): ProxyHandler<T> {
|
||||
let listeners: ((mutation) => Record<string, any>)[] = [];
|
||||
const proxies = new WeakMap();
|
||||
|
||||
// Set的add方法
|
||||
function add(
|
||||
rawObj: { add: (any) => void; has: (any) => boolean; values: () => any[] },
|
||||
value: any
|
||||
): Record<string, any> {
|
||||
if (!rawObj.has(proxies.get(value))) {
|
||||
const proxy = createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, valueChange: change.mutation.from },
|
||||
{ ...rawObj, valueChange: change.mutation.to }
|
||||
);
|
||||
listener.current({
|
||||
...change,
|
||||
mutation,
|
||||
});
|
||||
listeners.forEach(lst =>
|
||||
lst({
|
||||
...change,
|
||||
mutation,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
const oldValues = Array.from(rawObj.values());
|
||||
|
||||
proxies.set(value, proxy);
|
||||
|
||||
rawObj.add(proxies.get(value));
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{
|
||||
_type: 'Set',
|
||||
values: oldValues,
|
||||
},
|
||||
{
|
||||
_type: 'Set',
|
||||
values: Array.from(rawObj.values()),
|
||||
}
|
||||
);
|
||||
|
||||
observer.setProp(value, mutation);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
function add(rawObj: T, value: any): Record<string, any> {
|
||||
return baseAddFunOfSet(rawObj, value, listener, listeners, 'Set', valProxies);
|
||||
}
|
||||
|
||||
return rawObj;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function has(rawObj: { has: (string) => boolean }, value: any): boolean {
|
||||
function has(rawObj: T, value: any): boolean {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(value);
|
||||
|
||||
return rawObj.has(proxies.get(value));
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function deleteFun(
|
||||
rawObj: { has: (key: any) => boolean; delete: (value: any) => void; values: () => any[] },
|
||||
value: any
|
||||
) {
|
||||
const val = rawObj.has(proxies.get(value)) ? proxies.get(value) : value;
|
||||
if (rawObj.has(val)) {
|
||||
const oldValues = Array.from(rawObj.values());
|
||||
rawObj.delete(val);
|
||||
|
||||
proxies.delete(value);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{
|
||||
_type: 'Set',
|
||||
values: oldValues,
|
||||
},
|
||||
{
|
||||
_type: 'Set',
|
||||
values: Array.from(rawObj.values()),
|
||||
}
|
||||
);
|
||||
|
||||
observer.setProp(value, mutation);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
|
||||
return true;
|
||||
return baseHasFun(rawObj, value, valProxies);
|
||||
}
|
||||
|
||||
return false;
|
||||
function deleteFun(rawObj: T, value: any) {
|
||||
return baseDeleteFun(rawObj, value, 'Set', valProxies);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function clear(rawObj: { size: number; clear: () => void }) {
|
||||
const oldSize = rawObj.size;
|
||||
rawObj.clear();
|
||||
|
||||
if (oldSize > 0) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.allChange();
|
||||
function clear(rawObj: T) {
|
||||
baseClearFun(rawObj, valProxies, 'Set');
|
||||
}
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function size(rawObj: { size: number }) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
return rawObj.size;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const handler = {
|
||||
get,
|
||||
add,
|
||||
|
@ -151,86 +64,21 @@ export function createSetProxy<T extends Record<string, any>>(
|
|||
[typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']: forOf,
|
||||
};
|
||||
|
||||
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
||||
if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
||||
const value = Reflect.get(handler, key, receiver);
|
||||
return value.bind(null, rawObj);
|
||||
function get(rawObj: T, key: any, receiver: any): any {
|
||||
return baseGetFun(rawObj, key, receiver, listeners, handler, 'Set');
|
||||
}
|
||||
|
||||
if (key === 'size') {
|
||||
return size(rawObj);
|
||||
}
|
||||
|
||||
if (key === 'addListener') {
|
||||
return listener => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'removeListener') {
|
||||
return listener => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
if (key === 'watch') {
|
||||
function wrapIterator(rawObj: T, rawIt: IterableIterator<any>) {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (key === RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
return Reflect.get(rawObj, key, receiver);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function wrapIterator(rawObj: Record<string, any>, rawIt: { next: () => { value: any; done: boolean } }) {
|
||||
const observer = getObserver(rawObj);
|
||||
const hookObserver = hookObserverMap.get(rawObj);
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||
|
||||
return {
|
||||
next() {
|
||||
const currentListener = {
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, valueChange: change.mutation.from },
|
||||
{ ...rawObj, valueChange: change.mutation.to }
|
||||
);
|
||||
listener.current({
|
||||
...change,
|
||||
mutation,
|
||||
});
|
||||
listeners.forEach(lst =>
|
||||
lst({
|
||||
...change,
|
||||
mutation,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
const { value, done } = rawIt.next();
|
||||
if (done) {
|
||||
return { value: createProxy(value, currentListener, hookObserver), done };
|
||||
if (!done) {
|
||||
observer.useProp(KeyTypes.COLLECTION_CHANGE);
|
||||
}
|
||||
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
|
||||
const newVal = createProxy(value, currentListener, hookObserver);
|
||||
|
||||
return { value: newVal, done };
|
||||
return { value: getValOrProxy('valueChange', false, value, rawObj, listener, listeners), done };
|
||||
},
|
||||
// 判断Symbol类型,兼容IE
|
||||
[typeof Symbol === 'function' ? Symbol.iterator : '@@iterator']() {
|
||||
|
@ -239,72 +87,27 @@ export function createSetProxy<T extends Record<string, any>>(
|
|||
};
|
||||
}
|
||||
|
||||
function keys(rawObj: { keys: () => { next: () => { value: any; done: boolean } } }) {
|
||||
function keys(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.keys());
|
||||
}
|
||||
|
||||
function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) {
|
||||
function values(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.values());
|
||||
}
|
||||
|
||||
function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) {
|
||||
function entries(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.entries());
|
||||
}
|
||||
|
||||
function forOf(rawObj: {
|
||||
entries: () => { next: () => { value: any; done: boolean } };
|
||||
values: () => { next: () => { value: any; done: boolean } };
|
||||
}) {
|
||||
const iterator = rawObj.values();
|
||||
return wrapIterator(rawObj, iterator);
|
||||
function forOf(rawObj: T) {
|
||||
return wrapIterator(rawObj, rawObj.values());
|
||||
}
|
||||
|
||||
function forEach(
|
||||
rawObj: { forEach: (callback: (value: any, key: any) => void) => void },
|
||||
callback: (valProxy: any, keyProxy: any, rawObj: any) => void
|
||||
) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(COLLECTION_CHANGE);
|
||||
rawObj.forEach((value, key) => {
|
||||
const currentListener = {
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, valueChange: change.mutation.from },
|
||||
{ ...rawObj, valueChange: change.mutation.to }
|
||||
);
|
||||
listener.current({
|
||||
...change,
|
||||
mutation,
|
||||
});
|
||||
listeners.forEach(lst =>
|
||||
lst({
|
||||
...change,
|
||||
mutation,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
const valProxy = createProxy(value, currentListener, hookObserverMap.get(rawObj));
|
||||
const keyProxy = createProxy(key, currentListener, hookObserverMap.get(rawObj));
|
||||
// 最后一个参数要返回代理对象
|
||||
return callback(valProxy, keyProxy, rawObj);
|
||||
});
|
||||
function forEach(rawObj: T, callback: (valProxy: any, keyProxy: any, rawObj: any) => void) {
|
||||
baseForEach(rawObj, callback, listener, listeners);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const boundHandler = {};
|
||||
Object.entries(handler).forEach(([id, val]) => {
|
||||
boundHandler[id] = (...args: any[]) => {
|
||||
return (val as any)(...args, hookObserver);
|
||||
};
|
||||
});
|
||||
return new Proxy(rawObj, { ...boundHandler });
|
||||
|
||||
registerListener(rawObj, listener, listeners);
|
||||
|
||||
return new Proxy(rawObj, handler);
|
||||
}
|
||||
|
|
|
@ -13,197 +13,64 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { getObserver } from '../ProxyHandler';
|
||||
import { isSame } from '../../CommonUtils';
|
||||
import { resolveMutation } from '../../CommonUtils';
|
||||
import { isPanelActive } from '../../devtools';
|
||||
import { RAW_VALUE } from '../../Constants';
|
||||
import { getValOrProxy, registerListener } from './HandlerUtils';
|
||||
import { CurrentListener, Listeners, ObjectType } from '../../types/ProxyTypes';
|
||||
import { baseDeleteFun, baseGetFun } from './BaseCollectionHandler';
|
||||
|
||||
const COLLECTION_CHANGE = '_collectionChange';
|
||||
|
||||
export function createWeakMapProxy(
|
||||
rawObj: Record<string, any>,
|
||||
listener: { current: (...args) => any },
|
||||
hookObserver = true
|
||||
): Record<string, any> {
|
||||
let listeners: ((mutation) => Record<string, any>)[] = [];
|
||||
export function createWeakMapProxy<T extends WeakMap<any, any>>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
|
||||
const listeners: Listeners = [];
|
||||
|
||||
const handler = {
|
||||
get,
|
||||
set,
|
||||
add,
|
||||
delete: deleteFun,
|
||||
clear,
|
||||
has,
|
||||
};
|
||||
|
||||
function getFun(rawObj: { get: (key: any) => any }, key: any) {
|
||||
function getFun(rawObj: T, key: any) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(key);
|
||||
|
||||
const value = rawObj.get(key);
|
||||
// 对于value也需要进一步代理
|
||||
const valProxy = createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [key]: change.mutation.from },
|
||||
{ ...rawObj, [key]: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
|
||||
return valProxy;
|
||||
return getValOrProxy(key, false, value, rawObj, listener, listeners);
|
||||
}
|
||||
|
||||
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
||||
if (key === 'get') {
|
||||
return getFun.bind(null, rawObj);
|
||||
function get(rawObj: T, key: any, receiver: any): any {
|
||||
return baseGetFun(rawObj, key, receiver, listeners, handler, 'WeakMap', getFun);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
||||
const value = Reflect.get(handler, key, receiver);
|
||||
return value.bind(null, rawObj);
|
||||
}
|
||||
|
||||
if (key === 'watch') {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'addListener') {
|
||||
return listener => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'removeListener') {
|
||||
return listener => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
|
||||
if (key === RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
return Reflect.get(rawObj, key, receiver);
|
||||
}
|
||||
|
||||
// Map的set方法
|
||||
function set(
|
||||
rawObj: { get: (key: any) => any; set: (key: any, value: any) => any; has: (key: any) => boolean },
|
||||
key: any,
|
||||
value: any
|
||||
) {
|
||||
function set(rawObj: T, key: any, value: any) {
|
||||
const oldValue = rawObj.get(key);
|
||||
const newValue = value;
|
||||
rawObj.set(key, newValue);
|
||||
const valChange = !isSame(newValue, oldValue);
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
rawObj.set(key, value);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : resolveMutation(null, rawObj);
|
||||
|
||||
if (valChange || !rawObj.has(key)) {
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
}
|
||||
|
||||
if (valChange) {
|
||||
if (observer.watchers?.[key]) {
|
||||
observer.watchers[key].forEach(cb => {
|
||||
cb(key, oldValue, newValue, mutation);
|
||||
});
|
||||
}
|
||||
|
||||
observer.setProp(key, mutation);
|
||||
if (!isSame(value, oldValue)) {
|
||||
observer.setProp(key, mutation, oldValue, value);
|
||||
}
|
||||
|
||||
return rawObj;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Set的add方法
|
||||
function add(
|
||||
rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean },
|
||||
value: any
|
||||
): Record<string, any> {
|
||||
const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
||||
if (!rawObj.has(value)) {
|
||||
rawObj.add(value);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = isPanelActive()
|
||||
? resolveMutation(oldCollection, rawObj)
|
||||
: { mutation: true, from: null, to: rawObj };
|
||||
observer.setProp(value, mutation);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
}
|
||||
|
||||
return rawObj;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function has(rawObj: { has: (string) => boolean }, key: any): boolean {
|
||||
function has(rawObj: T, key: any): boolean {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(key);
|
||||
|
||||
return rawObj.has(key);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function clear(rawObj: { size: number; clear: () => void }) {
|
||||
const oldSize = rawObj.size;
|
||||
rawObj.clear();
|
||||
|
||||
if (oldSize > 0) {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.allChange();
|
||||
}
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) {
|
||||
const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
||||
if (rawObj.has(key)) {
|
||||
rawObj.delete(key);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = isPanelActive()
|
||||
? resolveMutation(oldCollection, rawObj)
|
||||
: { mutation: true, from: null, to: rawObj };
|
||||
observer.setProp(key, mutation);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
|
||||
return true;
|
||||
function deleteFun(rawObj: T, key: any) {
|
||||
return baseDeleteFun(rawObj, key, 'WeakMap');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const boundHandler = {};
|
||||
Object.entries(handler).forEach(([id, val]) => {
|
||||
boundHandler[id] = (...args: any[]) => {
|
||||
return (val as any)(...args, hookObserver);
|
||||
};
|
||||
});
|
||||
return new Proxy(rawObj, { ...boundHandler });
|
||||
registerListener(rawObj, listener, listeners);
|
||||
|
||||
return new Proxy(rawObj as ObjectType, handler as any);
|
||||
}
|
||||
|
|
|
@ -13,16 +13,14 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { resolveMutation } from '../../CommonUtils';
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { RAW_VALUE } from '../../Constants';
|
||||
import { registerListener } from './HandlerUtils';
|
||||
import { CurrentListener, Listeners } from '../../types/ProxyTypes';
|
||||
import { baseGetFun, baseAddFunOfSet, baseHasFun, baseDeleteFun } from './BaseCollectionHandler';
|
||||
import { getObserver } from '../ProxyHandler';
|
||||
|
||||
export function createWeakSetProxy<T extends Record<string, any>>(
|
||||
rawObj: T,
|
||||
listener: { current: (...args) => any },
|
||||
hookObserver = true
|
||||
): ProxyHandler<T> {
|
||||
let listeners: ((mutation) => Record<string, any>)[] = [];
|
||||
export function createWeakSetProxy<T extends WeakSet<any>>(rawObj: T, listener: CurrentListener): ProxyHandler<T> {
|
||||
const listeners: Listeners = [];
|
||||
// 因为rawObj是WeakSet类型,里面存放的是proxy对象,所以需要一个map来存放真实的对象和proxy对象的映射关系
|
||||
const proxies = new WeakMap();
|
||||
|
||||
const handler = {
|
||||
|
@ -32,112 +30,26 @@ export function createWeakSetProxy<T extends Record<string, any>>(
|
|||
has,
|
||||
};
|
||||
|
||||
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
||||
if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
||||
const value = Reflect.get(handler, key, receiver);
|
||||
return value.bind(null, rawObj);
|
||||
}
|
||||
if (key === 'addListener') {
|
||||
return listener => {
|
||||
listeners.push(listener);
|
||||
};
|
||||
function get(rawObj: T, key: any, receiver: any): any {
|
||||
return baseGetFun(rawObj, key, receiver, listeners, handler, 'WeakSet');
|
||||
}
|
||||
|
||||
if (key === 'removeListener') {
|
||||
return listener => {
|
||||
listeners = listeners.filter(item => item != listener);
|
||||
};
|
||||
}
|
||||
if (key === 'watch') {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => {
|
||||
if (!observer.watchers[prop]) {
|
||||
observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[];
|
||||
}
|
||||
observer.watchers[prop].push(handler);
|
||||
return () => {
|
||||
observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler);
|
||||
};
|
||||
};
|
||||
function add(rawObj: T, value: any): Record<string, any> {
|
||||
return baseAddFunOfSet(rawObj, value, listener, listeners, 'WeakSet', proxies);
|
||||
}
|
||||
|
||||
if (key === RAW_VALUE) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
return Reflect.get(rawObj, key, receiver);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Set的add方法
|
||||
function add(rawObj: { add: (any) => void; has: (any) => boolean }, value: any): Record<string, any> {
|
||||
if (!rawObj.has(proxies.get(value))) {
|
||||
const proxy = createProxy(
|
||||
value,
|
||||
{
|
||||
current: change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
const mutation = resolveMutation(
|
||||
{ ...rawObj, [value]: change.mutation.from },
|
||||
{ ...rawObj, [value]: change.mutation.to }
|
||||
);
|
||||
listener.current({ ...change, mutation });
|
||||
listeners.forEach(lst => lst({ ...change, mutation }));
|
||||
},
|
||||
},
|
||||
hookObserverMap.get(rawObj)
|
||||
);
|
||||
|
||||
proxies.set(value, proxy);
|
||||
|
||||
rawObj.add(proxies.get(value));
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = { mutation: true, from: rawObj, to: value };
|
||||
|
||||
observer.setProp(value, mutation);
|
||||
}
|
||||
|
||||
return rawObj;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function has(rawObj: { has: (string) => boolean }, value: any): boolean {
|
||||
function has(rawObj: T, value: any): boolean {
|
||||
const observer = getObserver(rawObj);
|
||||
observer.useProp(value);
|
||||
|
||||
return rawObj.has(proxies.get(value));
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function deleteFun(rawObj: { has: (key: any) => boolean; delete: (value: any) => void }, value: any) {
|
||||
if (rawObj.has(proxies.get(value))) {
|
||||
rawObj.delete(proxies.get(value));
|
||||
|
||||
proxies.delete(value);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
const mutation = { mutation: true, from: value, to: rawObj };
|
||||
|
||||
observer.setProp(value, mutation);
|
||||
|
||||
return true;
|
||||
return baseHasFun(rawObj, value, proxies);
|
||||
}
|
||||
|
||||
return false;
|
||||
function deleteFun(rawObj: T, value: any) {
|
||||
return baseDeleteFun(rawObj, value, 'WeakSet', proxies);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
getObserver(rawObj).addListener(change => {
|
||||
if (!change.parents) change.parents = [];
|
||||
change.parents.push(rawObj);
|
||||
listener.current(change);
|
||||
listeners.forEach(lst => lst(change));
|
||||
});
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const boundHandler = {};
|
||||
Object.entries(handler).forEach(([id, val]) => {
|
||||
boundHandler[id] = (...args: any[]) => {
|
||||
return (val as any)(...args, hookObserver);
|
||||
};
|
||||
});
|
||||
return new Proxy(rawObj, { ...boundHandler });
|
||||
|
||||
registerListener(rawObj, listener, listeners);
|
||||
|
||||
return new Proxy(rawObj, handler);
|
||||
}
|
||||
|
|
|
@ -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 { getProcessingVNode } from '../../renderer/GlobalVar';
|
||||
import { createProxy } from '../proxy/ProxyHandler';
|
||||
import readonlyProxy from '../proxy/readonlyProxy';
|
||||
import readonlyProxy from '../proxy/ReadonlyProxy';
|
||||
import { Observer } from '../proxy/Observer';
|
||||
import { FunctionComponent, ClassComponent } from '../../renderer/vnode/VNodeTags';
|
||||
import { isPromise } from '../CommonUtils';
|
||||
|
@ -30,7 +30,7 @@ import type {
|
|||
StoreObj,
|
||||
UserActions,
|
||||
UserComputedValues,
|
||||
} from '../types';
|
||||
} from '../types/StoreTypes';
|
||||
import { VNode } from '../../renderer/vnode/VNode';
|
||||
import { devtools } from '../devtools';
|
||||
import {
|
||||
|
@ -43,6 +43,7 @@ import {
|
|||
SUBSCRIBED,
|
||||
UNSUBSCRIBED,
|
||||
} from '../devtools/constants';
|
||||
import { CurrentListener } from '../types/ProxyTypes';
|
||||
|
||||
const idGenerator = {
|
||||
id: 0,
|
||||
|
@ -52,14 +53,15 @@ const idGenerator = {
|
|||
};
|
||||
|
||||
const storeMap = new Map<string, StoreObj<any, any, any>>();
|
||||
const pendingMap = new WeakMap<any, boolean | number>();
|
||||
|
||||
// 通过该方法执行store.$queue中的action
|
||||
function tryNextAction(storeObj, proxyObj, config, plannedActions) {
|
||||
if (!plannedActions.length) {
|
||||
if (proxyObj.$pending) {
|
||||
if (pendingMap.get(proxyObj)) {
|
||||
const timestamp = Date.now();
|
||||
const duration = timestamp - proxyObj.$pending;
|
||||
proxyObj.$pending = false;
|
||||
const duration = timestamp - (pendingMap.get(proxyObj) as number);
|
||||
pendingMap.set(proxyObj, false);
|
||||
devtools.emit(QUEUE_FINISHED, {
|
||||
store: storeObj,
|
||||
endedAt: timestamp,
|
||||
|
@ -99,7 +101,7 @@ export function clearVNodeObservers(vNode: VNode) {
|
|||
}
|
||||
|
||||
// 注册VNode销毁时的清理动作
|
||||
function registerDestroyFunction() {
|
||||
export function registerDestroyFunction() {
|
||||
const processingVNode = getProcessingVNode();
|
||||
|
||||
// 获取不到当前运行的VNode,说明不在组件中运行,属于非法场景
|
||||
|
@ -157,25 +159,28 @@ export function createStore<S extends Record<string, any>, A extends UserActions
|
|||
|
||||
const id = config.id || idGenerator.get('UNNAMED_STORE');
|
||||
|
||||
const listener = {
|
||||
const listener: CurrentListener = {
|
||||
current: listener => {},
|
||||
};
|
||||
|
||||
const proxyObj = createProxy(config.state, listener, !config.options?.isReduxAdapter);
|
||||
|
||||
proxyObj.$pending = false;
|
||||
if (proxyObj !== undefined) {
|
||||
pendingMap.set(proxyObj, false);
|
||||
}
|
||||
|
||||
const $a: Partial<StoreActions<S, A>> = {};
|
||||
const $queue: Partial<StoreActions<S, A>> = {};
|
||||
const $c: Partial<ComputedValues<S, C>> = {};
|
||||
const storeObj = {
|
||||
id,
|
||||
$state: proxyObj,
|
||||
$s: proxyObj,
|
||||
$a: $a as StoreActions<S, A>,
|
||||
$c: $c as ComputedValues<S, C>,
|
||||
$queue: $queue as QueuedStoreActions<S, A>,
|
||||
$config: config,
|
||||
$listeners: [
|
||||
$subscriptions: [
|
||||
change => {
|
||||
devtools.emit(STATE_CHANGE, {
|
||||
store: storeObj,
|
||||
|
@ -185,16 +190,19 @@ export function createStore<S extends Record<string, any>, A extends UserActions
|
|||
],
|
||||
$subscribe: listener => {
|
||||
devtools.emit(SUBSCRIBED, { store: storeObj, listener });
|
||||
storeObj.$listeners.push(listener);
|
||||
storeObj.$subscriptions.push(listener);
|
||||
return () => {
|
||||
storeObj.$unsubscribe(listener);
|
||||
};
|
||||
},
|
||||
$unsubscribe: listener => {
|
||||
devtools.emit(UNSUBSCRIBED, { store: storeObj });
|
||||
storeObj.$listeners = storeObj.$listeners.filter(item => item != listener);
|
||||
storeObj.$subscriptions = storeObj.$subscriptions.filter(item => item != listener);
|
||||
},
|
||||
} as unknown as StoreObj<S, A, C>;
|
||||
|
||||
listener.current = (...args) => {
|
||||
storeObj.$listeners.forEach(listener => listener(...args));
|
||||
storeObj.$subscriptions.forEach(listener => listener(...args));
|
||||
};
|
||||
|
||||
const plannedActions: PlannedAction<S, ActionFunction<S>>[] = [];
|
||||
|
@ -214,11 +222,11 @@ export function createStore<S extends Record<string, any>, A extends UserActions
|
|||
fromQueue: true,
|
||||
});
|
||||
return new Promise(resolve => {
|
||||
if (!proxyObj.$pending) {
|
||||
proxyObj.$pending = Date.now();
|
||||
if (!pendingMap.get(proxyObj)) {
|
||||
pendingMap.set(proxyObj, Date.now());
|
||||
devtools.emit(QUEUE_PENDING, {
|
||||
store: storeObj,
|
||||
startedAt: proxyObj.$pending,
|
||||
startedAt: pendingMap.get(proxyObj),
|
||||
});
|
||||
|
||||
const result = config.actions![action].bind(storeObj, proxyObj)(...payload);
|
||||
|
@ -276,12 +284,15 @@ export function createStore<S extends Record<string, any>, A extends UserActions
|
|||
|
||||
if (config.computed) {
|
||||
Object.keys(config.computed).forEach(computeKey => {
|
||||
// 让store.$c[computeKey]可以访问到computed方法
|
||||
($c as any)[computeKey] = config.computed![computeKey].bind(storeObj, readonlyProxy(proxyObj));
|
||||
const computeFn = config.computed![computeKey].bind(storeObj, readonlyProxy(proxyObj));
|
||||
// 让store.$c[computeKey]可以访问到computed的值
|
||||
Object.defineProperty($c, computeKey, {
|
||||
get: computeFn as () => any,
|
||||
});
|
||||
|
||||
// 让store[computeKey]可以访问到computed的值
|
||||
Object.defineProperty(storeObj, computeKey, {
|
||||
get: $c[computeKey] as () => any,
|
||||
get: computeFn as () => any,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export interface IObserver {
|
||||
useProp: (key: string | symbol) => void;
|
||||
import { AddWatchProp, Listener } from './ProxyTypes';
|
||||
|
||||
addListener: (listener: (mutation: any) => void) => void;
|
||||
|
||||
removeListener: (listener: (mutation: any) => void) => void;
|
||||
|
||||
setProp: (key: string | symbol, mutation: any) => void;
|
||||
|
||||
triggerChangeListeners: (mutation: any) => void;
|
||||
|
||||
triggerUpdate: (vNode: any) => void;
|
||||
|
||||
allChange: () => void;
|
||||
|
||||
clearByVNode: (vNode: any) => void;
|
||||
}
|
||||
|
||||
export type StoreConfig<S extends Record<string, unknown>, A extends UserActions<S>, C extends UserComputedValues<S>> = {
|
||||
export type StoreConfig<
|
||||
S extends Record<string, unknown>,
|
||||
A extends UserActions<S>,
|
||||
C extends UserComputedValues<S>,
|
||||
> = {
|
||||
id?: string;
|
||||
state?: S;
|
||||
actions?: A;
|
||||
|
@ -45,7 +33,11 @@ export type UserActions<S extends Record<string, unknown>> = {
|
|||
[K: string]: ActionFunction<S>;
|
||||
};
|
||||
|
||||
export type ActionFunction<S extends Record<string, unknown>> = (this: StoreObj<S, any, any>, state: S, ...args: any[]) => any;
|
||||
export type ActionFunction<S extends Record<string, unknown>> = (
|
||||
this: StoreObj<S, any, any>,
|
||||
state: S,
|
||||
...args: any[]
|
||||
) => any;
|
||||
|
||||
export type StoreActions<S extends Record<string, unknown>, A extends UserActions<S>> = {
|
||||
[K in keyof A]: Action<A[K], S>;
|
||||
|
@ -56,15 +48,26 @@ type Action<T extends ActionFunction<any>, S extends Record<string, unknown>> =
|
|||
...args: RemoveFirstFromTuple<Parameters<T>>
|
||||
) => ReturnType<T>;
|
||||
|
||||
export type StoreObj<S extends Record<string, unknown>, A extends UserActions<S>, C extends UserComputedValues<S>> = {
|
||||
$s: S;
|
||||
export type StoreComputed<S extends Record<string, unknown>, C extends UserComputedValues<S>> = {
|
||||
[K in keyof C]: ReturnType<C[K]>;
|
||||
};
|
||||
|
||||
export type StoreObj<
|
||||
S extends Record<string, unknown> = Record<string, unknown>,
|
||||
A extends UserActions<S> = UserActions<S>,
|
||||
C extends UserComputedValues<S> = UserComputedValues<S>,
|
||||
> = {
|
||||
$s: AddWatchProp<S>;
|
||||
$state: AddWatchProp<S>;
|
||||
$a: StoreActions<S, A>;
|
||||
$c: UserComputedValues<S>;
|
||||
$c: StoreComputed<S, C>;
|
||||
$queue: QueuedStoreActions<S, A>;
|
||||
$listeners;
|
||||
$subscribe: (listener: (mutation) => void) => void;
|
||||
$unsubscribe: (listener: (mutation) => void) => void;
|
||||
} & { [K in keyof S]: S[K] } & { [K in keyof A]: Action<A[K], S> } & { [K in keyof C]: ReturnType<C[K]> };
|
||||
$subscriptions: Array<Listener>;
|
||||
$subscribe: (listener: Listener) => void;
|
||||
$unsubscribe: (listener: Listener) => void;
|
||||
} & { [K in keyof AddWatchProp<S>]: AddWatchProp<S>[K] } & { [K in keyof A]: Action<A[K], S> } & {
|
||||
[K in keyof C]: ReturnType<C[K]>;
|
||||
};
|
||||
|
||||
export type PlannedAction<S extends Record<string, unknown>, F extends ActionFunction<S>> = {
|
||||
action: string;
|
||||
|
@ -79,11 +82,9 @@ type RemoveFirstFromTuple<T extends any[]> = T['length'] extends 0
|
|||
: [];
|
||||
|
||||
export type UserComputedValues<S extends Record<string, unknown>> = {
|
||||
[K: string]: ComputedFunction<S>;
|
||||
[K: string]: (state: S) => any;
|
||||
};
|
||||
|
||||
type ComputedFunction<S extends Record<string, unknown>> = (state: S) => any;
|
||||
|
||||
export type AsyncAction<T extends ActionFunction<any>, S extends Record<string, unknown>> = (
|
||||
this: StoreObj<S, any, any>,
|
||||
...args: RemoveFirstFromTuple<Parameters<T>>
|
|
@ -1,3 +1,4 @@
|
|||
packages:
|
||||
# all packages in direct subdirs of packages/
|
||||
- 'packages/*'
|
||||
- 'packages/**/*'
|
||||
|
|
Loading…
Reference in New Issue