Match-id-4cc3574b83fb955dd2c7b7f3a5eb4beab4f22c94

This commit is contained in:
* 2023-09-26 15:06:19 +08:00
parent ead091a670
commit dfffef41e2
67 changed files with 7077 additions and 311 deletions

View File

@ -1,5 +1,5 @@
{
"name": "inulajs",
"name": "inulajs-reactive",
"description": "Inulajs is a JavaScript framework library.",
"keywords": [
"inulajs"

View File

@ -0,0 +1,162 @@
/*
* 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 Inula, { render, createRef, useReactive, For } from '../../../../../src/index';
import { beforeEach } from '@jest/globals';
const Row = ({ item }) => {
return <li id={item.id} key={item.id}>{item.name}</li>;
};
let rObj;
let ref;
let appFn;
let App;
let itemFn;
describe('测试 For 组件的新增', () => {
beforeEach(() => {
ref = createRef();
appFn = jest.fn();
itemFn = jest.fn();
App = () => {
const _rObj = useReactive({
items: [
{ id: 'id-1', name: 'p1' },
{ id: 'id-2', name: 'p2' },
],
});
rObj = _rObj;
appFn();
return (
<div ref={ref}>
<For each={_rObj.items}>
{item => {
itemFn();
return <Row item={item} />;
}}
</For>
</div>
);
};
});
it('通过 push 在后面添加1行', () => {
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(2);
// 在后面添加一行
rObj.items.push({ id: 'id-3', name: 'p3' });
items = container.querySelectorAll('li');
expect(items.length).toEqual(3);
expect(appFn).toHaveBeenCalledTimes(1);
// 第一次渲染执行2次push更新执行1次
expect(itemFn).toHaveBeenCalledTimes(3);
});
it('通过 unshift 在前面添加2行', () => {
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(2);
// 在前面添加2行
rObj.items.unshift({ id: 'id-3', name: 'p3' }, { id: 'id-4', name: 'p4' });
items = container.querySelectorAll('li');
expect(items.length).toEqual(4);
expect(appFn).toHaveBeenCalledTimes(1);
// 第一次渲染执行2次unshift更新执行2次
expect(itemFn).toHaveBeenCalledTimes(4);
});
it('通过 set 在后面添加1行', () => {
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(2);
// 在后面添加一行
rObj.items.set([
{ id: 'id-1', name: 'p1' },
{ id: 'id-2', name: 'p2' },
{ id: 'id-3', name: 'p3' },
]);
items = container.querySelectorAll('li');
expect(items.length).toEqual(3);
expect(appFn).toHaveBeenCalledTimes(1);
// 第一次渲染执行2次push更新执行1次
expect(itemFn).toHaveBeenCalledTimes(3);
let li = container.querySelector('#id-3');
expect(li.innerHTML).toEqual('p3');
});
it('For标签使用使用push创建3000行表格数据', () => {
let reactiveObj;
const App = () => {
const sourceData = useReactive([]);
reactiveObj = sourceData;
return (
<div style={{ width: '100%', height: '100%', overflowY: 'auto' }}>
<table border='1' width='100%'>
<tr>
<th>序号</th>
<th>名称</th>
<th>年龄</th>
<th>性别</th>
<th>名族</th>
<th>其他</th>
</tr>
<For each={sourceData}>
{
eachItem => {
return (
<tr>
<th style={{ color: eachItem.color }}>{eachItem.value}</th>
<th style={{ color: eachItem.color }}>{eachItem.value}</th>
<th style={{ color: eachItem.color }}>{eachItem.value}</th>
<th style={{ color: eachItem.color }}>{eachItem.value}</th>
<th style={{ color: eachItem.color }}>{eachItem.value}</th>
<th style={{ color: eachItem.color }}>{eachItem.value}</th>
</tr>
);
}
}
</For>
</table>
</div>
);
};
render(<App />, container);
// 不推荐循环push
for (let i = 0; i < 2; i++) {
reactiveObj.push({ value: i, color: null });
}
expect(reactiveObj.get().length).toEqual(2);
let items = container.querySelectorAll('tr');
expect(items.length).toEqual(3);
});
});

View File

@ -0,0 +1,129 @@
/*
* 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 Inula, { render, createRef, useReactive, For } from '../../../../../src/index';
import { beforeEach } from '@jest/globals';
const Row = ({ item }) => {
return <li id={item.id} key={item.id}>{item.name}</li>;
};
let rObj;
let ref;
let appFn;
let App;
let itemFn;
describe('测试 For 组件的删除', () => {
beforeEach(() => {
ref = createRef();
appFn = jest.fn();
itemFn = jest.fn();
App = () => {
const _rObj = useReactive({
items: [
{ id: 'id-1', name: 'p1' },
{ id: 'id-2', name: 'p2' },
{ id: 'id-3', name: 'p3' },
{ id: 'id-4', name: 'p4' },
{ id: 'id-5', name: 'p5' },
],
});
rObj = _rObj;
appFn();
return (
<div ref={ref}>
<For each={_rObj.items}>
{item => {
itemFn();
return <Row item={item} />;
}}
</For>
</div>
);
};
});
it('通过 pop 删除最后1行', () => {
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(5);
// 删除最后一行
rObj.items.pop();
items = container.querySelectorAll('li');
expect(items.length).toEqual(4);
expect(appFn).toHaveBeenCalledTimes(1);
// 第一次渲染执行5次pop无需更新
expect(itemFn).toHaveBeenCalledTimes(5);
});
it('通过 splice 删除中间2行', () => {
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(5);
// 删除中间一行
rObj.items.splice(2, 2);
items = container.querySelectorAll('li');
expect(items.length).toEqual(3);
expect(appFn).toHaveBeenCalledTimes(1);
// 第一次渲染执行5次splice无需更新
expect(itemFn).toHaveBeenCalledTimes(5);
});
it('通过 splice 删除中间2行增加1行', () => {
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(5);
// 删除中间2行增加1行
rObj.items.splice(2, 2, ...[{ id: 6, name: 'p6' }]);
items = container.querySelectorAll('li');
expect(items.length).toEqual(4);
expect(appFn).toHaveBeenCalledTimes(1);
// 第一次渲染执行5次splice新增1行会执行1次
expect(itemFn).toHaveBeenCalledTimes(6);
});
it('通过 set 删除中间2行', () => {
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(5);
// 删除中间2行
rObj.items.set([
{ id: 'id-1', name: 'p1' },
{ id: 'id-2', name: 'p2' },
{ id: 'id-5', name: 'p5' },
]);
items = container.querySelectorAll('li');
expect(items.length).toEqual(3);
expect(appFn).toHaveBeenCalledTimes(1);
// 第一次渲染执行5次splice无需更新
expect(itemFn).toHaveBeenCalledTimes(5);
});
});

View File

@ -0,0 +1,251 @@
/*
* 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 Inula, { render, createRef, useReactive, reactive, memo, For } from '../../../../../src/index';
const Item = ({ item }) => {
return <li key={item.id}>{item.name}</li>;
};
describe('测试 For 组件', () => {
it('使用For组件遍历reactive“数组”', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const Item = ({ item }) => {
return <li key={item.id}>{item.name}</li>;
};
const App = () => {
const _rObj = useReactive({
items: [
{ name: 'p1', id: 1 },
{ name: 'p2', id: 2 },
],
});
rObj = _rObj;
fn();
return (
<div ref={ref}>
<For each={_rObj.items}>
{item => {
return <Item item={item} />;
}}
</For>
</div>
);
};
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(2);
// 每次修改items都会触发整个组件刷新
rObj.items.set([{ name: 'p11', id: 1 }]);
items = container.querySelectorAll('li');
expect(items.length).toEqual(1);
expect(fn).toHaveBeenCalledTimes(1);
// 每次修改items都会触发整个组件刷新
rObj.items.push({ name: 'p22', id: 2 });
items = container.querySelectorAll('li');
expect(items.length).toEqual(2);
expect(fn).toHaveBeenCalledTimes(1);
});
it('reactive“数组”从[]变成有值', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const Item = ({ item }) => {
return <li key={item.id}>{item.name}</li>;
};
const App = () => {
const _rObj = useReactive({
items: [],
});
rObj = _rObj;
fn();
return (
<div ref={ref}>
<For each={_rObj.items}>
{item => {
return <Item item={item} />;
}}
</For>
</div>
);
};
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(0);
// 每次修改items都会触发整个组件刷新
rObj.items.set([{ name: 'p11', id: 1 }]);
items = container.querySelectorAll('li');
expect(items.length).toEqual(1);
expect(fn).toHaveBeenCalledTimes(1);
// 每次修改items都会触发整个组件刷新
rObj.items.push({ name: 'p22', id: 2 });
items = container.querySelectorAll('li');
expect(items.length).toEqual(2);
expect(fn).toHaveBeenCalledTimes(1);
});
it('数组3行变到4行', () => {
const state = reactive({
data: {
lines: [
{ id: 'id-1', label: '1' },
{ id: 'id-2', label: '2' },
{ id: 'id-3', label: '3' },
],
},
});
const Row = memo(({ item }) => {
return (
<tr>
<td>{item.id}</td>
<td>
<a id={item.id}>{item.label}</a>
</td>
</tr>
);
});
const RowList = () => {
return <For each={state.data.lines}>{item => <Row item={item} />}</For>;
};
const App = () => {
return (
<div>
<table>
<tbody>
<RowList />
</tbody>
</table>
</div>
);
};
render(<App />, container);
let a = container.querySelector('#id-1');
expect(a.innerHTML).toEqual('1');
expect(state.data.lines.length).toEqual(3);
state.data.set({
lines: [
{ id: 'id-4', label: '4' },
{ id: 'id-5', label: '5' },
{ id: 'id-6', label: '6' },
{ id: 'id-7', label: '7' },
],
});
expect(state.data.lines.length).toEqual(4);
a = container.querySelector('#id-4');
expect(a.innerHTML).toEqual('4');
const b = container.querySelector('#id-6');
expect(b.innerHTML).toEqual('6');
});
it('使用基本数据数组的loop方法', () => {
let rObj;
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
items: [1, 2, 3, 4],
});
rObj = _rObj;
fn();
return (
<div>
{_rObj.items.map(rItem => {
return <li>{rItem}</li>;
})}
</div>
);
};
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(4);
// 每次修改items都会触发整个组件刷新
rObj.items.set([1, 2, 3]);
items = container.querySelectorAll('li');
expect(items.length).toEqual(3);
expect(fn).toHaveBeenCalledTimes(2);
});
});
describe('数组reverse', () => {
it('调用数组的reverse方法', () => {
let rObj;
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
items: [
{ id: 1, name: 'p1' },
{ id: 2, name: 'p2' },
{ id: 3, name: 'p3' },
],
});
rObj = _rObj;
fn();
return (
<div>
<For each={_rObj.items}>
{item => {
return <Item item={item} />;
}}
</For>
</div>
);
};
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(3);
// 反转
rObj.items.reverse();
items = container.querySelectorAll('li');
expect(items.length).toEqual(3);
expect(fn).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,94 @@
/*
* 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 Inula, { render, createRef, act, useReactive } from '../../../../src/index';
import { Block } from '../../../../src/reactive/components/Block';
describe('测试 Block 组件', () => {
it('使用 Block 控制更新范围', () => {
let rObj, rColor;
const ref = createRef();
const fn = jest.fn();
const fn1 = jest.fn();
const App = () => {
const _rObj = useReactive({ count: 0 });
const _rColor = useReactive('blue');
rObj = _rObj;
rColor = _rColor;
fn();
return (
<div ref={ref}>
111 222
<Block>
{() => {
fn1();
const count = _rObj.count.get();
return (
<>
<div>Count: {count}</div>
<div>{_rColor}</div>
</>
);
}}
</Block>
</div>
);
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('111 222<div>Count: 0</div><div>blue</div>');
// 会触发View刷新
rObj.count.set(1);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn1).toHaveBeenCalledTimes(2);
expect(ref.current.innerHTML).toEqual('111 222<div>Count: 1</div><div>blue</div>');
// 不会触发View刷新
rColor.set('red');
expect(fn).toHaveBeenCalledTimes(1);
expect(fn1).toHaveBeenCalledTimes(2);
expect(ref.current.innerHTML).toEqual('111 222<div>Count: 1</div><div>red</div>');
});
it('使用 Block 包裹一个Atom', () => {
let rObj;
const ref1 = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('blue');
rObj = _rObj;
fn();
return (
// div下面有多个元素_rObj就需要用RText包裹
<div ref={ref1}>
111 222
<Block>{_rObj}</Block>
</div>
);
};
render(<App />, container);
expect(ref1.current.innerHTML).toEqual('111 222blue');
rObj.set('red');
expect(fn).toHaveBeenCalledTimes(1);
expect(ref1.current.innerHTML).toEqual('111 222red');
});
});

View File

@ -0,0 +1,105 @@
/*
* 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 Inula, { render, createRef, useReactive, useComputed, For, Show, Switch } from '../../../../src/index';
describe('测试Switch、Show、For标签的组合使用时的组件渲染', () => {
it('Show、For标签的组合使用', () => {
const Item = ({ item }) => {
return <li key={item.id}>{item.name}</li>;
};
let reactiveObj;
const ref = createRef();
const ref1 = createRef();
const fn = jest.fn();
const App = () => {
const dataList = useReactive([]);
reactiveObj = dataList;
const listLen = useComputed(() => {
return dataList.get().length;
});
fn();
return (
<>
<Show if={() => dataList.get().length > 0} else={() => <div />}>
<div ref={ref} style={{ display: 'flex' }}>
<For each={dataList}>{item => <Item item={item} />}</For>
</div>
</Show>
<div ref={ref1}>{listLen}</div>
</>
);
};
render(<App />, container);
let liItems = container.querySelectorAll('li');
expect(liItems.length).toEqual(0);
reactiveObj.push({ id: 1, name: '1' });
expect(reactiveObj.get().length).toEqual(1);
liItems = container.querySelectorAll('li');
expect(liItems.length).toEqual(1);
reactiveObj.push({ id: 2, name: '2' });
expect(reactiveObj.get().length).toEqual(2);
liItems = container.querySelectorAll('li');
expect(liItems.length).toEqual(2);
expect(ref1.current.innerHTML).toEqual('2');
expect(fn).toHaveBeenCalledTimes(1);
});
it('Switch、Show和For标签的组合使用', () => {
const Item = ({ item }) => {
return <li key={item.id}>{item.name}</li>;
};
let reactiveObj;
const ref = createRef();
const App = () => {
const dataList = useReactive([]);
reactiveObj = dataList;
return (
<Switch>
<Show if={() => dataList.get().length === 0}>
<div />
</Show>
<Show if={() => dataList.get().length > 0}>
<div ref={ref} style={{ display: 'flex' }}>
<For each={dataList}>{item => <Item item={item} />}</For>
</div>
</Show>
</Switch>
);
};
render(<App />, container);
let liItems = container.querySelectorAll('li');
expect(liItems.length).toEqual(0);
reactiveObj.push({ id: 1, name: '1' });
expect(reactiveObj.get().length).toEqual(1);
liItems = container.querySelectorAll('li');
expect(liItems.length).toEqual(1);
});
});

View File

@ -0,0 +1,44 @@
/*
* 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 Inula, { render, createRef, act, useReactive, useCompute, reactive, RText } from '../../../../src/index';
describe('测试 RText 组件', () => {
it('使用RText精准更新', () => {
let rObj;
const ref1 = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('blue');
rObj = _rObj;
fn();
return (
// div下面有多个元素_rObj就需要用RText包裹
<div ref={ref1}>
111 222
<RText>{_rObj}</RText>
</div>
);
};
render(<App />, container);
expect(ref1.current.innerHTML).toEqual('111 222blue');
rObj.set('red');
expect(fn).toHaveBeenCalledTimes(1);
expect(ref1.current.innerHTML).toEqual('111 222red');
});
});

View File

@ -0,0 +1,200 @@
/*
* 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 Inula, { render, createRef, act, useReactive, useCompute, reactive, Show } from '../../../../src/index';
describe('测试 Show 组件', () => {
it('if为primitive值', () => {
let rObj;
const ref1 = createRef();
const ref2 = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('blue');
rObj = _rObj;
fn();
return (
// 如果else中的dom和children一个类型需要增加key否则会被框架当作同一个dom
<Show
if={_rObj}
else={
<div key="else" ref={ref2}>
Loading...
</div>
}
>
<div key="if" ref={ref1}>
{_rObj}
</div>
</Show>
);
};
render(<App />, container);
expect(ref1.current.innerHTML).toEqual('blue');
rObj.set('');
expect(ref2.current.innerHTML).toEqual('Loading...');
expect(fn).toHaveBeenCalledTimes(1);
});
it('if为primitive值没有else', () => {
let rObj;
const ref1 = createRef();
const ref2 = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('blue');
rObj = _rObj;
fn();
return (
// 如果else中的dom和children一个类型需要增加key否则会被框架当作同一个dom
<Show if={_rObj}>
<div ref={ref1}>{_rObj}</div>
</Show>
);
};
render(<App />, container);
expect(ref1.current.innerHTML).toEqual('blue');
rObj.set('');
expect(ref2.current).toEqual(null);
expect(fn).toHaveBeenCalledTimes(1);
});
it('if为reactive object值', () => {
let rObj;
const ref1 = createRef();
const ref2 = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
color: 'blue',
});
rObj = _rObj;
fn();
return (
// 如果else中的dom和children一个类型需要增加key否则会被框架当作同一个dom
<Show
if={_rObj.color}
else={
<div key="else" ref={ref2}>
Loading...
</div>
}
>
<div key="if" ref={ref1}>
{_rObj.color}
</div>
</Show>
);
};
render(<App />, container);
expect(ref1.current.innerHTML).toEqual('blue');
rObj.color.set('');
expect(ref2.current.innerHTML).toEqual('Loading...');
expect(fn).toHaveBeenCalledTimes(1);
});
it('if为函数', () => {
let rObj;
const ref1 = createRef();
const ref2 = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
color: 'blue',
});
rObj = _rObj;
fn();
return (
// 如果else中的dom和children一个类型需要增加key否则会被框架当作同一个dom
<Show
if={() => _rObj.color}
else={
<div key="else" ref={ref2}>
Loading...
</div>
}
>
<div key="if" ref={ref1}>
{_rObj.color}
</div>
</Show>
);
};
render(<App />, container);
expect(ref1.current.innerHTML).toEqual('blue');
rObj.color.set('');
expect(ref2.current.innerHTML).toEqual('Loading...');
expect(fn).toHaveBeenCalledTimes(1);
});
it('if的children、else是函数', () => {
const ref1 = createRef();
const ref2 = createRef();
const fn = jest.fn();
const _count = reactive(0);
const _rObj = reactive({
color: 'blue',
});
const App = () => {
fn();
return (
// 如果else中的dom和children一个类型需要增加key否则会被框架当作同一个dom
<Show
if={() => _rObj.color}
else={() => (
<div key="else" ref={ref2}>
Loading...
</div>
)}
>
{() => {
const text = useCompute(() => {
return _rObj.color.get() + _count.get();
});
return (
<div key="if" ref={ref1}>
{text}
</div>
);
}}
</Show>
);
};
render(<App />, container);
expect(ref1.current.innerHTML).toEqual('blue0');
// 修改children函数中使用到的响应式变量也会触发Show组件更新
_count.set(1);
expect(ref1.current.innerHTML).toEqual('blue1');
_rObj.color.set('');
expect(ref2.current.innerHTML).toEqual('Loading...');
expect(fn).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,75 @@
/*
* 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 Inula, { render, createRef, act, useReactive, Show, Switch } from '../../../../src/index';
describe('测试 Switch 组件', () => {
it('Switch 配合 Show 使用', () => {
let rObj;
const refBlue = createRef();
const refRed = createRef();
const refYellow = createRef();
const refNothing = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('blue');
rObj = _rObj;
fn();
return (
<Switch default={<div ref={refNothing}>nothing</div>}>
{/*if不能写成 _rObj === 'red' 或者 _rObj.get() === 'red' */}
<Show if={() => _rObj.get() === 'blue'}>
<div id="1" ref={refBlue}>
{_rObj}
</div>
</Show>
<Show if={() => _rObj.get() === 'red'}>
<div id="2" ref={refRed}>
{_rObj}
</div>
</Show>
<Show if={() => _rObj.get() === 'yellow'}>
<div id="3" ref={refYellow}>
{_rObj}
</div>
</Show>
</Switch>
);
};
render(<App />, container);
expect(refBlue.current.innerHTML).toEqual('blue');
// rObj被3个RContext依赖分别是Switch组件、Show组件、div[id=1]的Children
expect(rObj.usedRContexts.size).toEqual(3);
act(() => {
rObj.set('red');
});
expect(refRed.current.innerHTML).toEqual('red');
// rObj被3个Effect依赖分别是Switch组件、Show组件、div[id=2]的Children
expect(rObj.usedRContexts.size).toEqual(3);
act(() => {
rObj.set('black');
});
expect(refNothing.current.innerHTML).toEqual('nothing');
// rObj被1个RContext依赖分别是Switch组件
expect(rObj.usedRContexts.size).toEqual(1);
expect(fn).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,51 @@
import Inula, { computed, createRef, reactive, render } from '../../../src/index';
describe('测试 computed', () => {
it('在class组件render中使用computed', () => {
let rObj;
let appInst;
const ref = createRef();
const fn = jest.fn();
class App extends Inula.Component {
constructor(props) {
super(props);
appInst = this;
this.state = {
name: 1,
};
this._rObj = reactive(1);
rObj = this._rObj;
}
render() {
const computedVal = computed(() => {
fn();
return this._rObj.get() + '!!!';
});
return <div ref={ref}>{computedVal}</div>;
}
}
render(<App />, container);
expect(ref.current.innerHTML).toEqual('1!!!'); // computed执行2次
expect(fn).toHaveBeenCalledTimes(1);
rObj.set('2');
expect(ref.current.innerHTML).toEqual('2!!!');
expect(fn).toHaveBeenCalledTimes(2); // computed执行2次
// 触发组件重新渲染
appInst.setState({ name: 2 });
expect(fn).toHaveBeenCalledTimes(3); // 生成新的一个computation再执行了1次computed总共执行3次
rObj.set('3');
expect(ref.current.innerHTML).toEqual('3!!!');
expect(fn).toHaveBeenCalledTimes(5); // 两个computation各执行了一次computed总共执行5次
});
});

View File

@ -0,0 +1,377 @@
/*
* 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 Inula, {
createRef,
For,
reactive,
render,
useCompute,
useReactive,
computed,
} from '../../../src/index';
describe('computed 基本使用', () => {
it('computed 返回的是一个响应式对象,用到的响应式对象是原始类型', () => {
const rObj = reactive('123');
const comp = computed(() => {
return rObj.get() + '!!!';
});
expect(comp.get()).toEqual('123!!!');
rObj.set('456');
expect(comp.get()).toEqual('456!!!');
});
it('computed 返回的是一个响应式对象,用到两个响应式对象', () => {
const rObj1 = reactive({ name: 'xiaoming' });
const rObj2 = reactive({ age: 18 });
const comp = computed(() => {
return rObj1.name.get() + ' is ' + rObj2.age.get();
});
expect(comp.get()).toEqual('xiaoming is 18');
rObj1.name.set('xiaowang');
rObj2.set(prev => ({ age: prev.age + 2 }));
expect(comp.get()).toEqual('xiaowang is 20');
});
it('computed 返回的是一个复杂响应式对象', () => {
const rObj = reactive({ array: [1, 2, 3, 4, 5, 6] });
const comp = computed(() => {
return { newArray: rObj.array.get().filter(x => x > 4) };
});
expect(comp.get()).toEqual({ newArray: [5, 6] });
expect(comp.newArray.get()).toEqual([5, 6]);
rObj.array.push(...[100]);
expect(comp.get()).toEqual({ newArray: [5, 6, 100] });
});
it('computed 返回的是一个响应式对象,用到的响应式对象是对象类型', () => {
const rObj = reactive({ array: [1, 2, 3, 4, 5, 6] });
const comp = computed(() => {
return rObj.array.get().filter(x => x > 4);
});
expect(comp.get()).toEqual([5, 6]);
rObj.array.set([1, 2, 3, 4, 5, 6, 7, 8, 9]);
expect(comp.get()).toEqual([5, 6, 7, 8, 9]);
rObj.array.push(...[10, 11]);
expect(comp.get()).toEqual([5, 6, 7, 8, 9, 10, 11]);
rObj.set({ array: [100, 101, 102] });
expect(comp.get()).toEqual([100, 101, 102]);
});
it('computed 返回的是一个复杂响应式对象2', () => {
const rObj = reactive({ array: [1, 2, 3, 4, 5, 6] });
const comp = computed(() => {
return { newArray: rObj.array.get().filter(x => x > 4) };
});
expect(comp.newArray.get()).toEqual([5, 6]);
rObj.array.push(...[7, 8]);
expect(comp.newArray.get()).toEqual([5, 6, 7, 8]);
rObj.array.set([1, 100, 101, 102]);
expect(comp.newArray.get()).toEqual([100, 101, 102]);
expect(comp.get()).toEqual({ newArray: [100, 101, 102] });
});
});
describe('测试 useCompute', () => {
it('useComputed基本使用 使用get方法(组件式更新)', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('123');
rObj = _rObj;
const _cObj = useCompute(() => {
return _rObj.get() + '!!!';
});
fn();
return <div ref={ref}>{_cObj.get()}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('123!!!');
expect(fn).toHaveBeenCalledTimes(1);
rObj.set('456');
expect(ref.current.innerHTML).toEqual('456!!!');
expect(fn).toHaveBeenCalledTimes(2);
rObj.set('789');
expect(ref.current.innerHTML).toEqual('789!!!');
expect(fn).toHaveBeenCalledTimes(3);
});
it('useComputed基本使用 直接使用对象(Dom级更新)', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('123');
rObj = _rObj;
const _cObj = useCompute(() => {
return _rObj.get() + '!!!';
});
fn();
return <div ref={ref}>{_cObj}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('123!!!');
expect(fn).toHaveBeenCalledTimes(1);
rObj.set('456');
expect(ref.current.innerHTML).toEqual('456!!!');
rObj.set('789');
expect(ref.current.innerHTML).toEqual('789!!!');
expect(fn).toHaveBeenCalledTimes(1);
});
it('useComputed 基本使用2', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const compFn = jest.fn();
const App = () => {
const _rObj = useReactive({ array: [1, 2, 3, 4, 5, 6] });
rObj = _rObj;
const cObj = useCompute(() => {
compFn();
return { len: _rObj.array.get().filter(x => x >= 4).length };
});
fn();
return <div ref={ref}>{cObj.len}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('3');
rObj.array.push(...[7, 8]);
expect(ref.current.innerHTML).toEqual('5');
expect(fn).toHaveBeenCalledTimes(1);
rObj.array.unshift(...[0, 100]);
expect(ref.current.innerHTML).toEqual('6');
rObj.set({ array: [1, 100, 101, 102, 103] });
expect(ref.current.innerHTML).toEqual('4');
expect(compFn).toHaveBeenCalledTimes(4);
expect(fn).toHaveBeenCalledTimes(1);
});
it('连锁useComputed使用', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const compFn = jest.fn();
const App = () => {
const _rObj = useReactive(1);
rObj = _rObj;
const double = useCompute(() => _rObj.get() * 2);
const dd = useCompute(() => {
compFn();
return double.get() * 2;
});
fn();
return <div ref={ref}>{dd}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('4');
expect(compFn).toHaveBeenCalledTimes(1);
rObj.set('2');
expect(ref.current.innerHTML).toEqual('8');
expect(compFn).toHaveBeenCalledTimes(2);
rObj.set('4');
expect(ref.current.innerHTML).toEqual('16');
expect(compFn).toHaveBeenCalledTimes(3);
expect(fn).toHaveBeenCalledTimes(1);
});
it('useComputed中使用到了两个响应式对象', () => {
let _rObj1;
let _rObj2;
const ref = createRef();
const fn = jest.fn();
const compFn = jest.fn();
const App = () => {
const rObj1 = useReactive({ name: 'xiaoming' });
const rObj2 = useReactive({ age: 18 });
_rObj1 = rObj1;
_rObj2 = rObj2;
const words = useCompute(() => {
compFn();
return `${rObj1.name.get()} is ${rObj2.age.get()}`;
});
fn();
return <div ref={ref}>{words}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('xiaoming is 18');
expect(compFn).toHaveBeenCalledTimes(1);
_rObj1.name.set('xiaowang');
expect(ref.current.innerHTML).toEqual('xiaowang is 18');
expect(compFn).toHaveBeenCalledTimes(2);
_rObj2.set({ age: 20 });
expect(ref.current.innerHTML).toEqual('xiaowang is 20');
expect(compFn).toHaveBeenCalledTimes(3);
_rObj1.name.set('laowang');
_rObj2.set({ age: 30 });
expect(ref.current.innerHTML).toEqual('laowang is 30');
expect(compFn).toHaveBeenCalledTimes(5);
expect(fn).toHaveBeenCalledTimes(1);
});
it('多个reactive的compute', () => {
let a;
const ref = createRef();
const compFn = jest.fn();
const computeFn = jest.fn();
const App = () => {
const _a = useReactive('a');
const b = useReactive('b');
const cond = useReactive(true);
a = _a;
const compute = useCompute(() => {
computeFn();
return cond.get() ? _a.get() : b.get();
});
compFn();
return (
<button
ref={ref}
className={compute}
onClick={() => {
cond.set(false);
}}
>
{compute}
</button>
);
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('a');
ref.current.click();
expect(ref.current.innerHTML).toEqual('b');
a.set('aa');
expect(computeFn).toHaveBeenCalledTimes(3);
expect(ref.current.innerHTML).toEqual('b');
});
it('useCompute返回一个数组对象', () => {
let rObj;
let cObj;
let ref = createRef();
let appFn = jest.fn();
let itemFn = jest.fn();
const App = () => {
const _rObj = useReactive([
{ id: 'id-1', name: 'p1' },
{ id: 'id-2', name: 'p2' },
{ id: 'id-3', name: 'p3' },
]);
rObj = _rObj;
const _cObj = useCompute(() => {
return _rObj.get().slice();
});
cObj = _cObj;
appFn();
return (
<div ref={ref}>
<For each={_cObj}>
{item => {
itemFn();
return (
<li id={item.id} key={item.id}>
{item.name}
</li>
);
}}
</For>
</div>
);
};
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(3);
rObj.push({ id: 'id-4', name: 'p4' });
items = container.querySelectorAll('li');
expect(items.length).toEqual(4);
// rObj[1].name.get();
rObj[1].set({ id: 'id-2', name: 'p222' });
let li = container.querySelector('#id-2');
expect(li.innerHTML).toEqual('p222');
// // 更新
// cObj.set([true]);
//
// items = container.querySelectorAll('li');
// expect(items.length).toEqual(1);
// expect(appFn).toHaveBeenCalledTimes(1);
//
// // 第一次渲染执行3次更新也触发了1次
// expect(itemFn).toHaveBeenCalledTimes(4);
});
xit('测试compute在checkbox中的使用', () => {
let a;
const ref = createRef();
const compFn = jest.fn();
const computeFn = jest.fn();
const App = () => {
const rObj = useReactive({ checked: true });
const checked = useCompute(() => {
return rObj.checked.get();
});
compFn();
return <Checkbox checked={checked} />;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('a');
ref.current.click();
expect(ref.current.innerHTML).toEqual('b');
a.set('aa');
expect(computeFn).toHaveBeenCalledTimes(3);
expect(ref.current.innerHTML).toEqual('b');
});
});

View File

@ -0,0 +1,77 @@
/*
* 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 Inula, { render, createRef, act, useReactive } from '../../../src/index';
import { Show } from '../../../src/reactive/components/Show';
import { Switch } from '../../../src/reactive/components/Switch';
describe('响应式数据usedRContexts', () => {
it('测试响应式数据的usedRContexts会随着VNode的删除而清除', () => {
let rObj;
const refBlue = createRef();
const refRed = createRef();
const refYellow = createRef();
const refNothing = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('blue');
rObj = _rObj;
fn();
return (
<Switch default={<div ref={refNothing}>nothing</div>}>
{/*if不能写成 _rObj === 'red' 或者 _rObj.get() === 'red' */}
<Show if={() => _rObj.get() === 'blue'}>
<div id="1" ref={refBlue}>
{_rObj}
</div>
</Show>
<Show if={() => _rObj.get() === 'red'}>
<div id="2" ref={refRed}>
{_rObj}
</div>
</Show>
<Show if={() => _rObj.get() === 'yellow'}>
<div id="3" ref={refYellow}>
{_rObj}
</div>
</Show>
</Switch>
);
};
render(<App />, container);
expect(refBlue.current.innerHTML).toEqual('blue');
// rObj被3个RContext依赖分别是Switch组件、Show组件、div[id=1]的Children
expect(rObj.usedRContexts.size).toEqual(3);
act(() => {
rObj.set('red');
});
expect(refRed.current.innerHTML).toEqual('red');
// rObj被3个Effect依赖分别是Switch组件、Show组件、div[id=2]的Children
expect(rObj.usedRContexts.size).toEqual(3);
act(() => {
rObj.set('black');
});
expect(refNothing.current.innerHTML).toEqual('nothing');
// rObj被1个RContext依赖分别是Switch组件
expect(rObj.usedRContexts.size).toEqual(1);
expect(fn).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,60 @@
/*
* 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 Inula, {createRef, render, useReactive, useState, Show} from '../../../src/index';
describe('传统API和响应式API混合使用', () => {
it('混合使用1', () => {
let rObj, isShow, update;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _isShow = useReactive(true);
isShow = _isShow;
const [_, setState] = useState({});
update = () => setState({});
return (
<Show if={isShow}>
<Child />
</Show>
);
};
const Child = () => {
const _rObj = useReactive('blue');
rObj = _rObj;
fn();
return <div ref={ref} className={_rObj}></div>;
};
render(<App />, container);
expect(ref.current.className).toEqual('blue');
// 改变了DOM结构
isShow.set(false);
expect(ref.current).toEqual(null);
update();
expect(ref.current).toEqual(null);
});
});

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import Inula, { render, createRef, act, useReactive } from '../../../src/index';
describe('测试混合型的 children', () => {
it('children是 字符串+Atom 场景', () => {
let rObj;
const ref1 = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive(0);
rObj = _rObj;
fn();
return (
// div下面有多个元素
<div ref={ref1}>Count: {_rObj}</div>
);
};
render(<App />, container);
expect(ref1.current.innerHTML).toEqual('Count: 0');
rObj.set(1);
expect(fn).toHaveBeenCalledTimes(1);
expect(ref1.current.innerHTML).toEqual('Count: 1');
});
it('children是 字符串+Atom 场景2', () => {
let rObj;
const ref1 = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({ count: 0 });
rObj = _rObj;
fn();
return (
// div下面有多个元素
<div ref={ref1}>Count: {_rObj.count}</div>
);
};
render(<App />, container);
expect(ref1.current.innerHTML).toEqual('Count: 0');
rObj.count.set(1);
expect(fn).toHaveBeenCalledTimes(1);
expect(ref1.current.innerHTML).toEqual('Count: 1');
});
});

View File

@ -0,0 +1,973 @@
/*
* 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 Inula, {
render,
createRef,
useReactive,
useCompute,
reactive,
computed,
watchReactive,
} from '../../../src/index';
import { GET_R_NODE } from '../../../src/reactive/proxy/RProxyHandler';
import { isAtom, isReactiveProxy, isRNode } from '../../../src/reactive/Utils';
describe('测试 useReactive(对象)', () => {
it('reactive基本使用', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
color: 'blue',
});
rObj = _rObj;
fn();
return <div ref={ref}>{_rObj.color}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('blue');
rObj.color.set('red');
expect(rObj.color.get()).toEqual('red');
expect(ref.current.innerHTML).toEqual('red');
rObj.color.set(prev => prev + '!!');
expect(rObj.color.get()).toEqual('red!!');
expect(ref.current.innerHTML).toEqual('red!!');
expect(fn).toHaveBeenCalledTimes(1);
});
it('响应式对象赋值修改为一个对象', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
data: { framework: 'Vue' },
});
rObj = _rObj;
fn();
return <div ref={ref}>{_rObj.data.framework}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('Vue');
rObj.data.set({ framework: 'React' });
expect(rObj.data.framework.get()).toEqual('React');
expect(ref.current.innerHTML).toEqual('React');
expect(fn).toHaveBeenCalledTimes(1);
});
it('赋值修改复杂响应式对象', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
data: { framework: { js: 'Vue' } },
});
rObj = _rObj;
fn();
return <div ref={ref}>{_rObj.data.framework.js}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('Vue');
rObj.data.set({ framework: { js: 'React' } });
expect(rObj.data.framework.get()).toEqual({ js: 'React' });
expect(ref.current.innerHTML).toEqual('React');
expect(fn).toHaveBeenCalledTimes(1);
});
it('赋值修改响应式对象中Atom的值', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
rdata: { framework: 'Vue' },
});
rObj = _rObj;
fn();
return <div ref={ref}>{_rObj.rdata.framework}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('Vue');
rObj.rdata.framework.set('React');
expect(rObj.rdata.get()).toEqual({ framework: 'React' });
expect(ref.current.innerHTML).toEqual('React');
expect(fn).toHaveBeenCalledTimes(1);
});
it('把响应式属性传递到子组件', () => {
let rObj;
const ref = createRef();
const fn1 = jest.fn();
const fn2 = jest.fn();
const App = () => {
const _rObj = useReactive({
data: {
color: 'blue',
},
});
rObj = _rObj;
fn1();
return <Child color={_rObj.data.color} />;
};
const Child = ({ color }) => {
fn2();
const cl = useCompute(() => {
return 'cl-' + color.get();
});
return <div ref={ref} className={cl}></div>;
};
render(<App />, container);
expect(ref.current.className).toEqual('cl-blue');
expect(fn1).toHaveBeenCalledTimes(1);
expect(fn2).toHaveBeenCalledTimes(1);
rObj.data.color.set('red');
expect(fn1).toHaveBeenCalledTimes(1);
expect(fn2).toHaveBeenCalledTimes(1);
expect(ref.current.className).toEqual('cl-red');
});
it('reactive对象中“原始数据”被赋值为“对象”', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
data: 'blue',
});
rObj = _rObj;
_rObj.data.set({ color: 'red' });
fn();
return <div ref={ref}>{_rObj.data.color}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('red');
rObj.data.color.set('blue');
expect(rObj.data.color.get()).toEqual('blue');
expect(ref.current.innerHTML).toEqual('blue');
expect(fn).toHaveBeenCalledTimes(1);
});
it('reactive对象中“对象”被赋值为“新对象”', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
data: {
cl: 'blue',
},
});
rObj = _rObj;
_rObj.data.set({ color: 'red' });
fn();
return <div ref={ref}>{_rObj.data.color}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('red');
rObj.data.color.set('blue');
expect(rObj.data.color.get()).toEqual('blue');
expect(ref.current.innerHTML).toEqual('blue');
expect(fn).toHaveBeenCalledTimes(1);
});
});
describe('测试reactive数组', () => {
it('reactive“数组”length的使用', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
data: [
{ name: 'p1', age: 1 },
{ name: 'p2', age: 2 },
],
});
rObj = _rObj;
fn();
// 在DOM中使用length无法精细响应式
return <div ref={ref}>{_rObj.data.length}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('2');
rObj.data.set([{ name: 'p1', age: 1 }]);
expect(ref.current.innerHTML).toEqual('1');
expect(fn).toHaveBeenCalledTimes(2);
});
it('reactive“数组”的使用', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
data: [
{ name: 'p1', age: 1 },
{ name: 'p2', age: 2 },
],
});
rObj = _rObj;
fn();
return <div ref={ref}>{_rObj.data[0].name}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('p1');
// 这种修改无法响应!
// rObj.data.set([
// { name: 'p11', age: 1 },
// ]);
// 直接修改数组中被使用属性
rObj.data[0].name.set('p11');
expect(ref.current.innerHTML).toEqual('p11');
// 在DOM中使用length无法精细响应式
expect(fn).toHaveBeenCalledTimes(1);
});
it('jsx中通过items.get().map遍历reactive“数组”', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
items: [
{ name: 'p1', id: 1 },
{ name: 'p2', id: 2 },
],
});
rObj = _rObj;
fn();
return (
<div ref={ref}>
{_rObj.items.get().map(item => {
return <li key={item.id}>{item.name}</li>;
})}
</div>
);
};
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(2);
// 每次修改items都会触发整个组件刷新
rObj.items.set([{ name: 'p11', age: 1 }]);
items = container.querySelectorAll('li');
expect(items.length).toEqual(1);
expect(fn).toHaveBeenCalledTimes(2);
// 每次修改items都会触发整个组件刷新
rObj.items.push({ name: 'p22', id: 2 });
items = container.querySelectorAll('li');
expect(items.length).toEqual(2);
expect(fn).toHaveBeenCalledTimes(3);
});
it('jsx中通过items.get().map遍历reactive“数组”孩子是Item', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const Item = ({ item }) => {
return <li key={item.id}>{item.name}</li>;
};
const App = () => {
const _rObj = useReactive({
items: [
{ name: 'p1', id: 1 },
{ name: 'p2', id: 2 },
],
});
rObj = _rObj;
fn();
return (
<div ref={ref}>
{/*items必须要调用get()才能map*/}
{_rObj.items.get().map(item => {
return <Item item={item} />;
})}
</div>
);
};
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(2);
expect(fn).toHaveBeenCalledTimes(1);
// 每次修改items都会触发整个组件刷新
rObj.items.set([{ name: 'p11', age: 1 }]);
items = container.querySelectorAll('li');
expect(items.length).toEqual(1);
expect(fn).toHaveBeenCalledTimes(2);
// 每次修改items都会触发整个组件刷新
rObj.items.push({ name: 'p22', id: 2 });
items = container.querySelectorAll('li');
expect(items.length).toEqual(2);
expect(fn).toHaveBeenCalledTimes(3);
});
it('jsx中通过items.map遍历reactive“数组”具有响应式', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const Item = ({ item }) => {
return <li key={item.id}>{item.name}</li>;
};
const App = () => {
const _rObj = useReactive({
items: [
{ name: 'p1', id: 1 },
{ name: 'p2', id: 2 },
{ name: 'p3', id: 3 },
],
});
rObj = _rObj;
fn();
return (
<div ref={ref}>
{_rObj.items.map(item => {
return <Item item={item} />;
})}
</div>
);
};
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(3);
expect(fn).toHaveBeenCalledTimes(1);
rObj.items.set([
{ name: 'p11', age: 1 },
{ name: 'p22', age: 2 },
]);
items = container.querySelectorAll('li');
// 子元素不会响应式变化
expect(items.length).toEqual(2);
expect(fn).toHaveBeenCalledTimes(2);
});
it('jsx中通过items.map遍历reactive“数组”孩子是ItemItem对象具有响应式', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const Item = ({ item }) => {
const id = useCompute(() => {
return `id-${item.id.get()}`;
});
return (
<li key={item.id} id={id}>
{item.name}
</li>
);
};
const App = () => {
const _rObj = useReactive({
items: [
{ name: 'p1', id: 1 },
{ name: 'p2', id: 2 },
],
});
rObj = _rObj;
fn();
return (
<div ref={ref}>
{_rObj.items.map(item => {
return <Item item={item} />;
})}
</div>
);
};
render(<App />, container);
let item = container.querySelector('#id-1');
expect(item.innerHTML).toEqual('p1');
expect(fn).toHaveBeenCalledTimes(1);
rObj.items[0].name.set('p111');
item = container.querySelector('#id-1');
// 子元素会响应式变化
expect(item.innerHTML).toEqual('p111');
expect(fn).toHaveBeenCalledTimes(1);
});
it('测试响应式数据', () => {
const obj = reactive({
data: [
{
id: '1',
value: 'val-1',
},
{
id: '2',
value: 'val-2',
},
],
});
// 使用让创建children
obj.data[1].value.read();
obj.set({
data: [
{
id: '11',
value: 'val-11',
},
],
});
obj.set({
data: [
{
id: '111',
value: 'val-111',
},
{
id: '222',
value: 'val-222',
},
],
});
expect(obj.data[1].value.get()).toEqual('val-222');
});
it('响应式对象为复杂对象时使用set重新设置', () => {
const rObj = reactive({ data: [1, 2, 3, 4, 5, 6] });
rObj.data.push(...[7, 8]);
expect(rObj.data.get()).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
rObj.data.set([100, 101]);
expect(rObj.get()).toEqual({ data: [100, 101] });
});
it('使用set直接修改响应式对象数组中某个元素的值', () => {
const rObj = reactive({ data: [1, 2, 3] });
rObj.data.push(...[4, 5, 6]);
expect(rObj.data.get()).toEqual([1, 2, 3, 4, 5, 6]);
// 修改数组第4个元素
rObj.data[1].set({ val: 2 });
expect(rObj.get()).toEqual({ data: [1, { val: 2 }, 3, 4, 5, 6] });
});
it('使用set直接修改响应式对象数组中某个元素的值2', () => {
const rObj = reactive({ data: [1, 2, 3] });
rObj.data.push(...[4, 5, 6]);
expect(rObj.data.get()).toEqual([1, 2, 3, 4, 5, 6]);
// 修改数组第4个元素
rObj.data[4].set({ val: 2 });
expect(rObj.get()).toEqual({ data: [1, 2, 3, 4, { val: 2 }, 6] });
});
it('在删除数组中一个数字再加一个对象类型是RNode', () => {
const rObj = reactive({ data: [1, 2, 3, 4] });
// 使用最后一个数据在children中创建出child
rObj.data[3].get();
// 删除最后一个数据
rObj.data.set([1, 2, 3]);
// 重新增加一个obj类型的数据
rObj.data.set([1, 2, 3, { val: 4 }]);
// rObj.data[3]是RNode
expect(isRNode(rObj.data[3][GET_R_NODE])).toBeTruthy();
expect(rObj.data[3].val.get()).toEqual(4);
});
xit('钻石问题', () => {
const fn = jest.fn();
const rObj = reactive(0);
const evenOrOdd = computed(() => (rObj.get() % 2 === 0 ? 'even' : 'odd'));
watchReactive(() => {
fn();
rObj.get();
evenOrOdd.get();
});
rObj.set(1);
// TODO
expect(fn).toHaveBeenCalledTimes(3);
});
it('数组中的数据由“对象”变成“字符串”', () => {
let fn = jest.fn();
let fn1 = jest.fn();
const rObj = reactive({
items: [
{ name: 'p1', id: 1 },
{ name: { n: 'p22' }, id: 2 },
],
});
watchReactive(rObj.items[1].name, () => {
fn();
});
watchReactive(rObj.items[1].name.n, () => {
fn1();
});
rObj.items.set([
{ name: 'p1', id: 1 },
{ name: 'p2', id: 2 }, // name 改为 基本数据类型
]);
expect(fn).toHaveBeenCalledTimes(1);
// 无法触发fn1
expect(fn1).toHaveBeenCalledTimes(0);
});
it('数组中的数据由“字符串”变成“对象”', () => {
let fn = jest.fn();
let fn1 = jest.fn();
const rObj = reactive({
items: [
{ name: 'p1', id: 1 },
{ name: 'p2', id: 2 },
],
});
watchReactive(rObj.items[1].name, () => {
fn();
});
// 允许使用或监听没有定义的属性
watchReactive(rObj.items[1].name.n, () => {
fn1();
});
rObj.items.set([
{ name: 'p1', id: 1 },
{ name: { n: 'p22' }, id: 2 },
]);
expect(fn).toHaveBeenCalledTimes(1);
// 可以触发fn1
expect(fn1).toHaveBeenCalledTimes(1);
});
it('访问一个不存在的属性,会抛出异常', () => {
let fn = jest.fn();
let fn1 = jest.fn();
const rObj = reactive({
items: [{ name: 'p1' }, { name: 'p2' }],
});
watchReactive(() => {
rObj.items[1].get();
fn();
});
watchReactive(() => {
// 会抛异常
rObj.items[1].name.n.get();
fn1();
});
rObj.items.set([{ name: 'p1' }, { name: { n: 'p22' } }]);
expect(fn).toHaveBeenCalledTimes(2);
// 无法触发fn1
expect(fn1).toHaveBeenCalledTimes(2);
});
it('数组中的数据由“字符串”变成“对象”3', () => {
let fn = jest.fn();
let fn1 = jest.fn();
const rObj = reactive({
items: [{ a: 1 }, 2, 3],
});
watchReactive(() => {
rObj.items[1].get();
fn();
});
rObj.items.set([2, 3, 4]);
expect(fn).toHaveBeenCalledTimes(2);
});
it('数组中的数据由“数字”变成“对象”', () => {
let fn = jest.fn();
let fn1 = jest.fn();
const rObj = reactive({
items: [1, 2, 3],
});
watchReactive(() => {
rObj.items[0].get();
fn();
});
watchReactive(() => {
rObj.get();
fn1();
});
rObj.items.set([{ a: 1 }, 3, 4]);
expect(fn).toHaveBeenCalledTimes(2);
// 父数据也会触发
expect(fn1).toHaveBeenCalledTimes(2);
});
it('数组中的数据由“对象”变成“数组”', () => {
let fn = jest.fn();
let fn1 = jest.fn();
const rObj = reactive({
items: [{ a: 1 }, 2, 3],
});
watchReactive(() => {
rObj.items[0].get();
fn();
});
watchReactive(() => {
rObj.get();
fn1();
});
rObj.items.set([[1], 3, 4]);
expect(fn).toHaveBeenCalledTimes(2);
// 父数据也会触发
expect(fn1).toHaveBeenCalledTimes(2);
});
it('数组中的数据由“空数组”变成“空数组”', () => {
let fn = jest.fn();
let fn1 = jest.fn();
const rObj = reactive({
items: [[], 2, 3],
});
watchReactive(() => {
rObj.items[0].get();
fn();
});
watchReactive(() => {
rObj.get();
fn1();
});
rObj.items.set([[], 3, 4]);
expect(fn).toHaveBeenCalledTimes(2);
// 父数据也会触发
expect(fn1).toHaveBeenCalledTimes(2);
});
it('数组中的2个数据由“对象”变成“对象”', () => {
let fn = jest.fn();
let fn1 = jest.fn();
const rObj = reactive({
items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3],
});
watchReactive(() => {
rObj.items[0].a.get();
rObj.items[0].b.get();
fn();
});
watchReactive(() => {
rObj.get();
fn1();
});
// 第一个a 由{b: 1} -> {b: 2}能够精准更新
rObj.items.set([{ a: { b: 2 }, b: { c: 3 } }, { a: 3 }, 4]);
expect(fn).toHaveBeenCalledTimes(2);
// 父数据也会触发
expect(fn1).toHaveBeenCalledTimes(2);
});
it('数组中的2个数据由“对象”变成“对象”前一个属性能精准更新会触发后面那个', () => {
let fn = jest.fn();
let fn1 = jest.fn();
let fn2 = jest.fn();
let fn3 = jest.fn();
let fn4 = jest.fn();
const rObj = reactive({
items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3],
});
watchReactive(() => {
rObj.items[0].a.get();
fn();
});
watchReactive(() => {
// b由 { c: 2 } -> { c: 3 } 可以触发
rObj.items[0].b.get();
fn1();
});
watchReactive(() => {
// b由 1 -> undefined 可以触发
rObj.items[0].a.b.get();
fn2();
});
watchReactive(() => {
// c由 2 -> 3 可以触发
rObj.items[0].b.c.get();
fn3();
});
watchReactive(() => {
rObj.get();
fn4();
});
// 第一个a 由{b: 1} -> {d: 2}能够精准更新
rObj.items.set([{ a: { d: 2 }, b: { c: 3 } }, { a: 3 }, 4]);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn1).toHaveBeenCalledTimes(2);
expect(fn2).toHaveBeenCalledTimes(2);
expect(fn3).toHaveBeenCalledTimes(2);
// 父数据也会触发
expect(fn4).toHaveBeenCalledTimes(2);
});
it('数组中的2个数据由“对象”变成“对象”前一个属性不能精准更新也不再触发后面那个', () => {
let fn = jest.fn();
let fn1 = jest.fn();
let fn2 = jest.fn();
let fn3 = jest.fn();
let fn4 = jest.fn();
const rObj = reactive({
items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3],
});
watchReactive(() => {
rObj.items[0].a.get();
fn();
});
watchReactive(() => {
// b由 { c: 2 } -> { c: 3 } 可以触发
rObj.items[0].b.get();
fn1();
});
watchReactive(() => {
rObj.items[0].a.b.get();
fn2();
});
watchReactive(() => {
rObj.items[0].b.c.get();
fn3();
});
watchReactive(() => {
rObj.get();
fn4();
});
// 第一个 a 由{b: 1} -> 1 不能够精准更新
rObj.items.set([{ a: 1, b: { c: 3 } }, { a: 3 }, 4]);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn1).toHaveBeenCalledTimes(2);
// 由 { b: 1 } -> 1 是不会触发 b 精准更新
expect(fn2).toHaveBeenCalledTimes(1);
// 前一个属性不能精准更新,也不触发后面那个的精准更新
expect(fn3).toHaveBeenCalledTimes(1);
// 父数据也会触发
expect(fn4).toHaveBeenCalledTimes(2);
});
it('数组中的2个数据由“对象”变成“[]”,前一个属性不能精准更新,也不再触发后面那个', () => {
let fn = jest.fn();
let fn1 = jest.fn();
let fn2 = jest.fn();
let fn3 = jest.fn();
let fn4 = jest.fn();
const rObj = reactive({
items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3],
});
watchReactive(() => {
rObj.items[0].a.get();
fn();
});
watchReactive(() => {
// b由 { c: 2 } -> { c: 3 } 可以触发
rObj.items[0].b.get();
fn1();
});
watchReactive(() => {
rObj.items[0].a.b.get();
fn2();
});
watchReactive(() => {
rObj.items[0].b.c.get();
fn3();
});
watchReactive(() => {
rObj.get();
fn4();
});
// 第一个 a 由{b: 1} -> [] 不能够精准更新
rObj.items.set([{ a: [], b: { c: 3 } }, { a: 3 }, 4]);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn1).toHaveBeenCalledTimes(2);
// 由 { b: 1 } -> 1 是不会触发 b 精准更新
expect(fn2).toHaveBeenCalledTimes(1);
// 前一个属性不能精准更新,也不触发后面那个的精准更新
expect(fn3).toHaveBeenCalledTimes(1);
// 父数据也会触发
expect(fn4).toHaveBeenCalledTimes(2);
});
it('数组中的2个数据由“对象”变成“null”前一个属性不能精准更新也不再触发后面那个', () => {
let fn = jest.fn();
let fn1 = jest.fn();
let fn2 = jest.fn();
let fn3 = jest.fn();
let fn4 = jest.fn();
const rObj = reactive({
items: [{ a: { b: 1 }, b: { c: 2 } }, { a: 2 }, 3],
});
watchReactive(() => {
rObj.items[0].a.get();
fn();
});
watchReactive(() => {
// b由 { c: 2 } -> { c: 3 } 可以触发
rObj.items[0].b.get();
fn1();
});
watchReactive(() => {
rObj.items[0].a.b.get();
fn2();
});
watchReactive(() => {
rObj.items[0].b.c.get();
fn3();
});
watchReactive(() => {
rObj.get();
fn4();
});
// 第一个 a 由{b: 1} -> [] 不能够精准更新
rObj.items.set([{ a: null, b: { c: 3 } }, { a: 3 }, 4]);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn1).toHaveBeenCalledTimes(2);
// 由 { b: 1 } -> 1 是不会触发 b 精准更新
expect(fn2).toHaveBeenCalledTimes(1);
// 前一个属性不能精准更新,也不触发后面那个的精准更新
expect(fn3).toHaveBeenCalledTimes(1);
// 父数据也会触发
expect(fn4).toHaveBeenCalledTimes(2);
});
it('执行基本数据数组的loop方法', () => {
let fn = jest.fn();
let fn1 = jest.fn();
const rObj = reactive({
items: [1, 2, 3, 4],
});
rObj.items.forEach(rItem => {
expect(isReactiveProxy(rItem)).toBeTruthy();
});
watchReactive(() => {
rObj.items.get();
fn();
});
watchReactive(() => {
rObj.get();
fn1();
});
rObj.items.set([1, 2, 3]);
expect(fn).toHaveBeenCalledTimes(2);
// 父数据也会触发
expect(fn1).toHaveBeenCalledTimes(2);
});
});

View File

@ -0,0 +1,266 @@
/*
* 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 Inula, { render, createRef, useState, useReactive, useCompute } from '../../../src/index';
describe('测试 useReactive(原生数据)', () => {
it('reactive.get()作为children', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('1');
rObj = _rObj;
fn();
return <div ref={ref}>{_rObj.get()}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('1');
rObj.set('2');
expect(ref.current.innerHTML).toEqual('2');
expect(fn).toHaveBeenCalledTimes(2);
});
it('reactive作为children', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('1');
rObj = _rObj;
fn();
return <div ref={ref}>{_rObj}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('1');
rObj.set('2');
expect(ref.current.innerHTML).toEqual('2');
rObj.set(prev => prev + '??');
expect(ref.current.innerHTML).toEqual('2??');
expect(fn).toHaveBeenCalledTimes(1);
});
it('reactive.get()作为prop', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive(1);
rObj = _rObj;
fn();
return <div ref={ref} className={_rObj.get()}></div>;
};
render(<App />, container);
expect(ref.current.className).toEqual('1');
rObj.set(2);
expect(ref.current.className).toEqual('2');
expect(fn).toHaveBeenCalledTimes(2);
});
it('reactive作为prop', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive(1);
rObj = _rObj;
fn();
return <div ref={ref} className={_rObj}></div>;
};
render(<App />, container);
expect(ref.current.className).toEqual('1');
rObj.set(2);
expect(ref.current.className).toEqual('2');
expect(fn).toHaveBeenCalledTimes(1);
});
it('reactive.get()传入style', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('blue');
rObj = _rObj;
fn();
return <div ref={ref} style={{ color: _rObj.get() }}></div>;
};
render(<App />, container);
const style = window.getComputedStyle(ref.current);
expect(style.color).toEqual('blue');
rObj.set('red');
expect(ref.current.style.color).toEqual('red');
expect(fn).toHaveBeenCalledTimes(2);
});
it('reactive传入style', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('blue');
rObj = _rObj;
fn();
return <div ref={ref} style={{ color: _rObj }}></div>;
};
render(<App />, container);
const style = window.getComputedStyle(ref.current);
expect(style.color).toEqual('blue');
rObj.set('red');
expect(ref.current.style.color).toEqual('red');
expect(fn).toHaveBeenCalledTimes(1);
});
it('reactive传入Input value', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('blue');
rObj = _rObj;
fn();
return <input ref={ref} value={_rObj}></input>;
};
render(<App />, container);
expect(ref.current.value).toEqual('blue');
rObj.set('red');
expect(ref.current.value).toEqual('red');
expect(fn).toHaveBeenCalledTimes(1);
});
it('reactive传入Textarea value', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('blue');
rObj = _rObj;
fn();
return <textarea ref={ref} value={_rObj}></textarea>;
};
render(<App />, container);
expect(ref.current.value).toEqual('blue');
rObj.set('red');
expect(ref.current.value).toEqual('red');
expect(fn).toHaveBeenCalledTimes(1);
});
it('reactive父组件刷新 effect不应该重新监听', () => {
let rObj, update;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const [_, setState] = useState({});
update = () => setState({});
return <Child />;
};
const Child = () => {
const _rObj = useReactive('blue');
rObj = _rObj;
fn();
return <div ref={ref} className={_rObj}></div>;
};
render(<App />, container);
expect(ref.current.className).toEqual('blue');
expect(fn).toHaveBeenCalledTimes(1);
update();
expect(fn).toHaveBeenCalledTimes(2);
rObj.set('red');
expect(fn).toHaveBeenCalledTimes(2);
expect(ref.current.className).toEqual('red');
});
it('不允许:从“原生数据”变成“对象”', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive('1');
rObj = _rObj;
fn();
const cp = useCompute(() => {
return _rObj.get() === '1' ? '1' : _rObj.data.get();
});
return <div ref={ref}>{cp}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('1');
// 不允许:从“原生数据”变成“对象”
expect(() => rObj.set({ data: '2' })).toThrow(Error('Not allowed Change Primitive to Object'));
});
it('允许一个reactive属性从“原生数据”变成“对象”', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({
data: '1',
});
rObj = _rObj;
fn();
const cp = useCompute(() => {
return _rObj.data.get() === '1' ? '1' : _rObj.data.num.get();
});
return <div ref={ref}>{cp}</div>;
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('1');
rObj.data.set({ num: '2' });
expect(ref.current.innerHTML).toEqual('2');
expect(fn).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,96 @@
/*
* 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 Inula, { createRef, render, useReactive } from '../../../src/index';
describe('测试在DOM的props中使用响应式数据', () => {
it('在class props中使用响应式数据', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({ class: 'c1', color: 'blue' });
rObj = _rObj;
fn();
return (
<div ref={ref} className={_rObj.class}>
{_rObj.color}
</div>
);
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('blue');
expect(ref.current.getAttribute('class')).toEqual('c1');
rObj.class.set('c2');
expect(ref.current.getAttribute('class')).toEqual('c2');
expect(fn).toHaveBeenCalledTimes(1);
});
it('在style中使用响应式数据', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({ class: 'c1', color: 'blue' });
rObj = _rObj;
fn();
return (
<div ref={ref} style={{ color: _rObj.color }}>
{_rObj.color}
</div>
);
};
render(<App />, container);
expect(ref.current.innerHTML).toEqual('blue');
expect(ref.current.getAttribute('style')).toEqual('color: blue;');
rObj.color.set('red');
expect(ref.current.getAttribute('style')).toEqual('color: red;');
expect(fn).toHaveBeenCalledTimes(1);
});
it('在input中使用响应式数据', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive({ class: 'c1', color: 'blue' });
rObj = _rObj;
fn();
return <input ref={ref} value={_rObj.color}></input>;
};
render(<App />, container);
expect(ref.current.value).toEqual('blue');
rObj.color.set('red');
expect(ref.current.value).toEqual('red');
expect(fn).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,122 @@
import Inula, {render, createRef, useReactive, useWatch, useCompute, For} from '../../../src/index';
describe('测试 watch', () => {
it('watch 一个参数', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive(1);
useWatch(() => {
_rObj.get();
fn();
});
rObj = _rObj;
return <div ref={ref}>{_rObj}</div>;
};
render(<App />, container);
rObj.set('2');
expect(fn).toHaveBeenCalledTimes(2);
});
it('watch 2个参数', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive(1);
useWatch(_rObj, () => {
fn();
});
rObj = _rObj;
return <div ref={ref}>{_rObj}</div>;
};
render(<App />, container);
rObj.set('2');
expect(fn).toHaveBeenCalledTimes(1);
});
it('watch 2个参数第一个是函数', () => {
let rObj;
const ref = createRef();
const fn = jest.fn();
const App = () => {
const _rObj = useReactive(1);
useWatch(
() => {
_rObj.get();
},
() => {
fn();
}
);
rObj = _rObj;
return <div ref={ref}>{_rObj}</div>;
};
render(<App />, container);
rObj.set('2');
expect(fn).toHaveBeenCalledTimes(1);
});
it('响应式数据的孩子变更watch也应该被触发', () => {
let rObj;
let ref = createRef();
let fn = jest.fn();
let appFn = jest.fn();
let itemFn = jest.fn();
const App = () => {
const _rObj = useReactive([
{ id: 'id-1', name: 'p1' },
{ id: 'id-2', name: 'p2' },
{ id: 'id-3', name: 'p3' },
]);
rObj = _rObj;
useWatch(() => {
_rObj.get();
fn();
});
appFn();
return (
<div ref={ref}>
<For each={_rObj}>
{item => {
itemFn();
return (
<li id={item.id} key={item.id}>
{item.name}
</li>
);
}}
</For>
</div>
);
};
render(<App />, container);
let items = container.querySelectorAll('li');
expect(items.length).toEqual(3);
expect(fn).toHaveBeenCalledTimes(1);
rObj.push({ id: 'id-4', name: 'p4' });
items = container.querySelectorAll('li');
expect(items.length).toEqual(4);
expect(fn).toHaveBeenCalledTimes(2);
rObj[1].set({ id: 'id-2', name: 'p222' });
let li = container.querySelector('#id-2');
expect(li.innerHTML).toEqual('p222');
expect(fn).toHaveBeenCalledTimes(3);
});
});

View File

@ -23,7 +23,7 @@ import execute from 'rollup-plugin-execute';
import { terser } from 'rollup-plugin-terser';
import { version as inulaVersion } from '../../package.json';
const extensions = ['.js', '.ts'];
const extensions = ['.js', '.ts', '.tsx'];
const libDir = path.join(__dirname, '../..');
const rootDir = path.join(__dirname, '../..');

View File

@ -188,7 +188,7 @@ export function submitDomUpdate(tag: string, vNode: VNode) {
}
export function clearText(dom: Element): void {
dom.innerHTML = '';
dom.textContent = '';
}
// 添加child元素

View File

@ -19,6 +19,14 @@ import { setStyles } from './StyleHandler';
import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding';
import { isEventProp } from '../validators/ValidateProps';
import { getCurrentRoot } from '../../renderer/RootStack';
import { getValue, isReactiveObj} from '../../reactive/Utils';
import { handleReactiveProp } from '../../reactive/RContextCreator';
import { ReactiveProxy } from '../../reactive/types';
export function unwrapVal(propName: string, propVal: any, dom: Element, styleName?: string) {
const rawVal: any = handleReactiveProp(dom, propName, propVal, styleName);
return rawVal;
}
// 初始化DOM属性和更新 DOM 属性
export function setDomProps(dom: Element, props: Record<string, any>, isNativeTag: boolean, isInit: boolean): void {
@ -43,8 +51,8 @@ export function setDomProps(dom: Element, props: Record<string, any>, isNativeTa
} else if (propName === 'children') {
// 只处理纯文本子节点其他children在VNode树中处理
const type = typeof propVal;
if (type === 'string' || type === 'number') {
dom.textContent = propVal;
if (type === 'string' || type === 'number' || isReactiveObj(propVal)) {
dom.textContent = unwrapVal(propName, propVal, dom);
}
} else if (propName === 'dangerouslySetInnerHTML') {
dom.innerHTML = propVal.__html;
@ -55,9 +63,9 @@ export function setDomProps(dom: Element, props: Record<string, any>, isNativeTa
}
// 找出两个 DOM 属性的差别,生成需要更新的属性集合
export function compareProps(oldProps: Record<string, any>, newProps: Record<string, any>): Record<string, any> {
export function compareProps(oldProps: Record<string, any>, newProps: Record<string, any>, dom: Element): Record<string, any> {
let updatesForStyle = {};
const toUpdateProps = {};
const toUpdateProps: Record<string | number, any> = {};
const keysOfOldProps = Object.keys(oldProps);
const keysOfNewProps = Object.keys(newProps);
@ -103,11 +111,14 @@ export function compareProps(oldProps: Record<string, any>, newProps: Record<str
for (let i = 0; i < keysOfNewProps.length; i++) {
propName = keysOfNewProps[i];
newPropValue = newProps[propName];
oldPropValue = oldProps !== null && oldProps !== undefined ? oldProps[propName] : null;
oldPropValue = oldProps != null ? oldProps[propName] : null;
if (
newPropValue === oldPropValue ||
((newPropValue === null || newPropValue === undefined) && (oldPropValue === null || oldPropValue === undefined))
(newPropValue == null && oldPropValue == null) ||
(isReactiveObj(newPropValue) &&
isReactiveObj(oldPropValue) &&
getValue(newPropValue) === getValue(oldPropValue))
) {
// 新旧属性值未发生变化,或者新旧属性皆为空值,不需要进行处理
continue;
@ -143,31 +154,49 @@ export function compareProps(oldProps: Record<string, any>, newProps: Record<str
} else if (propName === 'dangerouslySetInnerHTML') {
newHTML = newPropValue ? newPropValue.__html : undefined;
oldHTML = oldPropValue ? oldPropValue.__html : undefined;
if (newHTML !== null && newHTML !== undefined) {
if (newHTML != null) {
if (oldHTML !== newHTML) {
toUpdateProps[propName] = newPropValue;
appendToUpdateProps(toUpdateProps, propName, newPropValue, dom);
}
}
} else if (propName === 'children') {
if (typeof newPropValue === 'string' || typeof newPropValue === 'number') {
toUpdateProps[propName] = String(newPropValue);
if (typeof newPropValue === 'string' || typeof newPropValue === 'number' || isReactiveObj(newPropValue)) {
appendToUpdateProps<string | number | ReactiveProxy<any>, string>(
toUpdateProps,
propName,
newPropValue,
dom,
String
);
}
} else if (isEventProp(propName)) {
const currentRoot = getCurrentRoot();
if (!allDelegatedInulaEvents.has(propName)) {
toUpdateProps[propName] = newPropValue;
appendToUpdateProps(toUpdateProps, propName, newPropValue, dom);
} else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) {
lazyDelegateOnRoot(currentRoot, propName);
}
} else {
toUpdateProps[propName] = newPropValue;
appendToUpdateProps(toUpdateProps, propName, newPropValue, dom);
}
}
// 处理style
if (Object.keys(updatesForStyle).length > 0) {
toUpdateProps['style'] = updatesForStyle;
appendToUpdateProps(toUpdateProps, 'style', updatesForStyle, dom);
}
return toUpdateProps;
}
function appendToUpdateProps<V, R>(
toUpdateProps: Record<string | number, any>,
propName: string,
propVal: V,
dom: Element,
formatter?: (value: V) => R
) {
const rawVal: any = handleReactiveProp(dom, propName, propVal);
toUpdateProps[propName] = formatter ? formatter(rawVal) : rawVal;
}

View File

@ -13,6 +13,8 @@
* See the Mulan PSL v2 for more details.
*/
import { unwrapVal } from './DOMPropertiesHandler';
/**
* css
*/
@ -85,12 +87,14 @@ export function setStyles(dom, styles) {
const style = dom.style;
Object.keys(styles).forEach(name => {
const styleVal = styles[name];
const val = unwrapVal('style', styleVal, dom, name);
// 以--开始的样式直接设置即可
if (name.indexOf('--') === 0) {
style.setProperty(name, styleVal);
style.setProperty(name, val);
} else {
// 使用这种赋值方式,浏览器可以将'WebkitLineClamp' 'backgroundColor'分别识别为'-webkit-line-clamp'和'backgroud-color'
style[name] = adjustStyleValue(name, styleVal);
style[name] = adjustStyleValue(name, val);
}
});
}

View File

@ -18,6 +18,7 @@ import { isInvalidValue } from '../validators/ValidateProps';
import { getNamespaceCtx } from '../../renderer/ContextSaver';
import { NSS } from '../utils/DomCreator';
import { getDomTag } from '../utils/Common';
import { unwrapVal } from './DOMPropertiesHandler';
// 不需要装换的svg属性集合
const svgHumpAttr = new Set();
@ -120,7 +121,7 @@ export function updateCommonProp(dom: Element, attrName: string, value: any, isN
if (value === null) {
dom.removeAttribute(attrName);
} else {
dom.setAttribute(attrName, String(value));
dom.setAttribute(attrName, String(unwrapVal(attrName, value, dom)));
}
} else if (['checked', 'multiple', 'muted', 'selected'].includes(propDetails.attrName)) {
if (value === null) {
@ -141,7 +142,7 @@ export function updateCommonProp(dom: Element, attrName: string, value: any, isN
// 即可以用作标志又可以是属性值的属性
attributeValue = '';
} else {
attributeValue = String(value);
attributeValue = String(unwrapVal(attrName, value, dom));
}
if (attrNS) {

View File

@ -15,6 +15,8 @@
import { updateCommonProp } from '../DOMPropertiesHandler/UpdateCommonProp';
import { Props } from '../utils/Interface';
import { getValue } from '../../reactive/Utils';
import { handleReactiveProp } from '../../reactive/RContextCreator';
function getInitValue(dom: HTMLInputElement, props: Props) {
const { value, defaultValue, checked, defaultChecked } = props;
@ -45,10 +47,12 @@ export function getInputPropsWithoutValue(dom: HTMLInputElement, props: Props) {
export function updateInputValue(dom: HTMLInputElement, props: Props) {
const { value, checked } = props;
if (value !== undefined) {
const val = getValue(value);
if (val !== undefined) {
// 处理 dom.value 逻辑
if (dom.value !== String(value)) {
dom.value = String(value);
if (dom.value !== String(val)) {
dom.value = String(val);
}
} else if (checked !== undefined) {
updateCommonProp(dom, 'checked', checked, true);
@ -62,7 +66,9 @@ export function setInitInputValue(dom: HTMLInputElement, props: Props) {
if (value !== undefined || defaultValue !== undefined) {
// value 的使用优先级 value 属性 > defaultValue 属性 > 空字符串
const initValueStr = String(initValue);
const initValueStr = getValue(initValue);
handleReactiveProp(dom, 'value', value);
dom.value = initValueStr;

View File

@ -14,6 +14,8 @@
*/
import { Props } from '../utils/Interface';
import { getValue } from '../../reactive/Utils';
import { handleReactiveProp } from '../../reactive/RContextCreator';
// 值的优先级 value > children > defaultValue
function getInitValue(props: Props) {
@ -48,7 +50,9 @@ export function updateTextareaValue(dom: HTMLTextAreaElement, props: Props, isIn
if (isInit) {
const initValue = getInitValue(props);
if (initValue !== '') {
dom.value = initValue;
dom.value = getValue(initValue);
handleReactiveProp(dom, 'value', props.value);
}
} else {
// 获取当前节点的 value 值

View File

@ -18,6 +18,7 @@ import { getProcessingClassVNode } from '../renderer/GlobalVar';
import { Source } from '../renderer/Types';
import { BELONG_CLASS_VNODE_KEY } from '../renderer/vnode/VNode';
import { InulaElement, KVObject } from '../types';
import { isReactiveObj } from '../reactive/Utils';
/**
* vtype element
@ -75,7 +76,17 @@ const keyArray = ['key', 'ref', '__source', '__self'];
function buildElement(isClone, type, setting, children) {
// setting中的值优先级最高clone情况下从 type 中取值,创建情况下直接赋值为 null
const key = setting && setting.key !== undefined ? String(setting.key) : isClone ? type.key : null;
let key;
if (setting && setting.key !== undefined) {
if (isReactiveObj(setting.key)) {
key = setting.key;
} else {
key = String(setting.key);
}
} else {
key = isClone ? type.key : null;
}
const ref = setting && setting.ref !== undefined ? setting.ref : isClone ? type.ref : null;
const props = isClone ? { ...type.props } : {};
let vNode = isClone ? type[BELONG_CLASS_VNODE_KEY] : getProcessingClassVNode();

View File

@ -43,6 +43,11 @@ import {
useRef,
useState,
useDebugValue,
useAtom,
useCompute,
useComputed,
useReactive,
useWatch,
} from './renderer/hooks/HookExternal';
import {
isContextProvider,
@ -72,6 +77,15 @@ import {
import { syncUpdates as flushSync } from './renderer/TreeBuilder';
import { toRaw } from './inulax/proxy/ProxyHandler';
import { For } from './reactive/components/For';
import { Show } from './reactive/components/Show';
import { Switch } from './reactive/components/Switch';
import { RText } from './reactive/components/RText';
import { reactive } from './reactive/Reactive';
import { computed } from './reactive/Computed';
import { isReactiveObj } from './reactive/Utils';
import { watch as watchReactive } from './reactive/Watch';
const Inula = {
Children,
createRef,
@ -122,6 +136,20 @@ const Inula = {
Profiler,
StrictMode,
Suspense,
// reactive
reactive,
computed,
watchReactive,
isReactiveObj,
For,
Show,
Switch,
RText,
useAtom,
useReactive,
useCompute,
useComputed,
useWatch,
};
export const version = __VERSION__;
@ -178,6 +206,20 @@ export {
Profiler,
StrictMode,
Suspense,
// reactive
reactive,
computed,
watchReactive,
isReactiveObj,
For,
Show,
Switch,
RText,
useAtom,
useReactive,
useCompute,
useComputed,
useWatch,
};
export * from './types';

View File

@ -0,0 +1,78 @@
/*
* 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 { RContextSet, triggerRContexts } from './RContext';
import { getRNodeVal, trackReactiveData } from './RNode';
import { isFunction, isObject } from './Utils';
import {AtomNode, RNode, Root, RootRNode} from './types';
export interface Atom<T = any> extends AtomNode<T> {
root: Root<T>;
parent: RNode;
parentKey: string | symbol | null;
usedRContexts?: RContextSet;
}
const atomKey = Symbol('atomAccessKey');
export const atomSymbol = Symbol('ReactiveAtom');
// 对原始数据做响应式的时候使用
export function Atom<T>(value: T, parent: RNode | null = null, parentKey: string | symbol | null = null) {
if (parent === null && parentKey === null) {
this.parent = {
parent: null,
parentKey: null,
root: {
$: { [atomKey]: value },
},
type: atomSymbol,
} as RootRNode<T>;
this.parentKey = atomKey;
this.root = this.parent.root;
} else {
this.parent = parent;
this.parentKey = parentKey;
this.root = this.parent.root;
}
}
Atom.prototype.get = function <T>(): T {
trackReactiveData(this);
return this.read();
};
Atom.prototype.set = function <T>(value: T | ((prev: T) => T)) {
// 修改Atom值与父元素值
const prevParent = getRNodeVal(this.parent);
const prevValue = prevParent[this.parentKey];
const newValue = isFunction(value) ? value(prevValue) : value;
// 如果要改为非原始对象切父元素类型不为atomSymbol说明该对象是Atom对象不允许从“原生数据”变成“对象”
if (this.parent.type === atomSymbol && isObject(newValue)) {
throw Error('Not allowed Change Primitive to Object');
}
// 1) 修改Node底层原始值
prevParent[this.parentKey] = newValue;
// 2) 触发使用到它的RContexts
triggerRContexts(this, prevValue, newValue, false);
return this;
};
Atom.prototype.read = function <T>(): T {
return getRNodeVal(this);
};

View File

@ -0,0 +1,92 @@
/*
* 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 { RContextCallback, RContextParam, Reactive } from './types';
import { VNode } from '../renderer/Types';
import { updateShouldUpdateOfTree } from '../renderer/vnode/VNodeShouldUpdate';
export interface BatchItem {
callback: RContextCallback;
params: RContextParam;
reactive: Reactive;
}
let batchCount = 0;
let _batch: BatchItem[] = [];
let _batchMap = new Map();
export function addToBatch(item: BatchItem) {
if (batchCount > 0) {
const existing = _batchMap.get(item.callback);
if (existing) {
// const params = existing.params;
// params.value = item.params.value;
} else {
_batch.push(item);
_batchMap.set(item.callback, true);
}
} else {
item.callback(item.params, item.reactive);
}
}
export function startBatch() {
batchCount++;
}
export function endBatch() {
batchCount--;
if (batchCount <= 0) {
batchCount = 0;
const batch = _batch;
_batch = [];
_batchMap = new Map();
const toUpdateVNodes: VNode[] = [];
const toUpdateVNodeItems: BatchItem[] = [];
for (let i = 0; i < batch.length; i++) {
const b = batch[i];
// 如果要刷新的是组件(函数组件或类组件)
if (b.params?.vNode) {
// 设置vNode为shouldUpdate
updateShouldUpdateOfTree(b.params.vNode);
b.params.vNode.isStoreChange = true;
toUpdateVNodes.push(b.params.vNode);
toUpdateVNodeItems.push(b);
}
}
for (let i = 0; i < batch.length; i++) {
const b = batch[i];
const { callback, reactive } = b;
if (!b.params?.vNode) {
callback(b.params, reactive);
}
}
if (toUpdateVNodes.length) {
const b = toUpdateVNodeItems[0];
const { callback, reactive } = b;
callback(b.params, reactive);
}
}
}

View File

@ -0,0 +1,55 @@
/*
* 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 { Computed } from './types';
import { createReactiveObj, setRNodeVal } from './RNode';
import { calculateReactive, getRNode, isPromise } from './Utils';
import { RContext } from './RContext';
function computed<T = any>(fn: () => T | Promise<T>): Computed<T> {
const setComputed = (value: any, trigger: boolean) => {
root.readOnly = false;
const raw = getRNode(rObj);
setRNodeVal(raw, value, trigger);
root.readOnly = true;
};
// 依赖的响应式数据变化时调用
const update = (trigger: boolean) => {
const value = calculateReactive(fn);
if (isPromise(value)) {
value.then(val => setComputed(val, trigger));
} else {
setComputed(value, trigger);
}
};
const rContext = new RContext(() => update(true));
const end = rContext.start();
// 首次更新不触发usedRContexts
const value = calculateReactive(fn);
end();
const rObj = createReactiveObj(value);
const rawNode = getRNode(rObj);
const root = rawNode.root;
// 默认readOnly为true
root.readOnly = true;
return rObj as Computed<T>;
}
export { computed };

View File

@ -0,0 +1,199 @@
/*
* 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.
*/
export enum Operation {
// 数组长度不同
Nop = 0,
Insert = 1,
Delete = 2,
// 数组长度相同
Update = 3,
Exchange = 4,
}
export interface Diff {
action: Operation;
index: number;
}
export interface DiffOperator {
isOnlyNop: boolean;
opts: Diff[];
}
function longestCommonPrefix<T>(arr1: T[], arr2: T[]): number {
if (!arr1.length || !arr2.length || arr1[0] !== arr1[0]) {
return 0;
}
let low = 0;
let start = 0;
// 最短数组的长度
let high = Math.min(arr1.length, arr2.length);
while (low <= high) {
const mid = (high + low) >> 1;
if (isArrayEqual(arr1, arr2, start, mid)) {
low = mid + 1;
start = mid;
} else {
high = mid - 1;
}
}
return low - 1;
}
function isArrayEqual<T>(str1: T[], str2: T[], start: number, end: number): boolean {
for (let j = start; j < end; j++) {
if (str1[j] !== str2[j]) {
return false;
}
}
return true;
}
/**
* @param origin
* @param target
* @returns Diff
*/
export function arrayDiff<T>(origin: T[], target: T[]): DiffOperator {
// 使用二分查找计算共同前缀与后缀
const prefixLen = longestCommonPrefix(origin, target);
const suffixLen = longestCommonPrefix([...origin].reverse(), [...target].reverse());
// 删除原数组与目标数组的共同前缀与后缀
const optimizedOrigin = origin.slice(prefixLen, origin.length - suffixLen);
const optimizedTarget = target.slice(prefixLen, target.length - suffixLen);
const originLen = optimizedOrigin.length;
const targetLen = optimizedTarget.length;
const dp: number[][] = Array.from(Array(originLen + 1), () => {
return Array(targetLen + 1).fill(0);
});
const pathMatrix: Operation[][] = Array.from(Array(originLen + 1), () => {
return Array(targetLen + 1).fill('');
});
let diffs: Diff[] = [];
// 计算最长公共子序列
for (let i = 1; i < originLen + 1; i++) {
for (let j = 1; j < targetLen + 1; j++) {
if (optimizedOrigin[i - 1] === optimizedTarget[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
// 如果相等,则表示不需要进行任何操作
pathMatrix[i][j] = Operation.Nop;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
dp[i][j] = dp[i - 1][j];
// 如果不相等,则需要进行删除操作
pathMatrix[i][j] = Operation.Delete;
} else {
dp[i][j] = dp[i][j - 1];
// 如果不相等,则需要进行插入操作
pathMatrix[i][j] = Operation.Insert;
}
}
}
let hasDelete = false;
let hasInsert = false;
// 计算操作序列
function diff(oLen: number, tLen: number) {
const stack: Record<string, number>[] = [{ i: oLen, j: tLen }];
while (stack.length > 0) {
const obj = stack.pop();
const { i, j } = obj!;
if (i === 0 || j === 0) {
if (i !== 0) {
diffs.unshift(
...optimizedOrigin.slice(0, i).map((item, idx) => ({
action: Operation.Delete,
index: idx,
}))
);
hasDelete = true;
}
if (j !== 0) {
diffs.unshift(
...optimizedTarget.slice(0, j).map((item, idx) => ({
action: Operation.Insert,
index: idx,
}))
);
hasInsert = true;
}
}
if (pathMatrix[i][j] === Operation.Nop) {
stack.push({ i: i - 1, j: j - 1 });
// 如果不需要进行任何操作,则表示是公共元素,将其添加到 diffs 中
diffs.unshift({ action: Operation.Nop, index: i - 1 });
} else if (pathMatrix[i][j] === Operation.Delete) {
stack.push({ i: i - 1, j });
// 如果需要进行删除操作,则将其添加到 diffs 中
diffs.unshift({ action: Operation.Delete, index: i - 1 });
hasDelete = true;
} else if (pathMatrix[i][j] === Operation.Insert) {
stack.push({ i, j: j - 1 });
// 如果需要进行插入操作,则将其添加到 diffs 中
diffs.unshift({ action: Operation.Insert, index: j - 1 });
hasInsert = true;
}
}
}
// 计算操作序列
diff(originLen, targetLen);
diffs.map(i => (i.index += prefixLen));
const prefixOpts = Array.from(Array(prefixLen), (_, index) => index).map(idx => ({
action: Operation.Nop,
index: idx,
}));
const suffixOpts = Array.from(Array(suffixLen), (_, index) => index).map(idx => ({
action: Operation.Nop,
index: origin.length - suffixLen + idx,
}));
diffs = prefixOpts.concat(diffs, suffixOpts);
return {
isOnlyNop: !hasDelete && !hasInsert,
opts: diffs,
};
}
// export function sameLenArrayDiff<T>(origin: T[], target: T[]): Diff[] {
// // 只要id相同就认为是同一个对象
// const diff: Diff[] = [];
// const hasId = origin.every(i => getIDField(i));
// const arrayLength = origin.length;
// if (hasId && arrayLength > 0) {
// const idField = getIDField(origin[0])!;
// for (let j = 0; j < target.length; j++) {
// if (origin[j][idField] === target[j][idField]) {
// diff.push({ action: Operation.Nop, index: j });
// } else {
// diff.push({ action: Operation.Update, index: j });
// }
// }
// return diff;
// }
// return arrayDiff(origin, target);
// }

View File

@ -0,0 +1,355 @@
/*
* 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 { isArray } from '../inulax/CommonUtils';
import { isObject, isPrimitive } from './Utils';
import { arrayDiff, DiffOperator, Operation } from './DiffUtils';
import { ArrayState, RContextCallback, RContextParam, Reactive, RNode } from './types';
import { getOrCreateChildNode } from './RNode';
import {addToBatch, startBatch, endBatch, BatchItem} from './Batch';
export let currentDependent: RContext | null = null;
const reactiveContextStack: RContext[] = [];
export type RContextSet = Set<RContext>;
/**
* 412Block级3watch/computed4DOM级
*/
export class RContext {
callback: RContextCallback;
// 记录该Context使用时的参数
params: RContextParam;
// 记录该RContext中使用到的Reactive中的RContextSet
reactiveDependents: Set<RContextSet> | null = null;
constructor(callback: RContextCallback, params: RContextParam = {}) {
this.callback = callback;
this.params = params;
}
setParam(params: RContextParam) {
this.params = params;
}
start() {
cleanupRContext(this);
currentDependent = this;
reactiveContextStack.push(this);
return endEffect;
}
}
function endEffect() {
reactiveContextStack.pop();
currentDependent = reactiveContextStack[reactiveContextStack.length - 1] ?? null;
}
// 清除 RContext和响应式数据的绑定双向清除
export function cleanupRContext(rContext: RContext) {
if (rContext.reactiveDependents !== null) {
for (const usedRContexts of rContext.reactiveDependents) {
usedRContexts.delete(rContext);
}
rContext.reactiveDependents.clear();
rContext.reactiveDependents = null;
}
}
// 绑定RContext和响应式数据双向绑定
export function bindReactiveWithContext(reactive: Reactive, rContext: RContext) {
if (reactive.usedRContexts === undefined) {
reactive.usedRContexts = new Set<RContext>();
}
reactive.usedRContexts.add(rContext);
if (rContext.reactiveDependents === null) {
rContext.reactiveDependents = new Set<RContextSet>();
}
rContext.reactiveDependents.add(reactive.usedRContexts);
}
// 递归触发依赖这reactive数据的所有RContext
export function triggerRContexts(reactive: Reactive, prevValue: any, value: any, isFromArrModify?: boolean) {
const isObj = isObject(value);
const isPrevObj = isObject(prevValue);
startBatch();
if (isObj && isPrevObj) {
triggerChildrenContexts(reactive as RNode, value, prevValue, isFromArrModify);
}
callRContexts(reactive);
// 触发父数据的RContext不希望触发组件刷新只触发computed和watch
triggerParents(reactive.parent);
endBatch();
}
function triggerParents(reactive: Reactive | null) {
if (reactive) {
// 在触发父数据的时候不希望触发组件刷新只触发computed和watch
callRContexts(reactive, true);
triggerParents(reactive.parent);
}
}
// 当value和prevValue都是对象或数组时才触发
function triggerChildrenContexts(rNode: RNode, value: any, prevValue: any, isFromArrModify?: boolean): boolean {
// 可以精准更新
let canPreciseUpdate = true;
const isArr = isArray(value);
const isPrevArr = isArray(prevValue);
// 1、变化来自数组的Modify方法某些行可能完全不变
if (isFromArrModify) {
// 获取数组间差异RNode只能增删不能修改修改会导致Effect不会随数据的位置变化
const diffOperator = arrayDiff(prevValue, value);
const states: ArrayState[] = [];
let childIndex = 0;
for (const opt of diffOperator.opts) {
switch (opt.action) {
// 从已有RNode中取值
case Operation.Nop: {
const childRNode = rNode.children?.get(String(opt.index));
// children没有使用时可以为undefined或没有该child
if (childRNode !== undefined) {
childRNode.parentKey = String(childIndex);
states.push(ArrayState.Fresh);
childIndex++;
// 删除旧的,重设新值。处理场景:元素还在,但是在数组中的位置变化了。
rNode.children?.delete(String(opt.index));
rNode.children?.set(childRNode.parentKey, childRNode);
}
break;
}
// 从Value中新建RNode
case Operation.Insert: {
getOrCreateChildNode(value[opt.index], rNode, String(opt.index));
states.push(ArrayState.NotFresh);
childIndex++;
break;
}
case Operation.Delete: {
rNode.children?.delete(String(opt.index));
break;
}
}
}
rNode.diffOperator = diffOperator;
if (!rNode.diffOperators) {
rNode.diffOperators = [];
}
rNode.diffOperators.push(diffOperator);
// 记录:新数据,哪些需要处理,哪些不需要
rNode.states = states;
// 数组长度不同确定会产生变化调用callDependents一次
callRContexts(rNode);
return canPreciseUpdate;
}
// 2、都是数组
if (isArr && isPrevArr) {
const minLen = Math.min(value.length, prevValue.length);
// 遍历数组或对象触发子数据的Effects
const canPreciseUpdates = updateSameLengthArray(rNode, value, prevValue, minLen);
const maxLen = Math.max(value.length, prevValue.length);
if (maxLen !== minLen || canPreciseUpdates.includes(false)) {
canPreciseUpdate = false;
}
// 在reactive中保存opts
const diffOperator: DiffOperator = {
isOnlyNop: false,
opts: [],
};
const states: ArrayState[] = [];
// 相同长度的部分
for (let i = 0; i < minLen; i++) {
diffOperator.opts.push({ action: Operation.Nop, index: i });
// 如果该行数据无法精准更新设置为NotFresh
states.push(canPreciseUpdates[i] ? ArrayState.Fresh : ArrayState.NotFresh);
}
// 超出部分:新增
if (value.length > prevValue.length) {
for (let i = minLen; i < maxLen; i++) {
diffOperator.opts.push({ action: Operation.Insert, index: i });
states.push(ArrayState.NotFresh);
getOrCreateChildNode(value[i], rNode, String(i));
}
} else if (value.length < prevValue.length) { // 减少部分:删除
for (let i = minLen; i < maxLen; i++) {
diffOperator.opts.push({ action: Operation.Delete, index: i });
states.push(ArrayState.NotFresh);
}
}
diffOperator.isOnlyNop = !states.includes(ArrayState.NotFresh);
rNode.diffOperator = diffOperator;
rNode.states = states;
return canPreciseUpdate;
}
// 都是对象
if (!isArr && !isPrevArr) {
const keys = Object.keys(value);
const prevKeys = Object.keys(prevValue);
// 合并keys和prevKeys
const keySet = new Set(keys.concat(prevKeys));
keySet.forEach((key) => {
const val = value[key];
const prevVal = prevValue[key];
const isChanged = val !== prevVal;
// 如果数据有变化就触发Effects
if (isChanged) {
const childRNode = rNode.children?.get(key);
const isObj = isObject(val);
const isPrevObj = isObject(prevVal);
// val和prevVal都是对象或数组
if (isObj) {
// 1、如果上一个属性无法精准更新就不再递归下一个属性了
// 2、如果childRNode为空说明这个数据未被引用过也不需要调用RContexts
if (canPreciseUpdate && childRNode !== undefined) {
canPreciseUpdate = triggerChildrenContexts(childRNode as RNode, val, prevVal);
}
} else if (!isObj && !isPrevObj) { // val和prevVal都不是对象或数组
canPreciseUpdate = true;
} else { // 类型不同(一个是对象或数组,另外一个不是)
canPreciseUpdate = false;
}
// 有childRNode说明这个数据被使引用过
if (childRNode !== undefined) {
callRContexts(childRNode);
}
}
});
return canPreciseUpdate;
}
// 一个是对象,一个是数组
canPreciseUpdate = false;
return canPreciseUpdate;
}
// 对于数组的变更,尽量尝试精准更新,会记录每行数据是否能够精准更新
function updateSameLengthArray(rNode: RNode, value: any, prevValue: any, len: number): boolean[] {
const canPreciseUpdates: boolean[] = [];
// 遍历数组或对象触发子数据的RContexts
for (let i = 0; i < len; i++) {
const val = value[i];
const prevVal = prevValue[i];
const isChanged = val !== prevVal;
// 如果数据有变化就触发RContexts
if (isChanged) {
const childRNode = rNode.children?.get(String(i));
const isObj = isObject(val);
const isPrevObj = isObject(prevVal);
// val和prevVal都是对象或数组时
if (isObj && isPrevObj) {
// 如果childRNode为空说明这个数据未被引用过也不需要调用RContexts
if (childRNode !== undefined) {
canPreciseUpdates[i] = triggerChildrenContexts(childRNode as RNode, val, prevVal);
}
} else if (!isObj && !isPrevObj) { // val和prevVal都不是对象或数组
canPreciseUpdates[i] = true;
} else { // 类型不同(一个是对象或数组,另外一个不是)
canPreciseUpdates[i] = false;
}
// 有childRNode说明这个数据被使引用过
if (childRNode) {
callRContexts(childRNode);
}
} else {
canPreciseUpdates[i] = true;
}
}
return canPreciseUpdates;
}
// 调用响应式数据reactive所收集的依赖RContext
function callRContexts(reactive: Reactive, isNoComponentRContext = false) {
if (reactive.usedRContexts !== undefined && reactive.usedRContexts.size) {
// Array.from 浅克隆防止callback中扩缩usedRContexts数组
const usedRContexts = Array.from<RContext>(reactive.usedRContexts);
const len = usedRContexts.length;
let rContext: RContext;
for (let i = 0; i < len; i++) {
rContext = usedRContexts[i];
// dep.callback可能被清除
if (rContext.callback) {
// 在触发父数据的时候,不希望触发组件刷新
if (isNoComponentRContext && rContext.params.vNode) {
continue;
}
const batchItem: BatchItem = {
callback: rContext.callback,
params: rContext.params,
reactive: reactive,
};
addToBatch(batchItem);
}
}
}
}
/**
* Reactive的dependents
* @param reactive
*/
export function disposeReactive(reactive: Reactive) {
if (reactive.usedRContexts) {
const usedRContexts = reactive.usedRContexts;
for (const rContext of usedRContexts) {
cleanupRContext(rContext);
}
delete reactive.usedRContexts;
}
}

View File

@ -0,0 +1,202 @@
/*
* 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 { launchUpdateFromVNode } from '../renderer/TreeBuilder';
import { bindReactiveWithContext, cleanupRContext, RContext } from './RContext';
import { For } from './components/For';
import { VNode } from '../renderer/Types';
import { RContextParam, Reactive, RNode } from './types';
import { getRNodeVal } from './RNode';
import { getVNode } from '../dom/DOMInternalKeys';
import { updateInputValue } from '../dom/valueHandler/InputValueHandler';
import { updateTextareaValue } from '../dom/valueHandler/TextareaValueHandler';
import { setDomProps } from '../dom/DOMPropertiesHandler/DOMPropertiesHandler';
import { getRNodeFromProxy, isAtom, isReactiveProxy } from './Utils';
const vNodeEffectMap = new WeakMap<VNode, RContext>();
/**
* Class组件Dependent
* @param renderFn Class的render
* @param vNode
*/
export function createComponentDependent<T>(renderFn: () => T, vNode: VNode): T {
let compRContext = vNode.compRContext;
if (!compRContext) {
compRContext = new RContext(
(params, reactive) => {
vNode.isStoreChange = true;
// 如果是For组件
if (For === vNode.type && reactive.diffOperator) {
// 如果Reactive的For组件
const { isOnlyNop } = reactive.diffOperator;
// 如果没有需要处理
if (isOnlyNop) {
return;
}
}
// 同步刷新
// syncUpdates(() =>{
// // 触发vNode更新
// launchUpdateFromVNode(vNode);
// });
// 触发vNode更新
launchUpdateFromVNode(vNode);
},
{ vNode }
);
vNode.compRContext = compRContext;
}
const endRContext = compRContext.start();
const result = renderFn();
endRContext();
return result;
}
/**
* DOM的属性props或childrencallback
* @param dom DOM元素
* @param propName
* @param propVal
* @param styleName style里面的某个属性
*/
function subscribeAttr(dom: Element, propName: string, propVal: Reactive, styleName?: string) {
const attrRContext = new RContext(
params => {
let changeList;
if (propName === 'style' && styleName) {
changeList = {
style: {
[styleName]: getRNodeVal(params.reactive!),
},
};
} else {
changeList = {
[propName]: getRNodeVal(params.reactive!),
};
}
const type = getVNode(dom)?.type;
if (type === 'input' && propName === 'value') {
updateInputValue(dom as HTMLInputElement, changeList);
} else if (type === 'textarea' && propName === 'value') {
updateTextareaValue(dom as HTMLTextAreaElement, changeList);
} else {
setDomProps(dom, changeList, true, false);
}
},
{ reactive: propVal }
);
bindReactiveWithContext(propVal, attrRContext);
// vNode保存RContext用于cleanup
const vNode = getVNode(dom);
saveAttrRContexts(vNode, attrRContext);
}
export function handleReactiveProp(dom: Element, propName: string, propVal: any, styleName?: string): any {
let rawVal = propVal;
const isA = isAtom(propVal);
const isProxy = isReactiveProxy(propVal);
if (isA || isProxy) {
let reactive = propVal;
if (isProxy) {
reactive = getRNodeFromProxy(propVal);
}
rawVal = getRNodeVal(reactive as Reactive);
subscribeAttr(dom, propName, reactive, styleName);
}
return rawVal;
}
/**
* DOM Key Dependent
* @param callback VNode的key
* @param params Effect调用所需要的参数
*/
export function createKeyDependent(callback: (params: RContextParam) => void, params?: RContextParam) {
return new RContext(callback, params);
}
/**
* <div>Count: {_rObj}</div>
* @param textDom VNode的key
* @param rText Effect调用所需要的参数
*/
export function subscribeReactiveComponent(textDom: Element, rText: Reactive) {
const textContext = new RContext(
params => {
textDom.textContent = getRNodeVal(params.reactive as Reactive);
},
{ reactive: rText }
);
bindReactiveWithContext(rText, textContext);
// vNode保存RContext用于cleanup
const vNode = getVNode(textDom);
saveAttrRContexts(vNode, textContext);
}
// TODO 删除
export function cleanupVNodeEffect(vNode: VNode) {
const effect = vNodeEffectMap.get(vNode);
if (effect) {
cleanupRContext(effect);
vNodeEffectMap.delete(vNode);
}
}
/**
* DOM Key Dependent
* @param vNode
* @param rKey
*/
export function subscribeKeyEffect(vNode: VNode, rKey: RNode) {
const keyContext = new RContext(
params => {
vNode.key = getRNodeVal(rKey);
},
{ reactive: rKey }
);
bindReactiveWithContext(rKey, keyContext);
// vNode保存RContext用于cleanup
saveAttrRContexts(vNode, keyContext);
}
// vNode保存RContext用于cleanup
function saveAttrRContexts(vNode: VNode | null, rContext: RContext) {
if (vNode) {
if (!vNode.attrRContexts) {
vNode.attrRContexts = new Set();
}
vNode.attrRContexts.add(rContext);
}
}

View File

@ -0,0 +1,156 @@
/*
* 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 { bindReactiveWithContext, currentDependent, triggerRContexts } from './RContext';
import { isAtom, isFunction, isRNode, isPrimitive } from './Utils';
import { createProxy } from './proxy/RProxyHandler';
import { Atom } from './Atom';
import { isSame } from '../renderer/utils/compare';
import { ProxyRNode, Reactive, ReactiveProxy, RNode, RootRNode } from './types';
export const nodeSymbol = Symbol('ReactiveNode');
export function createReactiveObj<T extends any>(raw?: T): ReactiveProxy<T> {
if (isPrimitive(raw) || raw === null || raw === undefined) {
return new Atom(raw);
} else {
const node = createRootRNode(raw);
const proxyObj = createProxy<T>(node);
node.proxy = proxyObj;
return proxyObj as ReactiveProxy<T>;
}
}
export function createRootRNode<T extends any>(raw?: T): RootRNode<T> {
const root: RootRNode<T> = {
type: nodeSymbol,
root: { $: raw },
parent: null,
parentKey: null,
};
return root;
}
export function getOrCreateChildRNode(node: RNode, key: string | symbol): RNode {
let child = node.children?.get(key);
if (!child || isAtom(child)) {
child = {
type: nodeSymbol,
root: node.root,
parent: node,
parentKey: key,
};
if (!node.children) {
node.children = new Map();
}
node.children.set(key, child);
}
return child;
}
export function getOrCreateChildAtom(node: RNode, key: string | symbol, value: any): Atom {
const child = node.children?.get(key);
// 如果没有子节点或者子节点是RNode类型就创建一个新的Atom否则返回已存在的子节点
// 注意不能使用getNodeVal来判断之前节点的值因为根数据已被修改只能通过判断child的类型为
if (!child || isRNode(child)) {
const atom = new Atom(value, node, key);
if (!node.children) {
node.children = new Map();
}
node.children.set(key, atom);
return atom;
}
return child;
}
export function getOrCreateChildNode(value: unknown, parent: RNode, key: string | symbol): Atom | RNode {
let child: Atom | RNode;
// if (isPrimitive(value) || value === null || value === undefined) {
// child = getOrCreateChildAtom(parent, key, value);
// } else {
child = getOrCreateChildRNode(parent, key);
// }
return child;
}
export function getOrCreateChildProxy(value: unknown, parent: RNode, key: string | symbol): Atom | ProxyRNode<any> {
let child: Atom | RNode;
// if (isPrimitive(value) || value === null || value === undefined) {
// child = getOrCreateChildAtom(parent, key, value);
// return child;
// } else {
child = getOrCreateChildRNode(parent, key);
if (!child.proxy) {
child.proxy = createProxy(child);
}
return child.proxy;
// }
}
// 最终响应式数据的使用
export function trackReactiveData(reactive: Reactive) {
if (currentDependent !== null) {
bindReactiveWithContext(reactive, currentDependent);
}
}
export function getRNodeVal(node: Reactive): any {
let currentNode = node;
const keys: (string | symbol)[] = [];
while (currentNode.parentKey !== null && currentNode.parent !== null) {
keys.push(currentNode.parentKey);
currentNode = currentNode.parent;
}
let rawObj = node.root.$;
for (let i = keys.length - 1; i >= 0; i--) {
if (keys[i] !== undefined && rawObj) {
rawObj = rawObj[keys[i]];
}
}
return rawObj;
}
export function setRNodeVal(rNode: RNode, value: unknown, trigger = false, isArrayModified = false): void {
if (rNode.root.readOnly) {
return;
}
const { parent, parentKey } = rNode;
const isRoot = parent === null;
let prevValue: unknown;
let newValue: unknown;
if (isRoot) {
prevValue = rNode.root.$;
newValue = isFunction<(...prev: any) => any>(value) ? value(prevValue) : value;
rNode.root.$ = newValue;
} else {
const parentVal = getRNodeVal(parent!);
prevValue = parentVal[parentKey!];
newValue = isFunction<(...prev: any) => any>(value) ? value(prevValue) : value;
parentVal[parentKey!] = newValue;
}
if (trigger && !isSame(newValue, prevValue)) {
triggerRContexts(rNode, prevValue, newValue, isArrayModified);
}
}

View File

@ -0,0 +1,21 @@
/*
* 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 { createReactiveObj } from './RNode';
import { ReactiveProxy } from './types';
export function reactive<T extends any>(obj: T): ReactiveProxy<T> {
return createReactiveObj(obj);
}

View File

@ -0,0 +1,89 @@
/*
* 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 {Atom} from './Atom';
import {getRNodeVal, nodeSymbol} from './RNode';
import {AtomNode, AtomNodeFn, ProxyRNode, ProxyRNodeFn, Reactive, ReactiveProxy, RNode} from './types';
import {GET_R_NODE} from './proxy/RProxyHandler';
export function isAtom(val: unknown): val is Atom {
return val instanceof Atom;
}
export function isRNode(val: unknown): val is RNode {
return typeof val === 'object' && val != null && val['type'] === nodeSymbol;
}
export function isReactiveProxy(val: unknown): val is RNode {
return typeof val === 'object' && val != null && val[GET_R_NODE] !== undefined;
}
export function isReactiveObj(val: unknown): val is ProxyRNodeFn<any> | AtomNodeFn<any> {
return isAtom(val) || isReactiveProxy(val);
}
export function isObject(obj: unknown): boolean {
const type = typeof obj;
return obj != null && (type === 'object' || type === 'function');
}
export function isPrimitive(obj: unknown): boolean {
const type = typeof obj;
return obj != null && type !== 'object' && type !== 'function';
}
export function isFunction<T extends (...prev: any) => any>(obj: unknown): obj is T {
return typeof obj === 'function';
}
/**
* reactive就调用get()
* @param val /reactive对象/
* @return
*/
export function calculateReactive(val: any | (() => any)): any {
let ret = val;
if (typeof val === 'function') {
ret = val();
}
if (isReactiveObj(ret)) {
ret = ret.get();
}
return ret;
}
export function getRNode<T = any>(rObj: ProxyRNode<T> | AtomNode<T>): RNode {
return isReactiveProxy(rObj) ? rObj[GET_R_NODE] : rObj;
}
export function getRNodeFromProxy<T = any>(rObj: ProxyRNode<T>): RNode {
return rObj[GET_R_NODE];
}
export function isPromise<T>(obj: unknown): obj is Promise<T> {
return obj instanceof Promise;
}
export function getValue(value: any): any {
if (isAtom(value)) {
return getRNodeVal(value as Reactive);
}
if (isReactiveProxy(value)) {
return getRNodeVal(value[GET_R_NODE]);
}
return value;
}

View File

@ -0,0 +1,22 @@
/*
* 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.
*/
export const inJSX: { current: boolean } = {
current: false,
};
export const inComputation: { current: boolean } = {
current: false,
};

View File

@ -0,0 +1,42 @@
/*
* 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 { RContext } from './RContext';
import { calculateReactive } from './Utils';
import { Reactive } from './types';
export function watch(fn: () => any | Reactive, callback?: () => void): any {
// 有两个参数,第一个是要监听数据/函数,第二个是回调
if (typeof callback === 'function') {
const effect = new RContext(() => {
callback();
});
const endEffect = effect.start();
calculateReactive(fn);
endEffect();
} else {
// 只有一个参数
const effect = new RContext(fn);
const endEffect = effect.start();
fn();
endEffect();
}
}

View File

@ -0,0 +1,35 @@
/*
* 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 { JSXElement } from '../../renderer/Types';
import { calculateReactive } from '../Utils';
/**
* Block可以控制更新范围View的children函数中通过get()使children函数会被重新执行
* <Block>
{() => {
const count = _rObj.count.get();
return <>
<div>{count}</div>
</>;
}}
</Block>
* @param children
*/
export function Block({ children }: { children: () => JSXElement }): any {
const result = calculateReactive(children);
return result;
}

View File

@ -0,0 +1,91 @@
/*
* 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 { JSXElement } from '../../renderer/Types';
import { createElement } from '../../external/JSXElement';
import { useMemo } from '../../renderer/hooks/HookExternal';
import { memo } from '../../renderer/components/Memo';
import { shallowCompare } from '../../renderer/utils/compare';
import { ArrayState } from '../types';
import { GET_R_NODE } from '../proxy/RProxyHandler';
/**
* For组件用于循环渲染数据
* @param each
* @param children
* @returns JSX元素或null
*/
export function For<T>({
each,
children,
}: {
each: any;
children?: (value: any, index: number) => JSXElement;
}): JSXElement[] | null {
// 获取可观察对象中的数据数组
const reactiveArr = each.get();
if (!reactiveArr || !reactiveArr.length) {
return null;
}
const rNode = each[GET_R_NODE];
const states = rNode.states !== undefined ? rNode.states : [];
let Item: Partial<JSXElement> | null = null;
if (children) {
Item = useMemo(() => {
return memo(({ item, index }) => children(item, index), itemCompare);
}, []);
}
const ret: JSXElement[] = [];
const len = reactiveArr.length;
for (let i = 0; i < len; i++) {
const state = states[i];
const isFresh = state === ArrayState.Fresh;
// 创建并添加JSX元素
ret.push(createElement(Item, { isFresh, item: each[i], index: i }));
}
// 用完,重置
rNode.states = [];
// 合并多次的diffOperator
if (rNode.diffOperators) {
rNode.diffOperators.forEach(() => {
});
}
ret.diffOperator = rNode.diffOperator;
// 用完,删除
delete rNode.diffOperators;
delete rNode.diffOperator;
// 返回JSX元素数组
return ret;
}
// 如果属性中有isFresh就优先判断isFresh
function itemCompare(oldProps, newProps) {
if (newProps.isFresh !== undefined) {
return Boolean(newProps.isFresh);
} else {
return shallowCompare(oldProps, newProps);
}
}

View File

@ -0,0 +1,25 @@
/*
* 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 { memo } from '../../renderer/components/Memo';
import { JSXElement } from '../../renderer/Types';
import { calculateReactive } from '../Utils';
export const RText = memo(
function ({ children }: { children: any }): JSXElement {
return calculateReactive(children);
},
() => true // 属性指针始终为true不刷新
);

View File

@ -0,0 +1,38 @@
/*
* 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 { JSXElement } from '../../renderer/Types';
import { calculateReactive } from '../Utils';
export function Show<T>({
if: rIf,
else: rElse,
children,
}: {
if: any | (() => T);
else?: any;
children: JSXElement | (() => JSXElement);
}): any {
const ifValue: any = calculateReactive(rIf);
let child: JSXElement | (() => JSXElement) | null = null;
if (ifValue) {
child = typeof children === 'function' ? children() : children;
} else {
child = typeof rElse === 'function' ? rElse() : rElse;
}
return child;
}

View File

@ -0,0 +1,42 @@
/*
* 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 { calculateReactive } from '../Utils';
import { JSXElement, VNode } from '../../renderer/Types';
import { Children } from '../../external/ChildrenUtil';
export function Switch<T>({
children,
default: df,
}: {
children: JSXElement[] | Record<any, () => JSXElement>;
default?: JSXElement;
}): JSXElement | null {
const arr = Children.toArray(children);
let index = -1;
for (let i = 0; i < arr.length; i++) {
const showComp = arr[i];
const ifValue: any = calculateReactive((showComp as VNode).props.if);
if (ifValue) {
index = i;
break;
}
}
return index >= 0 ? arr[index] : df ?? null;
}

View File

@ -0,0 +1,146 @@
/*
* 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 { isArray } from '../../inulax/CommonUtils';
import { getOrCreateChildProxy, getRNodeVal, setRNodeVal, trackReactiveData } from '../RNode';
import { RNode, ProxyRNode } from '../types';
const GET = 'get';
const SET = 'set';
const READ = 'read';
const DELETE = 'delete';
const ONCHANGE = 'onChange';
export const GET_R_NODE = '$$getRNode';
const PROTOTYPE = 'prototype';
// 数组的修改方法
const MODIFY_ARR_FNS = new Set<string | symbol>([
'push',
'pop',
'splice',
'shift',
'unshift',
'reverse',
'sort',
'fill',
'from',
'copyWithin',
]);
// 数组的遍历方法
const LOOP_ARR_FNS = new Set<string | symbol>(['forEach', 'map', 'every', 'some', 'filter', 'join']);
export function createProxy<T extends any>(proxyNode: RNode): ProxyRNode<T> {
return new Proxy(proxyNode, {
get,
set,
});
}
const GetFns: Record<string | symbol, (args: RNode) => any> = {
[GET]: getFn,
[READ]: readFn,
[DELETE]: deleteFn,
[ONCHANGE]: onChangeFn,
};
function get(rNode: RNode, key: string | symbol): any {
// 处理get,read,delete,onchange方法
const fn = GetFns[key];
if (fn && typeof fn === 'function') {
return () => fn(rNode);
}
// 调用set()方法
if (key === SET) {
return function (val: any) {
setRNodeVal(rNode, val, true, false);
};
}
if (key === GET_R_NODE) {
return rNode;
}
const rawObj = getRNodeVal(rNode);
// const value = rawObj !== undefined ? Reflect.get(rawObj, key) : rawObj;
const value = rawObj !== undefined ? rawObj[key] : rawObj;
// 对于prototype不做代理
if (key === PROTOTYPE) {
return value;
}
if (isArray(rawObj) && key === 'length') {
// 标记依赖
trackReactiveData(rNode);
return value;
}
// 处理数组的方法
if (typeof value === 'function') {
if (isArray(rawObj)) {
// 处理数组的修改方法
if (MODIFY_ARR_FNS.has(key)) {
return (...args: any[]) => {
// 调用数组方法的时候,前后是相同的引用,所以需要先浅拷贝数组,并在浅拷贝的数组上进行操作
const value = rawObj.slice();
const ret = value[key](...args);
// 调用了数组的修改方法,默认值有变化
setRNodeVal(rNode, value, true, true);
return ret;
};
} else if (LOOP_ARR_FNS.has(key)) { // 处理数组的遍历方法
// 标记被使用了
trackReactiveData(rNode);
return function (callBackFn: any, thisArg?: any) {
function cb(_: any, index: number, array: any[]) {
const idx = String(index);
const itemProxy = getOrCreateChildProxy(array[idx], rNode, idx);
return callBackFn(itemProxy, index, array);
}
return rawObj[key](cb, thisArg);
};
}
}
return value.bind(rawObj);
}
return getOrCreateChildProxy(value, rNode, key);
}
function set(proxyNode: any, key: string, value: any, receiver: any): boolean {
return true;
}
// get()调用的处理
function getFn(node: RNode) {
trackReactiveData(node);
return readFn(node);
}
function readFn(node: RNode) {
return getRNodeVal(node);
}
// delete()调用的处理
function deleteFn() {}
// onChange()调用的处理
function onChangeFn() {}

View File

@ -0,0 +1,116 @@
/*
* 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 { VNode } from '../renderer/Types';
import { Atom } from './Atom';
import { RContextSet } from './RContext';
import { DiffOperator } from './DiffUtils';
export enum ArrayState {
Fresh = 0,
NotFresh = 1,
}
export interface Root<T> {
$?: T;
/**
* computed使用
* @param {readOnly} computed
*/
readOnly?: boolean;
}
export type PrimitiveType = string | number | boolean;
export type ValueType<T> = { [K in keyof T]: any } | Record<string, any> | PrimitiveType;
export interface BaseNodeFns<T> {
/**
*
*/
get(): T;
/**
*
*/
read(): T;
}
export interface ProxyRNodeFn<T> extends BaseNodeFns<T> {
set<V = ValueType<T>>(value: V | ((prev: T) => V));
}
export interface AtomNodeFn<T> extends BaseNodeFns<T> {
set<V extends PrimitiveType>(value: V | ((prev: T) => V));
}
type PropsRecursive<T, K extends keyof T, RecurseType> = T[K] extends PrimitiveType
? AtomNode<T[K]>
: T[K] extends any[]
? any[] & ProxyRNodeFn<T[K]>
: T extends Record<string, any>
? RecurseType
: T[K];
export type ProxyRNodeProps<T> = {
[K in keyof T]: PropsRecursive<T, K, ProxyRNode<T[K]>>;
};
export type ComputedProps<T> = {
[K in keyof T]: PropsRecursive<T, K, BaseNodeFns<T[K]>>;
};
export type ProxyRNode<T> = ProxyRNodeFn<T> & ProxyRNodeProps<T>;
export type AtomNode<T> = AtomNodeFn<T>;
export type Computed<T> = BaseNodeFns<T> & ComputedProps<T>;
export type ReactiveProxy<T> = T extends PrimitiveType ? AtomNode<T> : ProxyRNode<T>;
export interface BaseRNode<T> {
// 标识Node类型 atomSymbol,nodeSymbol,computedSymbol
type: symbol;
root: Root<T>;
children?: Map<string | symbol, RNode<T> | Atom<T>>;
usedRContexts?: RContextSet;
proxy?: any;
diffOperator?: DiffOperator;
diffOperators?: DiffOperator[];
states?: ArrayState[];
}
export interface RootRNode<T> extends BaseRNode<T> {
parentKey: null;
parent: null;
}
export interface ChildrenRNode<T> extends BaseRNode<T> {
parentKey: string | symbol;
parent: RNode;
}
export type RNode<T = any> = RootRNode<T> | ChildrenRNode<T>;
export type Reactive<T = any> = RNode<T> | Atom<T>;
export interface RContextParam {
vNode?: VNode;
reactive?: Reactive;
}
export type RContextCallback = (params: RContextParam, reactive: Reactive) => void;

View File

@ -18,7 +18,7 @@ import type { VNode } from './Types';
import { callRenderQueueImmediate, pushRenderCallback } from './taskExecutor/RenderQueue';
import { updateVNode } from './vnode/VNodeCreator';
import { ContextProvider, DomComponent, DomPortal, TreeRoot } from './vnode/VNodeTags';
import { FlagUtils, InitFlag, Interrupted } from './vnode/VNodeFlags';
import { FlagUtils, InitFlag, Interrupted, Deletion } from './vnode/VNodeFlags';
import { captureVNode } from './render/BaseComponent';
import { checkLoopingUpdateLimit, submitToRender } from './submit/Submit';
import { runAsyncEffects } from './submit/HookEffectHandler';
@ -81,13 +81,13 @@ function collectDirtyNodes(vNode: VNode, parent: VNode): void {
if (parent.dirtyNodes === null) {
parent.dirtyNodes = dirtyNodes;
} else {
parent.dirtyNodes.push(...vNode.dirtyNodes!);
parent.dirtyNodes.push(...dirtyNodes);
dirtyNodes.length = 0;
}
vNode.dirtyNodes = null;
}
if (FlagUtils.hasAnyFlag(vNode)) {
if (FlagUtils.hasAnyFlag(vNode) && vNode.flags !== Deletion) {
if (parent.dirtyNodes === null) {
parent.dirtyNodes = [vNode];
} else {
@ -273,44 +273,44 @@ function buildVNodeTree(treeRoot: VNode) {
changeMode(InRender, true);
// 计算出开始节点
const startVNode = calcStartUpdateVNode(treeRoot);
// const startVNode = calcStartUpdateVNode(treeRoot);
// 缓存起来
setStartVNode(startVNode);
setStartVNode(treeRoot);
// 清空toUpdateNodes
treeRoot.toUpdateNodes?.clear();
if (startVNode.tag !== TreeRoot) {
// 不是根节点
// 设置namespace用于createElement
let parent = startVNode.parent;
while (parent !== null) {
const tag = parent.tag;
if (tag === DomComponent) {
break;
} else if (tag === TreeRoot || tag === DomPortal) {
break;
}
parent = parent.parent;
}
// 当在componentWillUnmount中调用setStateparent可能是null因为startVNode会被clear
if (parent !== null) {
resetNamespaceCtx(parent);
setNamespaceCtx(parent, parent.realNode);
}
// 恢复父节点的context
recoverTreeContext(startVNode);
}
// if (startVNode.tag !== TreeRoot) {
// // 不是根节点
// // 设置namespace用于createElement
// let parent = startVNode.parent;
// while (parent !== null) {
// const tag = parent.tag;
// if (tag === DomComponent) {
// break;
// } else if (tag === TreeRoot || tag === DomPortal) {
// break;
// }
// parent = parent.parent;
// }
//
// // 当在componentWillUnmount中调用setStateparent可能是null因为startVNode会被clear
// if (parent !== null) {
// resetNamespaceCtx(parent);
// setNamespaceCtx(parent, parent.realNode);
// }
//
// // 恢复父节点的context
// recoverTreeContext(startVNode);
// }
// 重置环境变量,为重新进行深度遍历做准备
resetProcessingVariables(startVNode);
resetProcessingVariables(treeRoot);
// devProps 用于插件手动更新props值
if (startVNode.devProps !== undefined) {
startVNode.props = startVNode.devProps;
startVNode.devProps = undefined;
}
// if (startVNode.devProps !== undefined) {
// startVNode.props = startVNode.devProps;
// startVNode.devProps = undefined;
// }
while (processing !== null) {
try {
@ -327,11 +327,11 @@ function buildVNodeTree(treeRoot: VNode) {
handleError(treeRoot, thrownValue);
}
}
if (startVNode.tag !== TreeRoot) {
// 不是根节点
// 恢复父节点的context
resetTreeContext(startVNode);
}
// if (startVNode.tag !== TreeRoot) {
// // 不是根节点
// // 恢复父节点的context
// resetTreeContext(startVNode);
// }
setProcessingClassVNode(null);

View File

@ -14,6 +14,7 @@
*/
import { BELONG_CLASS_VNODE_KEY } from './vnode/VNode';
import { VNodeTag } from './vnode/VNodeTags';
export { VNode } from './vnode/VNode';
@ -82,3 +83,75 @@ export type Source = {
};
export type Callback = () => void;
export type VNode = {
tag: VNodeTag,
key: string | null, // 唯一标识符
props: any, // 传给组件的props的值类组件包含defaultPropsLazy组件不包含
type: any,
realNode: any, // 如果是类则存放实例如果是div这种则存放真实DOM
// 关系结构
parent: VNode | null, // 父节点
child: VNode | null, // 子节点
next: VNode | null, // 兄弟节点
cIndex, // 节点在children数组中的位置
eIndex, // HorizonElement在jsx中的位置例如jsx中的null不会生成vNode所以eIndex和cIndex不一致
ref: RefType | ((handle: any) => void) | null, // 包裹一个函数submit阶段使用比如将外部useRef生成的对象赋值到ref上
oldProps: any,
// 是否已经被从树上移除
isCleared,
changeList: any, // DOM的变更列表
effectList: any[] | null, // useEffect 的更新数组
updates: any[] | null, // TreeRoot和ClassComponent使用的更新数组
stateCallbacks: any[] | null, // 存放存在setState的第二个参数和HorizonDOM.render的第三个参数所在的node数组
isForceUpdate: boolean, // 是否使用强制更新
isSuspended, // 是否被suspense打断更新
state: any, // ClassComponent和TreeRoot的状态
hooks: Array<Hook<any, any>> | null, // 保存hook
depContexts: Array<ContextType<any>> | null, // FunctionComponent和ClassComponent对context的依赖列表
isDepContextChange: boolean, // context是否变更
dirtyNodes: Array<VNode> | null, // 需要改动的节点数组
shouldUpdate,
childShouldUpdate,
task: any,
// 使用这个变量来记录修改前的值,用于恢复。
context: any,
// 因为LazyComponent会修改tag和type属性为了能识别增加一个属性
isLazyComponent: boolean,
// 因为LazyComponent会修改type属性为了在diff中判断是否可以复用需要增加一个lazyType
lazyType: any,
flags,
clearChild: VNode | null,
// one tree相关属性
isCreated,
oldHooks: Array<Hook<any, any>> | null, // 保存上一次执行的hook
oldState: any,
oldRef: RefType | ((handle: any) => void) | null,
oldChild: VNode | null,
promiseResolve: boolean, // suspense的promise是否resolve
devProps: any, // 用于dev插件临时保存更新props值
suspenseState: SuspenseState | null,
path: string, // 保存从根到本节点的路径
// 根节点数据
toUpdateNodes: Set<VNode> | null, // 保存要更新的节点
delegatedEvents: Set<string>,
belongClassVNode: VNode | null, // 记录JSXElement所属class vNode处理ref的时候使用
// 状态管理器HorizonX使用
isStoreChange: boolean,
observers: Set<any> | null, // 记录这个函数组件/类组件依赖哪些Observer
classComponentWillUnmount: Function | null, // HorizonX会在classComponentWillUnmount中清除对VNode的引入用
src: Source | null, // 节点所在代码位置
// reactive
attrRContexts: Set<any> | null,
compRContext: any | null,
}

View File

@ -23,11 +23,14 @@ import {
createFragmentVNode,
createPortalVNode,
createDomTextVNode,
createReactiveVNode,
} from '../vnode/VNodeCreator';
import { isSameType, getIteratorFn, isTextType, isIteratorType, isObjectType } from './DiffTools';
import { travelChildren } from '../vnode/VNodeUtils';
import { markVNodePath } from '../utils/vNodePath';
import { BELONG_CLASS_VNODE_KEY } from '../vnode/VNode';
import { Operation } from '../../reactive/DiffUtils';
import { isReactiveProxy, isReactiveObj, getRNodeFromProxy } from '../../reactive/Utils';
enum DiffCategory {
TEXT_NODE = 'TEXT_NODE',
@ -101,6 +104,9 @@ function getNodeType(newChild: any): string | null {
return DiffCategory.TEXT_NODE;
}
if (isObjectType(newChild)) {
if (isReactiveObj(newChild)) {
return DiffCategory.REACTIVE_NODE;
}
if (Array.isArray(newChild) || isIteratorType(newChild)) {
return DiffCategory.ARR_NODE;
}
@ -152,6 +158,14 @@ function getNewNode(parentNode: VNode, newChild: any, oldNode: VNode | null) {
}
break;
}
case DiffCategory.REACTIVE_NODE: {
if (oldNode === null || oldNode.tag !== ReactiveComponent) {
resultNode = createReactiveVNode(newChild);
} else {
resultNode = updateVNode(oldNode, newChild);
}
break;
}
case DiffCategory.OBJECT_NODE: {
if (newChild.vtype === TYPE_COMMON_ELEMENT) {
if (newChild.type === TYPE_FRAGMENT) {
@ -564,7 +578,7 @@ function diffObjectNodeHandler(
}
let resultNode: VNode | null = null;
let startDelVNode: VNode | null = firstChildVNode;
let startDelVNode: VNode | null = node;
if (newChild.vtype === TYPE_COMMON_ELEMENT) {
if (canReuseNode) {
// 可以复用
@ -624,13 +638,91 @@ function diffObjectNodeHandler(
return null;
}
// 响应式For组件专用
function diffReactiveForNodeHandler(
parentNode: VNode,
firstChild: VNode | null,
newChildren: Array<any>,
): VNode | null {
let oldNode = firstChild;
let nextOldNode: VNode | null = null;
let resultingFirstChild: VNode | null = null;
let prevNewNode: VNode | null = null;
function appendNode(newNode: VNode) {
if (prevNewNode === null) {
resultingFirstChild = newNode;
newNode.cIndex = 0;
} else {
prevNewNode.next = newNode;
newNode.cIndex = prevNewNode.cIndex + 1;
}
markVNodePath(newNode);
prevNewNode = newNode;
}
// 特殊处理
// 如果新节点为空
if (newChildren.length === 0) {
if (firstChild) {
FlagUtils.markClear(parentNode);
parentNode.clearChild = firstChild;
return null;
}
}
let childIndex = 0;
const { opts } = newChildren.diffOperator;
for (let i = 0; i < opts.length; i++) {
const opt = opts[i];
if (oldNode !== null) {
// 先保存next因为getNewNode会修改next
nextOldNode = oldNode.next;
}
switch (opt.action) {
case Operation.Nop: {
const newNode = getNewNode(parentNode, newChildren[childIndex], oldNode);
newNode.eIndex = childIndex;
appendNode(newNode);
// 使用了加1
childIndex++;
// 使用了oldNode更新
oldNode = nextOldNode;
break;
}
case Operation.Insert: {
const newNode = getNewNode(parentNode, newChildren[childIndex], null);
FlagUtils.setAddition(newNode);
newNode.eIndex = childIndex;
appendNode(newNode);
// 使用了加1
childIndex++;
break;
}
case Operation.Delete: {
deleteVNode(parentNode, oldNode);
// 使用了oldNode更新
oldNode = nextOldNode;
break;
}
}
}
return resultingFirstChild;
}
// Diff算法的对外接口
export function createChildrenByDiff(
parentNode: VNode,
firstChild: VNode | null,
newChild: any,
child: any,
isComparing: boolean
): VNode | null {
let newChild = isReactiveProxy(child) ? getRNodeFromProxy(child) : child;
const isFragment = isNoKeyFragment(newChild);
newChild = isFragment ? newChild.props.children : newChild;
@ -649,7 +741,11 @@ export function createChildrenByDiff(
// 3. newChild是数组类型
if (Array.isArray(newChild)) {
return diffArrayNodesHandler(parentNode, firstChild, newChild);
if (newChild.diffOperator) {
return diffReactiveForNodeHandler(parentNode, firstChild, newChild);
} else {
return diffArrayNodesHandler(parentNode, firstChild, newChild);
}
}
// 4. newChild是迭代器类型

View File

@ -24,6 +24,11 @@ import { useReducerImpl } from './UseReducerHook';
import { useStateImpl } from './UseStateHook';
import { getNewContext } from '../components/context/Context';
import { getProcessingVNode } from '../GlobalVar';
import { useAtomImpl } from './reactive/UseAtom';
import { useComputedImpl } from './reactive/UseCompute';
import { useReactiveImpl } from './reactive/UseReactive';
import { AtomNode, PrimitiveType, Reactive } from '../../reactive/types';
import { useWatchImpl } from './reactive/UseWatch';
import type { MutableRef, RefCallBack, RefObject } from './HookType';
import type {
@ -115,3 +120,23 @@ export function useImperativeHandle<T, R extends T>(
// 兼容react-redux
export const useDebugValue = () => {};
// reactive hooks
export function useAtom<T extends PrimitiveType>(initialValue: T): [AtomNode<T>, (value: T) => void] {
return useAtomImpl(initialValue);
}
export function useCompute<T>(compute: () => T) {
return useComputedImpl(compute);
}
export function useComputed<T>(compute: () => T) {
return useComputedImpl(compute);
}
export function useReactive<T>(obj: T) {
return useReactiveImpl<T>(obj);
}
export function useWatch<T>(fn: () => any | Reactive, callback?: () => void) {
return useWatchImpl<T>(fn, callback);
}

View File

@ -0,0 +1,20 @@
/*
* 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 { watch } from '../../horizonx/proxy/watch';
export function useWatchImpl(fn: () => void) {
watch(fn);
}

View File

@ -0,0 +1,53 @@
/*
* 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 { getHookStage, HookStage } from '../HookStage';
import { createHook, getCurrentHook, throwNotInFuncError } from '../BaseHook';
import { disposeReactive } from '../../../reactive/RContext';
import { useCallbackImpl } from '../UseCallbackHook';
import { useEffectImpl } from '../UseEffectHook';
import { Atom } from '../../../reactive/Atom';
import { AtomNode, PrimitiveType } from '../../../reactive/types';
export function useAtomImpl<T extends PrimitiveType>(initialValue: T): [AtomNode<T>, (value: T) => void] {
const stage = getHookStage();
let atom: Atom<T>;
switch (stage) {
case HookStage.Init:
atom = new Atom(initialValue);
createHook(atom);
break;
case HookStage.Update:
atom = getCurrentHook().state as unknown as Atom;
break;
default:
throwNotInFuncError();
}
const setAtom = useCallbackImpl((value: T) => {
atom.set(value);
}, []);
// 组件销毁时清除effect
useEffectImpl(
() => () => {
disposeReactive(atom);
},
[]
);
return [atom, setAtom];
}

View File

@ -0,0 +1,24 @@
/*
* 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 { useMemoImpl } from '../UseMemoHook';
import { Computed } from '../../../reactive/types';
import { computed } from '../../../reactive/Computed';
export function useComputedImpl<T>(fn: () => T): Computed<T> {
return useMemoImpl(() => {
return computed(fn);
}, []);
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { reactive } from '../../../reactive/Reactive';
import { getHookStage, HookStage } from '../HookStage';
import { createHook, getCurrentHook, throwNotInFuncError } from '../BaseHook';
import { useEffectImpl } from '../UseEffectHook';
import { disposeReactive } from '../../../reactive/RContext';
import { Reactive, ReactiveProxy } from '../../../reactive/types';
export function useReactiveImpl<T>(obj) {
const stage = getHookStage();
let reactiveObj: any;
switch (stage) {
case HookStage.Init:
reactiveObj = reactive<T>(obj);
createHook(reactiveObj);
break;
case HookStage.Update:
reactiveObj = getCurrentHook().state as unknown as Reactive<T>;
break;
default:
throwNotInFuncError();
}
// 组件销毁时清除effect
useEffectImpl(
() => () => {
disposeReactive(reactiveObj);
},
[]
);
return reactiveObj as ReactiveProxy<T>;
}

View File

@ -0,0 +1,28 @@
/*
* 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 { useEffectImpl } from '../UseEffectHook';
import { Reactive } from '../../../reactive/types';
import { watch } from '../../../reactive/Watch';
import { useMemoImpl } from '../UseMemoHook';
export function useWatchImpl<T>(fn: () => any | Reactive, callback?: () => void): void {
useMemoImpl(() => {
watch(fn, callback);
}, []);
// 组件销毁时
useEffectImpl(() => () => {}, []);
}

View File

@ -34,6 +34,7 @@ import { processUpdates } from '../UpdateHandler';
import { setProcessingClassVNode } from '../GlobalVar';
import { onlyUpdateChildVNodes } from '../vnode/VNodeCreator';
import { createChildrenByDiff } from '../diff/nodeDiffComparator';
import { createComponentDependent } from '../../reactive/RContextCreator';
const emptyContextObj = {};
// 获取当前节点的context
@ -174,7 +175,7 @@ export function captureRender(processing: VNode): VNode | null {
// 不复用
if (shouldUpdate) {
return createChildren(ctor, processing);
return createComponentDependent(() => createChildren(ctor, processing), processing);
} else {
return onlyUpdateChildVNodes(processing);
}

View File

@ -21,7 +21,7 @@ import { appendChildElement, newDom, initDomProps, getPropChangeList, isTextChil
import { FlagUtils } from '../vnode/VNodeFlags';
import { markRef } from './BaseComponent';
import { DomComponent, DomPortal, DomText } from '../vnode/VNodeTags';
import { travelVNodeTree } from '../vnode/VNodeUtils';
import { isDomVNode, travelVNodeTree } from '../vnode/VNodeUtils';
import { createChildrenByDiff } from '../diff/nodeDiffComparator';
function updateDom(processing: VNode, type: any, newProps: Props) {
@ -74,7 +74,7 @@ export function bubbleRender(processing: VNode) {
travelVNodeTree(
vNode,
node => {
if (node.tag === DomComponent || node.tag === DomText) {
if (isDomVNode(node)) {
appendChildElement(dom, node.realNode);
}
},

View File

@ -22,6 +22,7 @@ import { ForwardRef } from '../vnode/VNodeTags';
import { FlagUtils, Update } from '../vnode/VNodeFlags';
import { onlyUpdateChildVNodes } from '../vnode/VNodeCreator';
import { createChildrenByDiff } from '../diff/nodeDiffComparator';
import { createComponentDependent } from '../../reactive/RContextCreator';
// 在useState, useReducer的时候会触发state变化
let stateChange = false;
@ -56,10 +57,14 @@ export function captureFunctionComponent(processing: VNode, funcComp: any, nextP
// 在执行exeFunctionHook前先设置stateChange为false
setStateChange(false);
const newElements = runFunctionWithHooks(
processing.tag === ForwardRef ? funcComp.render : funcComp,
nextProps,
processing.tag === ForwardRef ? processing.ref : undefined,
const newElements = createComponentDependent(
() =>
runFunctionWithHooks(
processing.tag === ForwardRef ? funcComp.render : funcComp,
nextProps,
processing.tag === ForwardRef ? processing.ref : undefined,
processing
),
processing
);

View File

@ -0,0 +1,46 @@
/*
* 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 type { VNode } from '../Types';
import { throwIfTrue } from '../utils/throwIfTrue';
import { newTextDom } from '../../dom/DOMOperator';
import { subscribeReactiveComponent } from '../../reactive/RContextCreator';
import { getRNode, getValue, isPrimitive } from '../../reactive/Utils';
export function captureRender(): VNode | null {
return null;
}
export function bubbleRender(processing: VNode) {
const text = processing.props;
const newText = getValue(text);
if (!processing.isCreated && processing.realNode != null) {
// 更新不需要处理
} else {
// 初始化
if (!isPrimitive(newText)) {
// 如果存在bug可能出现这种情况
throwIfTrue(processing.realNode === null, 'The reactive obj value must be a primitive.');
}
// 获得对应节点
processing.realNode = newTextDom(newText, processing);
// 监听Reactive
subscribeReactiveComponent(processing.realNode, getRNode(text));
}
}

View File

@ -27,6 +27,7 @@ import * as DomTextRender from './DomText';
import * as LazyComponentRender from './LazyComponent';
import * as MemoComponentRender from './MemoComponent';
import * as SuspenseComponentRender from './SuspenseComponent';
import * as ReactiveComponentRender from './ReactiveComponent';
import {
ClassComponent,
@ -42,6 +43,7 @@ import {
LazyComponent,
MemoComponent,
SuspenseComponent,
ReactiveComponent,
} from '../vnode/VNodeTags';
export { BaseComponentRender };
@ -60,4 +62,5 @@ export default {
[LazyComponent]: LazyComponentRender,
[MemoComponent]: MemoComponentRender,
[SuspenseComponent]: SuspenseComponentRender,
[ReactiveComponent]: ReactiveComponentRender,
};

View File

@ -50,9 +50,10 @@ import {
callUseLayoutEffectRemove,
} from './HookEffectHandler';
import { handleSubmitError } from '../ErrorHandler';
import { travelVNodeTree, clearVNode, isDomVNode, getSiblingDom } from '../vnode/VNodeUtils';
import { travelVNodeTree, clearVNode, isDomVNode, getSiblingDom, findDOMContainer, isDomContainer } from '../vnode/VNodeUtils';
import { shouldAutoFocus } from '../../dom/utils/Common';
import { BELONG_CLASS_VNODE_KEY } from '../vnode/VNode';
import { cleanupRContext } from '../../reactive/RContext';
function callComponentWillUnmount(vNode: VNode, instance: any) {
try {
@ -205,48 +206,119 @@ function unmountDomComponents(vNode: VNode): void {
// 这两个变量要一起更新
let currentParent;
travelVNodeTree(
vNode,
node => {
if (!currentParentIsValid) {
let parent = node.parent;
let tag;
while (parent !== null) {
tag = parent.tag;
if (tag === DomComponent || tag === TreeRoot || tag === DomPortal) {
currentParent = parent.realNode;
break;
let node = vNode;
while (true) {
// handle
if (!currentParentIsValid) {
let parent = node.parent;
let tag;
while (parent !== null) {
tag = parent.tag;
if (tag === DomComponent || tag === TreeRoot || tag === DomPortal) {
currentParent = parent.realNode;
break;
}
parent = parent.parent;
}
currentParentIsValid = true;
}
if (node.tag === DomComponent || node.tag === DomText) {
let nd = node;
// 卸载vNode递归遍历子vNode
outer: while (true) {
unmountVNode(nd);
// 找子节点
const childVNode = nd.child;
if (childVNode !== null && nd.tag !== DomPortal) {
childVNode.parent = nd;
nd = childVNode;
continue;
}
// 回到开始节点
if (nd === node) {
break;
}
// 找兄弟,没有就往上再找兄弟
while (nd.next === null) {
if (nd.parent === null || nd.parent === node) {
break outer;
}
parent = parent.parent;
nd = nd.parent;
}
currentParentIsValid = true;
// 找到兄弟
const siblingVNode = nd.next;
siblingVNode.parent = nd.parent;
nd = siblingVNode;
}
if (node.tag === DomComponent || node.tag === DomText) {
// 卸载vNode递归遍历子vNode
unmountNestedVNodes(node);
// 在所有子项都卸载后删除dom树中的节点
removeChildDom(currentParent, node.realNode);
} else if (node.tag === DomPortal) {
if (node.child !== null) {
currentParent = node.realNode;
}
} else {
unmountVNode(node);
// 在所有子项都卸载后删除dom树中的节点
removeChildDom(currentParent, node.realNode);
} else if (node.tag === DomPortal) {
if (node.child !== null) {
currentParent = node.realNode;
}
},
node =>
// 如果是dom不用再遍历child
node.tag === DomComponent || node.tag === DomText,
vNode,
node => {
} else {
unmountVNode(node);
}
// 找子节点
const childVNode = node.child;
if (childVNode !== null && node.tag !== DomComponent && node.tag !== DomText) {
childVNode.parent = node;
node = childVNode;
continue;
}
// 回到开始节点
if (node === vNode) {
return null;
}
// 找兄弟,没有就往上再找兄弟
while (node.next === null) {
if (node.parent === null || node.parent === vNode) {
return null;
}
node = node.parent;
if (node.tag === DomPortal) {
// 当离开portal需要重新设置parent
currentParentIsValid = false;
}
}
);
// 找到兄弟
const siblingVNode = node.next;
siblingVNode.parent = node.parent;
node = siblingVNode;
}
}
// dom节点上的attr值如果是reactive对象会自动创建attrEffect。所以在清除时需要销毁attrEffect。
function detachAttrRContexts(vNode: VNode) {
const attrRContexts = vNode.attrRContexts;
if (attrRContexts) {
for (const attrRContext of attrRContexts) {
cleanupRContext(attrRContext);
attrRContext.callback = null;
}
attrRContexts.clear();
}
}
// 函数组件和Class组件会自动创rContext所以在清除时需要销毁rContext
function detachCompRContext(vNode: VNode) {
const rContext = vNode.compRContext;
if (rContext) {
cleanupRContext(rContext);
rContext.callback = null;
}
}
// 卸载一个vNode不会递归
@ -256,6 +328,8 @@ function unmountVNode(vNode: VNode): void {
case ForwardRef:
case MemoComponent: {
callEffectRemove(vNode);
detachCompRContext(vNode);
break;
}
case ClassComponent: {
@ -273,10 +347,14 @@ function unmountVNode(vNode: VNode): void {
vNode.classComponentWillUnmount(vNode);
vNode.classComponentWillUnmount = null;
}
detachCompRContext(vNode);
break;
}
case DomComponent: {
detachRef(vNode);
detachAttrRContexts(vNode);
break;
}
case DomPortal: {
@ -390,6 +468,64 @@ function submitClear(vNode: VNode): void {
vNode.clearChild = null;
}
function submitClear2(vNode: VNode): void {
let domVNode;
if (isDomContainer(vNode)) {
domVNode = vNode;
}
// 1、拿到最近的父DOM
domVNode = findDOMContainer(vNode);
const dom = domVNode.realNode;
// 2、复制DOM节点
const cloneDom = dom.cloneNode(false);
// 3、复制cloneNode未能复制的属性
// 真实 dom 获取的keys只包含新增的属性
// 比如真实 dom 拿到的 keys 一般只有两个 horizon 自定义属性
// 但考虑到用户可能自定义其他属性,所以采用遍历赋值的方式
const customizeKeys = Object.keys(dom);
const keyLength = customizeKeys.length;
for (let i = 0; i < keyLength; i++) {
const key = customizeKeys[i];
// 测试代码 mock 实例的全部可遍历属性都会被Object.keys方法读取到
// children 属性被复制意味着复制了子节点,因此要排除
if (key !== 'children') {
cloneDom[key] = dom[key];
}
}
// 4、拿到最近的父DOM的父DOM
const parentDomVNode = findDOMContainer(domVNode);
const parentDom = parentDomVNode.realNode;
// 5、执行unmount
let clearChild = vNode.clearChild as VNode;
// 卸载 clearChild 和 它的兄弟节点
while (clearChild) {
// 卸载子vNode递归遍历子vNode
unmountNestedVNodes(clearChild);
clearVNode(clearChild);
clearChild = clearChild.next as VNode;
}
// 6、在所有子项都卸载后删除dom树中的节点
removeChildDom(parentDom, dom);
// 7、插入cloneDom
const realNodeNext = getSiblingDom(domVNode);
insertDom(parentDom, cloneDom, realNodeNext);
// 8、重置realNode
domVNode.realNode = cloneDom;
attachRef(domVNode);
// 9、清理
FlagUtils.removeFlag(vNode, Clear);
vNode.clearChild = null;
}
function submitDeletion(vNode: VNode): void {
// 遍历所有子节点删除dom节点detach ref 和 调用componentWillUnmount()
unmountDomComponents(vNode);
@ -440,6 +576,7 @@ export {
submitAddition,
submitDeletion,
submitClear,
submitClear2,
submitUpdate,
callAfterSubmitLifeCycles,
attachRef,

View File

@ -28,6 +28,7 @@ import {
submitUpdate,
detachRef,
submitClear,
submitClear2,
} from './LifeCycleHandler';
import { tryRenderFromRoot } from '../TreeBuilder';
import { InRender, copyExecuteMode, setExecuteMode, changeMode } from '../ExecuteMode';
@ -98,6 +99,7 @@ function submit(dirtyNodes: Array<VNode>) {
submitDeletion(node);
}
if (isClear) {
// submitClear2(node);
submitClear(node);
}
}

View File

@ -22,7 +22,7 @@ const PATH_DELIMITER = ',';
* @param vNode
*/
export function markVNodePath(vNode: VNode) {
vNode.path = `${vNode.parent!.path}${PATH_DELIMITER}${vNode.cIndex}`;
// vNode.path = `${vNode.parent!.path}${PATH_DELIMITER}${vNode.cIndex}`;
}
export function getPathArr(vNode: VNode) {

View File

@ -17,194 +17,152 @@
* DOM结构体
*/
import {
TreeRoot,
FunctionComponent,
ClassComponent,
ContextConsumer,
ContextProvider,
DomComponent,
DomPortal,
DomText,
ContextConsumer,
ForwardRef,
SuspenseComponent,
LazyComponent,
DomComponent,
Fragment,
ContextProvider,
Profiler,
FunctionComponent,
LazyComponent,
MemoComponent,
Profiler,
SuspenseComponent,
TreeRoot,
} from './VNodeTags';
import type { VNodeTag } from './VNodeTags';
import type { RefType, ContextType, SuspenseState, Source } from '../Types';
import type { Hook } from '../hooks/HookType';
import { InitFlag } from './VNodeFlags';
import { Observer } from '../../inulax/proxy/Observer';
import { VNode } from '../Types';
export const BELONG_CLASS_VNODE_KEY = typeof Symbol === 'function' ? Symbol('belongClassVNode') : 'belongClassVNode';
export class VNode {
tag: VNodeTag;
key: string | null; // 唯一标识符
props: any; // 传给组件的props的值类组件包含defaultPropsLazy组件不包含
type: any = null;
realNode: any; // 如果是类则存放实例如果是div这种则存放真实DOM
export function VirtualNode(tag: VNodeTag, props: any, key: null | string, realNode) {
this.tag = tag; // 对应组件的类型比如ClassComponent等
// 唯一标识符
// if (isReactiveObj(this.key)) {
// this.key = getReactiveValue(this.key);
// subscribeKeyEffect(this, this.key);
// } else {
this.key = key;
// }
this.props = props; // 传给组件的props的值类组件包含defaultPropsLazy组件不包含
this.type = null;
this.realNode = null; // 如果是类则存放实例如果是div这种则存放真实DOM
// 关系结构
parent: VNode | null = null; // 父节点
child: VNode | null = null; // 子节点
next: VNode | null = null; // 兄弟节点
cIndex = 0; // 节点在children数组中的位置
eIndex = 0; // InulaElement在jsx中的位置例如jsx中的null不会生成vNode所以eIndex和cIndex不一致
this.parent = null; // 父节点
this.child = null; // 子节点
this.next = null; // 兄弟节点
this.cIndex = 0; // 节点在children数组中的位置
this.eIndex = 0; // HorizonElement在jsx中的位置例如jsx中的null不会生成vNode所以eIndex和cIndex不一致
ref: RefType | ((handle: any) => void) | null = null; // 包裹一个函数submit阶段使用比如将外部useRef生成的对象赋值到ref上
oldProps: any = null;
this.ref = null; // 包裹一个函数submit阶段使用比如将外部useRef生成的对象赋值到ref上
this.oldProps = null;
// 是否已经被从树上移除
isCleared = false;
changeList: any; // DOM的变更列表
effectList: any[] | null; // useEffect 的更新数组
updates: any[] | null; // TreeRoot和ClassComponent使用的更新数组
stateCallbacks: any[] | null; // 存放存在setState的第二个参数和InulaDOM.render的第三个参数所在的node数组
isForceUpdate: boolean; // 是否使用强制更新
isSuspended = false; // 是否被suspense打断更新
state: any; // ClassComponent和TreeRoot的状态
hooks: Array<Hook<any, any>> | null; // 保存hook
depContexts: Array<ContextType<any>> | null; // FunctionComponent和ClassComponent对context的依赖列表
isDepContextChange: boolean; // context是否变更
dirtyNodes: Array<VNode> | null = null; // 需要改动的节点数组
shouldUpdate = false;
childShouldUpdate = false;
task: any;
this.dirtyNodes = null; // 需要改动的节点数组
this.shouldUpdate = false;
this.childShouldUpdate = false;
// 使用这个变量来记录修改前的值,用于恢复。
context: any;
// 因为LazyComponent会修改tag和type属性为了能识别增加一个属性
isLazyComponent: boolean;
this.flags = InitFlag;
this.clearChild = null;
this.isCreated = true;
this.oldRef = null;
this.oldChild = null;
// 因为LazyComponent会修改type属性为了在diff中判断是否可以复用需要增加一个lazyType
lazyType: any;
flags = InitFlag;
clearChild: VNode | null;
// one tree相关属性
isCreated = true;
oldHooks: Array<Hook<any, any>> | null; // 保存上一次执行的hook
oldState: any;
oldRef: RefType | ((handle: any) => void) | null = null;
oldChild: VNode | null = null;
promiseResolve: boolean; // suspense的promise是否resolve
devProps: any; // 用于dev插件临时保存更新props值
suspenseState: SuspenseState | null;
path = ''; // 保存从根到本节点的路径
// 根节点数据
toUpdateNodes: Set<VNode> | null; // 保存要更新的节点
delegatedEvents: Set<string>;
// @ts-ignore
[BELONG_CLASS_VNODE_KEY]: VNode | null = null; // 记录JSXElement所属class vNode处理ref的时候使用
// 状态管理器InulaX使用
isStoreChange: boolean;
observers: Set<Observer> | null = null; // 记录这个函数组件/类组件依赖哪些Observer
classComponentWillUnmount: ((vNode: VNode) => any) | null; // InulaX会在classComponentWillUnmount中清除对VNode的引入用
src: Source | null; // 节点所在代码位置
constructor(tag: VNodeTag, props: any, key: null | string, realNode) {
this.tag = tag; // 对应组件的类型比如ClassComponent等
this.key = key;
this.props = props;
switch (tag) {
case TreeRoot:
this.realNode = realNode;
this.task = null;
this.toUpdateNodes = new Set<VNode>();
this.delegatedEvents = new Set<string>();
this.updates = null;
this.stateCallbacks = null;
this.state = null;
this.oldState = null;
this.context = null;
break;
case FunctionComponent:
this.realNode = null;
this.effectList = null;
this.hooks = null;
this.depContexts = null;
this.isDepContextChange = false;
this.oldHooks = null;
this.isStoreChange = false;
this.observers = null;
this.classComponentWillUnmount = null;
this.src = null;
break;
case ClassComponent:
this.realNode = null;
this.updates = null;
this.stateCallbacks = null;
this.isForceUpdate = false;
this.state = null;
this.depContexts = null;
this.isDepContextChange = false;
this.oldState = null;
this.context = null;
this.isStoreChange = false;
this.observers = null;
this.classComponentWillUnmount = null;
this.src = null;
break;
case DomPortal:
this.realNode = null;
this.context = null;
this.delegatedEvents = new Set<string>();
this.src = null;
break;
case DomComponent:
this.realNode = null;
this.changeList = null;
this.context = null;
this.src = null;
break;
case DomText:
this.realNode = null;
break;
case SuspenseComponent:
this.realNode = null;
this.suspenseState = {
promiseSet: null,
didCapture: false,
promiseResolved: false,
oldChildStatus: '',
childStatus: '',
};
this.src = null;
break;
case ContextProvider:
this.src = null;
this.context = null;
break;
case MemoComponent:
this.effectList = null;
this.src = null;
break;
case LazyComponent:
this.realNode = null;
this.stateCallbacks = null;
this.isLazyComponent = true;
this.lazyType = null;
this.updates = null;
this.src = null;
break;
case Fragment:
break;
case ContextConsumer:
break;
case ForwardRef:
break;
case Profiler:
break;
default:
break;
}
switch (tag) {
case TreeRoot:
this.realNode = realNode;
this.task = null;
this.toUpdateNodes = new Set<VNode>();
this.delegatedEvents = new Set<string>();
this.updates = null;
this.stateCallbacks = null;
this.state = null;
this.oldState = null;
this.context = null;
break;
case FunctionComponent:
this.realNode = null;
this.effectList = null;
this.hooks = null;
this.depContexts = null;
this.isDepContextChange = false;
this.oldHooks = null;
this.isStoreChange = false;
this.observers = null;
this.classComponentWillUnmount = null;
this.src = null;
this.compRContext = null;
break;
case ClassComponent:
this.realNode = null;
this.updates = null;
this.stateCallbacks = null;
this.isForceUpdate = false;
this.state = null;
this.depContexts = null;
this.isDepContextChange = false;
this.oldState = null;
this.context = null;
this.isStoreChange = false;
this.observers = null;
this.classComponentWillUnmount = null;
this.src = null;
this.compRContext = null;
break;
case DomPortal:
this.realNode = null;
this.context = null;
this.delegatedEvents = new Set<string>();
this.src = null;
break;
case DomComponent:
this.realNode = null;
this.changeList = null;
this.context = null;
this.src = null;
this.attrRContexts = null;
break;
case DomText:
this.realNode = null;
break;
case SuspenseComponent:
this.realNode = null;
this.suspenseState = {
promiseSet: null,
didCapture: false,
promiseResolved: false,
oldChildStatus: '',
childStatus: '',
};
this.src = null;
break;
case ContextProvider:
this.src = null;
this.context = null;
break;
case MemoComponent:
this.effectList = null;
this.src = null;
break;
case LazyComponent:
this.realNode = null;
this.stateCallbacks = null;
this.isLazyComponent = true;
this.lazyType = null;
this.updates = null;
this.src = null;
break;
case Fragment:
break;
case ContextConsumer:
break;
case ForwardRef:
break;
case Profiler:
break;
}
}

View File

@ -29,6 +29,7 @@ import {
LazyComponent,
MemoComponent,
SuspenseComponent,
ReactiveComponent,
} from './VNodeTags';
import {
TYPE_CONTEXT,
@ -41,8 +42,8 @@ import {
TYPE_STRICT_MODE,
TYPE_SUSPENSE,
} from '../../external/JSXElementType';
import { VNode } from './VNode';
import { JSXElement, Source } from '../Types';
import { VirtualNode } from './VNode';
import { JSXElement, Source, VNode } from '../Types';
import { markVNodePath } from '../utils/vNodePath';
const typeLazyMap = {
@ -57,7 +58,7 @@ const typeMap = {
};
function newVirtualNode(tag: VNodeTag, key?: null | string, vNodeProps?: any, realNode?: any): VNode {
return new VNode(tag, vNodeProps, key as null | string, realNode);
return new VirtualNode(tag, vNodeProps, key as null | string, realNode);
}
function isClassComponent(comp: Function) {
@ -106,6 +107,12 @@ export function createFragmentVNode(fragmentKey, fragmentProps) {
return vNode;
}
export function createReactiveVNode(content) {
const vNode = newVirtualNode(ReactiveComponent, null, content);
vNode.shouldUpdate = true;
return vNode;
}
export function createDomTextVNode(content) {
const vNode = newVirtualNode(DomText, null, content);
vNode.shouldUpdate = true;
@ -231,32 +238,32 @@ export function onlyUpdateChildVNodes(processing: VNode): VNode | null {
}
// 当跳过子树更新时父节点path更新时需要更新所有子树path
if (processing.child && processing.path !== processing.child.path.slice(0, processing.path.length)) {
// bfs更新子树path
const queue: VNode[] = [];
const putChildrenIntoQueue = (vNode: VNode) => {
const child = vNode.child;
if (child) {
queue.push(child);
let sibling = child.next;
while (sibling) {
queue.push(sibling);
sibling = sibling.next;
}
}
};
putChildrenIntoQueue(processing);
while (queue.length) {
const vNode = queue.shift()!;
markVNodePath(vNode);
putChildrenIntoQueue(vNode);
}
}
// if (processing.child && processing.path !== processing.child.path.slice(0, processing.path.length)) {
// // bfs更新子树path
// const queue: VNode[] = [];
//
// const putChildrenIntoQueue = (vNode: VNode) => {
// const child = vNode.child;
// if (child) {
// queue.push(child);
// let sibling = child.next;
// while (sibling) {
// queue.push(sibling);
// sibling = sibling.next;
// }
// }
// };
//
// putChildrenIntoQueue(processing);
//
// while (queue.length) {
// const vNode = queue.shift()!;
//
// markVNodePath(vNode);
//
// putChildrenIntoQueue(vNode);
// }
// }
// 子树无需工作
return null;
}

View File

@ -33,3 +33,4 @@ export const SuspenseComponent = 'SuspenseComponent';
export const MemoComponent = 'MemoComponent';
export const LazyComponent = 'LazyComponent';
export const IncompleteClassComponent = 'IncompleteClassComponent';
export const ReactiveComponent = 'ReactiveComponent';

View File

@ -19,7 +19,7 @@
import type { VNode } from '../Types';
import { DomComponent, DomPortal, DomText, TreeRoot } from './VNodeTags';
import { DomComponent, DomPortal, DomText, TreeRoot, ReactiveComponent } from './VNodeTags';
import { getNearestVNode } from '../../dom/DOMInternalKeys';
import { Addition, InitFlag } from './VNodeFlags';
import { BELONG_CLASS_VNODE_KEY } from './VNode';
@ -129,15 +129,22 @@ export function clearVNode(vNode: VNode) {
const hook = window.__INULA_DEV_HOOK__;
hook.deleteVNode(vNode);
}
if (vNode.attrRContexts) {
vNode.attrRContexts = null;
}
if (vNode.compRContext) {
vNode.compRContext = null;
}
}
// 是dom类型的vNode
export function isDomVNode(node: VNode) {
return node.tag === DomComponent || node.tag === DomText;
return node.tag === DomComponent || node.tag === DomText || node.tag === ReactiveComponent;
}
// 是容器类型的vNode
function isDomContainer(vNode: VNode): boolean {
export function isDomContainer(vNode: VNode): boolean {
return vNode.tag === DomComponent || vNode.tag === TreeRoot || vNode.tag === DomPortal;
}
@ -277,3 +284,15 @@ export function findRoot(targetVNode, targetDom) {
}
return targetVNode;
}
export function findDOMContainer(vNode: VNode): VNode {
let parent = vNode.parent;
while (parent !== null) {
if (isDomContainer(parent)) {
break;
}
parent = parent.parent;
}
return parent as VNode;
}