diff --git a/packages/inula/package.json b/packages/inula/package.json index 6da6afac..5700fecb 100644 --- a/packages/inula/package.json +++ b/packages/inula/package.json @@ -1,5 +1,5 @@ { - "name": "inulajs", + "name": "inulajs-reactive", "description": "Inulajs is a JavaScript framework library.", "keywords": [ "inulajs" diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-add.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-add.test.js new file mode 100644 index 00000000..f6a91f8b --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-add.test.js @@ -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
  • {item.name}
  • ; +}; + +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 ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('通过 push 在后面添加1行', () => { + render(, 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(, 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(, 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 ( +
    + + + + + + + + + + + { + eachItem => { + return ( + + + + + + + + + ); + } + } + +
    序号名称年龄性别名族其他
    {eachItem.value}{eachItem.value}{eachItem.value}{eachItem.value}{eachItem.value}{eachItem.value}
    +
    + ); + }; + render(, 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); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-delete.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-delete.test.js new file mode 100644 index 00000000..b53be008 --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-delete.test.js @@ -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
  • {item.name}
  • ; +}; + +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 ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('通过 pop 删除最后1行', () => { + render(, 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(, 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(, 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(, 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); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-update.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-update.test.js new file mode 100644 index 00000000..272c7f12 --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for-update.test.js @@ -0,0 +1,1172 @@ +/* + * 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, For } from '../../../../../src/index'; +import { beforeEach } from '@jest/globals'; +import { getRNode } from '../../../../../src/reactive/Utils'; + +const Row = ({ item }) => { + return ( +
  • + {item.name} +
  • + ); +}; + +const TableList = ({ item }) => { + return {item => }; +}; + +const RowList = ({ item }) => { + return {item => }; +}; + +let rObj; +let ref; +let appFn; +let App; +let itemFn; +let globalData; + +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 ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('通过 set 更新每行数据的id', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + let a = container.querySelector('#id-1'); + expect(a.innerHTML).toEqual('p1'); + + // 更新id + rObj.items.set([ + { id: 'id-11', name: 'p1' }, + { id: 'id-22', name: 'p2' }, + { id: 'id-33', name: 'p3' }, + { id: 'id-44', name: 'p4' }, + { id: 'id-55', name: 'p5' }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + expect(appFn).toHaveBeenCalledTimes(1); + + // 只有第一次渲染执行5次 + expect(itemFn).toHaveBeenCalledTimes(5); + + a = container.querySelector('#id-11'); + expect(a.innerHTML).toEqual('p1'); + }); + + it('等长 set 更新每行数据', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + let li = container.querySelector('#id-1'); + expect(li.innerHTML).toEqual('p1'); + + // 更新 + rObj.items.set([ + { id: 'id-1', name: 'p11' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p33' }, + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p55' }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + expect(appFn).toHaveBeenCalledTimes(1); + + // 只有第一次渲染执行5次 + expect(itemFn).toHaveBeenCalledTimes(5); + + li = container.querySelector('#id-1'); + expect(li.innerHTML).toEqual('p11'); + }); + + it('通过 reverse 反转数组', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 反转数组 + rObj.items.reverse(); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行5次,反转需要5-1次 + expect(itemFn).toHaveBeenCalledTimes(9); + + let li1 = container.querySelector('li:nth-child(1)'); + expect(li1.innerHTML).toEqual('p5'); + let li2 = container.querySelector('li:nth-child(2)'); + expect(li2.innerHTML).toEqual('p4'); + let li3 = container.querySelector('li:nth-child(3)'); + expect(li3.innerHTML).toEqual('p3'); + let li4 = container.querySelector('li:nth-child(4)'); + expect(li4.innerHTML).toEqual('p2'); + let li5 = container.querySelector('li:nth-child(5)'); + expect(li5.innerHTML).toEqual('p1'); + }); + + it('通过 copyWithin 修改数组', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 反转数组 + rObj.items.copyWithin(3, 1, 4); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + expect(appFn).toHaveBeenCalledTimes(1); + + expect(itemFn).toHaveBeenCalledTimes(7); + + // 结果是: + // { id: 'id-1', name: 'p1' }, + // { id: 'id-2', name: 'p2' }, + // { id: 'id-3', name: 'p3' }, + // { id: 'id-2', name: 'p2' }, + // { id: 'id-3', name: 'p3' }, + + let li1 = container.querySelector('li:nth-child(1)'); + expect(li1.innerHTML).toEqual('p1'); + let li2 = container.querySelector('li:nth-child(2)'); + expect(li2.innerHTML).toEqual('p2'); + let li3 = container.querySelector('li:nth-child(3)'); + expect(li3.innerHTML).toEqual('p3'); + let li4 = container.querySelector('li:nth-child(4)'); + expect(li4.innerHTML).toEqual('p2'); + let li5 = container.querySelector('li:nth-child(5)'); + expect(li5.innerHTML).toEqual('p3'); + }); +}); + +describe('测试 For 组件的更新,3层数据', () => { + beforeEach(() => { + ref = createRef(); + appFn = jest.fn(); + itemFn = jest.fn(); + + App = () => { + const _rObj = useReactive({ + items: [ + { + id: 'id-1', + items: [ + { + id: 'id-1', + items: [ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + ], + }, + { + id: 'id-2', + items: [ + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p5' }, + { id: 'id-6', name: 'p6' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p8' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + { + id: 'id-2', + items: [ + { + id: 'id-1', + items: [ + { id: 'id-10', name: 'p10' }, + { id: 'id-11', name: 'p11' }, + { id: 'id-12', name: 'p12' }, + ], + }, + { + id: 'id-2', + items: [ + { id: 'id-13', name: 'p13' }, + { id: 'id-14', name: 'p14' }, + { id: 'id-15', name: 'p15' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-16', name: 'p16' }, + { id: 'id-17', name: 'p17' }, + { id: 'id-18', name: 'p18' }, + ], + }, + ], + }, + ], + }); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('通过 set 更新第三层数据', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + + let li = container.querySelector('#id-4'); + expect(li.innerHTML).toEqual('p4'); + + // 更新 + rObj.items.set([ + { + id: 'id-1', + items: [ + { + id: 'id-1', + items: [ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + ], + }, + { + id: 'id-2', + items: [ + { id: 'id-4', name: 'p444' }, + { id: 'id-5', name: 'p5' }, + { id: 'id-6', name: 'p6' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p8' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + { + id: 'id-2', + items: [ + { + id: 'id-1', + items: [ + { id: 'id-10', name: 'p10' }, + { id: 'id-11', name: 'p11' }, + { id: 'id-12', name: 'p12' }, + ], + }, + { + id: 'id-2', + items: [ + { id: 'id-13', name: 'p13' }, + { id: 'id-14', name: 'p14' }, + { id: 'id-15', name: 'p15' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-16', name: 'p16' }, + { id: 'id-17', name: 'p17' }, + { id: 'id-18', name: 'p18' }, + ], + }, + ], + }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + expect(appFn).toHaveBeenCalledTimes(1); + + // 只有第一次渲染执行2次 + expect(itemFn).toHaveBeenCalledTimes(2); + + li = container.querySelector('#id-4'); + expect(li.innerHTML).toEqual('p444'); + }); + + it('通过 set 删除第3层数据', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + + let li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p8'); + + // 更新 + rObj.items.set([ + { + id: 'id-1', + items: [ + { + id: 'id-1', + items: [{ id: 'id-1', name: 'p1' }], + }, + { + id: 'id-2', + items: [ + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p5' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p888' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + { + id: 'id-2', + items: [ + { + id: 'id-1', + items: [ + { id: 'id-10', name: 'p10' }, + { id: 'id-11', name: 'p11' }, + { id: 'id-12', name: 'p12' }, + ], + }, + { + id: 'id-2', + items: [ + { id: 'id-13', name: 'p13' }, + { id: 'id-14', name: 'p14' }, + { id: 'id-15', name: 'p15' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-16', name: 'p16' }, + { id: 'id-17', name: 'p17' }, + { id: 'id-18', name: 'p18' }, + ], + }, + ], + }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(15); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行2次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(3); + + li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p888'); + }); + + it('通过 set 删除第1、3层数据', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + + let li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p8'); + + // 更新 + rObj.items.set([ + { + id: 'id-1', + items: [ + { + id: 'id-1', + items: [{ id: 'id-1', name: 'p1' }], + }, + { + id: 'id-2', + items: [ + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p5' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p888' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(6); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行2次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(3); + + li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p888'); + }); + + it('通过 set 删除第1、2、3层数据', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + + let li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p8'); + + // 更新 + rObj.items.set([ + { + id: 'id-1', + items: [ + { + id: 'id-2', + items: [ + { id: 'id-4', name: 'p4' }, + { id: 'id-5', name: 'p5' }, + ], + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p888' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行2次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(3); + + li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p888'); + }); + + it('通过 set 把数组设置成boolean和number', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + + // 更新 + rObj.items.set([ + { + id: 'id-1', + items: [ + { + id: 'id-1', + items: [true], + }, + { + id: 'id-2', + items: 11, + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p8' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行2次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(3); + }); + + it('通过 set 把数组设置成boolean和number,再修改下面数据部分', () => { + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(18); + + // 更新 + rObj.items.set([ + { + id: 'id-1', + items: [ + { + id: 'id-1', + items: [true], + }, + { + id: 'id-2', + items: 11, // 数组变数字,不会报错 + xxx: 'xxx', // 多出来的数据不影响 + }, + { + id: 'id-3', + items: [ + { id: 'id-7', name: 'p7' }, + { id: 'id-8', name: 'p888' }, + { id: 'id-9', name: 'p9' }, + ], + }, + ], + }, + ]); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行2次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(3); + + let li = container.querySelector('#id-8'); + expect(li.innerHTML).toEqual('p888'); + }); + + it('通过 set 把对象中的数组设置成boolean', () => { + App = () => { + const _rObj = useReactive({ + id: 'id-1', + items: [ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + ], + }); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ( +
  • + {item.name} +
  • + ); + }} +
    +
    + ); + }; + + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + + // 更新 + rObj.set({ + id: 'id-1', + items: [true], + }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(1); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行3次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(4); + }); +}); + +describe('测试 For 组件的更新,反复增删', () => { + beforeEach(() => { + ref = createRef(); + appFn = jest.fn(); + itemFn = jest.fn(); + + App = () => { + const _rObj = useReactive({ + items: [], + }); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('先用 splice 删除1行,再通过 set 新增2行', () => { + render(, container); + + function removeFirstRow() { + rObj.items.splice(0, 1); + } + + removeFirstRow(); + + // 新增2行 + rObj.items.set([ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + ]); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + // 再新增2行 + rObj.items.set( + rObj.items.concat([ + { id: 'id-3', name: 'p3' }, + { id: 'id-4', name: 'p4' }, + ]) + ); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + }); + + it('先用 set 新增6行,删除1行,交换两行位置', () => { + render(, container); + + // 新增2行 + rObj.items.set([ + { 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' }, + { id: 'id-6', name: 'p6' }, + ]); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(6); + + // 删除一行 + rObj.items.splice(0, 1); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + function swapRows() { + const arr = rObj.items.slice(); + const tmp = arr[1]; + arr[1] = arr[arr.length - 2]; + arr[arr.length - 2] = tmp; + rObj.items.set(arr); + } + + swapRows(); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + let li2 = container.querySelector('li:nth-child(2)'); + expect(li2.innerHTML).toEqual('p5'); + + let li4 = container.querySelector('li:nth-child(4)'); + expect(li4.innerHTML).toEqual('p3'); + }); + + it('先用 set 新增4行,交换两行位置,删除1行', () => { + render(, container); + + // 新增2行 + rObj.items.set([ + { id: 'id-1', name: 'p1' }, + { id: 'id-2', name: 'p2' }, + { id: 'id-3', name: 'p3' }, + { id: 'id-4', name: 'p4' }, + ]); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(4); + + function swapRows() { + const arr = rObj.items.slice(); + const tmp = arr[1]; + arr[1] = arr[arr.length - 2]; + arr[arr.length - 2] = tmp; + rObj.items.set(arr); + } + + // 前后边上第2行交换 + swapRows(); + + // 删除一行 + rObj.items.splice(0, 1); + + // 结果是: + // { id: 'id-3', name: 'p3' }, + // { id: 'id-2', name: 'p2' }, + // { id: 'id-4', name: 'p4' }, + + let li2 = container.querySelector('li:nth-child(2)'); + expect(li2.innerHTML).toEqual('p2'); + }); +}); + +describe('测试 For 组件的更新,直接修改raw数组对象', () => { + beforeEach(() => { + ref = createRef(); + appFn = jest.fn(); + itemFn = jest.fn(); + + globalData = { + 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' }, + ], + }; + + App = () => { + const _rObj = useReactive(globalData); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('向原始数组中增加1行数据,再通过 set 更新响应式数据,是不会更新的', () => { + render(, container); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 新增1行 + globalData.items.push({ id: 'id-6', name: 'p6' }); + + // 无法触发更新,因为globalData.items的引用相同,不会触发监听 + rObj.set(globalData); + + items = container.querySelectorAll('li'); + // 不会更新 + expect(items.length).toEqual(5); + }); + + it('应该直接修改响应式数据的方式', () => { + render(, container); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 直接修改响应式数据的方式,新增1行 + rObj.items.push({ id: 'id-6', name: 'p6' }); + items = container.querySelectorAll('li'); + expect(items.length).toEqual(6); + }); +}); + +describe('更新多属性对象', () => { + beforeEach(() => { + ref = createRef(); + appFn = jest.fn(); + itemFn = jest.fn(); + + globalData = { + items: [ + { id: 'id-1', name: 'p1', class: 'c1' }, + { id: 'id-2', name: 'p2', class: 'c2' }, + { id: 'id-3', name: 'p3', class: 'c3' }, + { id: 'id-4', name: 'p4', class: 'c4' }, + { id: 'id-5', name: 'p5', class: 'c5' }, + ], + }; + + const Row = ({ item }) => { + return ( +
  • + {item.name} +
  • + ); + }; + + App = () => { + const _rObj = useReactive(globalData); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('对象数据的属性类型变化,后面的属性更正常', () => { + render(, container); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + let li = container.querySelector('#id-2'); + expect(li.getAttribute('class')).toEqual('c2'); + + rObj.set({ + items: [ + { id: 'id-1', name: 'p1', class: 'c1' }, + { id: 'id-2', name: [true], class: 'c2222' }, + { id: 'id-3', name: 'p3', class: 'c3' }, + { id: 'id-4', name: 'p4', class: 'c4' }, + { id: 'id-5', name: 'p5', class: 'c5' }, + ], + }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + li = container.querySelector('#id-2'); + expect(li.getAttribute('class')).toEqual('c2222'); + }); +}); + +describe('在class组件中使用for组件', () => { + it('在类中使用reactive数据', () => { + let rObj; + let appInst; + const ref = createRef(); + + class App extends Inula.Component { + constructor(props) { + super(props); + + appInst = this; + + this.state = { + name: 1, + }; + + this._rObj = reactive(1); + rObj = this._rObj; + } + + render() { + return
    {this._rObj}
    ; + } + } + + render(, container); + + expect(ref.current.innerHTML).toEqual('1'); + + // 触发组件重新渲染 + appInst.setState({ name: 2 }); + + rObj.set('2'); + + // rObj只应该有一个依赖 + expect(rObj.usedRContexts.size).toEqual(1); + + expect(ref.current.innerHTML).toEqual('2'); + }); + + it('在类中使用reactive数组数据', () => { + let rObj; + let appInst; + const ref = createRef(); + + class Row extends Inula.Component { + constructor(props) { + super(props); + } + + render() { + const { item } = this.props; + return ( +
  • + {item.name} +
  • + ); + } + } + + class App extends Inula.Component { + constructor(props) { + super(props); + + appInst = this; + + this.state = { + name: 1, + }; + + this._rObj = reactive({ + items: [ + { id: 'id-1', name: 'p1', class: 'c1' }, + { id: 'id-2', name: 'p2', class: 'c2' }, + { id: 'id-3', name: 'p3', class: 'c3' }, + { id: 'id-4', name: 'p4', class: 'c4' }, + { id: 'id-5', name: 'p5', class: 'c5' }, + ], + }); + rObj = this._rObj; + } + + render() { + return ( +
    + + {item => { + return ; + }} + +
    + ); + } + } + + render(, container); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(5); + + // 直接修改响应式数据的方式,新增1行 + rObj.items.push({ id: 'id-6', name: 'p6' }); + items = container.querySelectorAll('li'); + expect(items.length).toEqual(6); + + // 触发组件重新渲染 + appInst.setState({ name: 2 }); + + // rObj只应该有一个依赖 + expect(getRNode(rObj.items).usedRContexts.size).toEqual(1); + }); + + describe('更新多属性对象', () => { + beforeEach(() => { + ref = createRef(); + appFn = jest.fn(); + itemFn = jest.fn(); + + globalData = { + items: [ + { id: 'id-1', name: 'p1', class: 'c1' }, + { id: 'id-2', name: 'p2', class: 'c2' }, + ], + }; + + const Row = ({ item }) => { + return ( +
  • + {item.name} +
  • + ); + }; + + App = () => { + const _rObj = useReactive(globalData); + rObj = _rObj; + + appFn(); + + return ( +
    + + {item => { + itemFn(); + return ; + }} + +
    + ); + }; + }); + + it('更新数组的一个原数据,调试subscribeAttr,只被调用一次', () => { + render(, container); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + let li = container.querySelector('#id-2'); + expect(li.getAttribute('class')).toEqual('c2'); + + rObj.set({ + items: [ + { id: 'id-1', name: 'p1', class: 'c1' }, + { id: 'id-2', name: 'p2', class: 'c222' }, + ], + }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + li = container.querySelector('#id-2'); + expect(li.getAttribute('class')).toEqual('c222'); + }); + + it('更新数组的一个原数据', () => { + render(, container); + + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + let li = container.querySelector('#id-2'); + expect(li.getAttribute('class')).toEqual('c2'); + + rObj.items[1].set({ id: 'id-2', name: 'p2', class: 'c222' }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(2); + + li = container.querySelector('#id-2'); + expect(li.getAttribute('class')).toEqual('c222'); + + expect(itemFn).toHaveBeenCalledTimes(2); + }); + + it('For的数组是基本数据,更改其中一个,另外两个能精准更新', () => { + const rowFn = jest.fn(); + + const Row = ({ item, index }) => { + rowFn(); + return
  • {item}
  • ; + }; + + const App = () => { + const _rObj = useReactive({ + id: 'id-1', + items: [{ a: 1 }, 2, 3], + }); + rObj = _rObj; + + appFn(); + + return ( +
    + + {(item, index) => { + itemFn(); + return ; + }} + +
    + ); + }; + + render(, container); + let items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + + // 更新 + rObj.set({ + id: 'id-1', + items: [2, 3, 4], + }); + + items = container.querySelectorAll('li'); + expect(items.length).toEqual(3); + expect(appFn).toHaveBeenCalledTimes(1); + + // 第一次渲染执行3次,更新也触发了1次 + expect(itemFn).toHaveBeenCalledTimes(4); + + // 第一次渲染执行3次,更新也触发了1次 + expect(rowFn).toHaveBeenCalledTimes(4); + }); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for.test.js new file mode 100644 index 00000000..585b60ba --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/component/For/reactive-component-for.test.js @@ -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
  • {item.name}
  • ; +}; + +describe('测试 For 组件', () => { + it('使用For组件遍历reactive“数组”', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const Item = ({ item }) => { + return
  • {item.name}
  • ; + }; + + const App = () => { + const _rObj = useReactive({ + items: [ + { name: 'p1', id: 1 }, + { name: 'p2', id: 2 }, + ], + }); + rObj = _rObj; + + fn(); + + return ( +
    + + {item => { + return ; + }} + +
    + ); + }; + + render(, 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
  • {item.name}
  • ; + }; + + const App = () => { + const _rObj = useReactive({ + items: [], + }); + rObj = _rObj; + + fn(); + + return ( +
    + + {item => { + return ; + }} + +
    + ); + }; + + render(, 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 ( + + {item.id} + + {item.label} + + + ); + }); + + const RowList = () => { + return {item => }; + }; + + const App = () => { + return ( +
    + + + + +
    +
    + ); + }; + + render(, 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 ( +
    + {_rObj.items.map(rItem => { + return
  • {rItem}
  • ; + })} +
    + ); + }; + + render(, 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 ( +
    + + {item => { + return ; + }} + +
    + ); + }; + + render(, 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); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-block.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-block.test.js new file mode 100644 index 00000000..fa89e36e --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-block.test.js @@ -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 ( +
    + 111 222 + + {() => { + fn1(); + const count = _rObj.count.get(); + return ( + <> +
    Count: {count}
    +
    {_rColor}
    + + ); + }} +
    +
    + ); + }; + + render(, container); + + expect(ref.current.innerHTML).toEqual('111 222
    Count: 0
    blue
    '); + + // 会触发View刷新 + rObj.count.set(1); + expect(fn).toHaveBeenCalledTimes(1); + expect(fn1).toHaveBeenCalledTimes(2); + expect(ref.current.innerHTML).toEqual('111 222
    Count: 1
    blue
    '); + + // 不会触发View刷新 + rColor.set('red'); + expect(fn).toHaveBeenCalledTimes(1); + expect(fn1).toHaveBeenCalledTimes(2); + expect(ref.current.innerHTML).toEqual('111 222
    Count: 1
    red
    '); + }); + + 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包裹 +
    + 111 222 + {_rObj} +
    + ); + }; + + render(, container); + expect(ref1.current.innerHTML).toEqual('111 222blue'); + rObj.set('red'); + expect(fn).toHaveBeenCalledTimes(1); + expect(ref1.current.innerHTML).toEqual('111 222red'); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-combination.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-combination.test.js new file mode 100644 index 00000000..6871e89f --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-combination.test.js @@ -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
  • {item.name}
  • ; + }; + + 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 ( + <> + dataList.get().length > 0} else={() =>
    }> +
    + {item => } +
    + +
    {listLen}
    + + ); + }; + render(, 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
  • {item.name}
  • ; + }; + + let reactiveObj; + const ref = createRef(); + const App = () => { + const dataList = useReactive([]); + reactiveObj = dataList; + + return ( + + dataList.get().length === 0}> +
    + + dataList.get().length > 0}> +
    + {item => } +
    +
    + + ); + }; + render(, 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); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-rtext.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-rtext.test.js new file mode 100644 index 00000000..167ef990 --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-rtext.test.js @@ -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包裹 +
    + 111 222 + {_rObj} +
    + ); + }; + + render(, container); + expect(ref1.current.innerHTML).toEqual('111 222blue'); + rObj.set('red'); + expect(fn).toHaveBeenCalledTimes(1); + expect(ref1.current.innerHTML).toEqual('111 222red'); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-show.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-show.test.js new file mode 100644 index 00000000..ba02ae46 --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-show.test.js @@ -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 + + Loading... +
    + } + > +
    + {_rObj} +
    +
    + ); + }; + + render(, 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 + +
    {_rObj}
    +
    + ); + }; + + render(, 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 + + Loading... +
    + } + > +
    + {_rObj.color} +
    +
    + ); + }; + + render(, 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 + _rObj.color} + else={ +
    + Loading... +
    + } + > +
    + {_rObj.color} +
    +
    + ); + }; + + render(, 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 + _rObj.color} + else={() => ( +
    + Loading... +
    + )} + > + {() => { + const text = useCompute(() => { + return _rObj.color.get() + _count.get(); + }); + + return ( +
    + {text} +
    + ); + }} +
    + ); + }; + + render(, 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); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-switch.test.js b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-switch.test.js new file mode 100644 index 00000000..de736293 --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/component/reactive-component-switch.test.js @@ -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 ( + nothing}> + {/*if不能写成 _rObj === 'red' 或者 _rObj.get() === 'red' */} + _rObj.get() === 'blue'}> +
    + {_rObj} +
    +
    + _rObj.get() === 'red'}> +
    + {_rObj} +
    +
    + _rObj.get() === 'yellow'}> +
    + {_rObj} +
    +
    +
    + ); + }; + + render(, 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); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/computed.test.js b/packages/inula/scripts/__tests__/ReactivityTest/computed.test.js new file mode 100644 index 00000000..78ddac3c --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/computed.test.js @@ -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
    {computedVal}
    ; + } + } + + render(, 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次 + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-compute.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-compute.test.js new file mode 100644 index 00000000..716d36cb --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-compute.test.js @@ -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
    {_cObj.get()}
    ; + }; + + render(, 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
    {_cObj}
    ; + }; + + render(, 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
    {cObj.len}
    ; + }; + + render(, 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
    {dd}
    ; + }; + + render(, 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
    {words}
    ; + }; + + render(, 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 ( + + ); + }; + render(, 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 ( +
    + + {item => { + itemFn(); + return ( +
  • + {item.name} +
  • + ); + }} +
    +
    + ); + }; + + render(, 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 ; + }; + render(, 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'); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-memory.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-memory.test.js new file mode 100644 index 00000000..70877a52 --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-memory.test.js @@ -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 ( + nothing}> + {/*if不能写成 _rObj === 'red' 或者 _rObj.get() === 'red' */} + _rObj.get() === 'blue'}> +
    + {_rObj} +
    +
    + _rObj.get() === 'red'}> +
    + {_rObj} +
    +
    + _rObj.get() === 'yellow'}> +
    + {_rObj} +
    +
    +
    + ); + }; + + render(, 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); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-mix-use.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-mix-use.test.js new file mode 100644 index 00000000..ef145de7 --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-mix-use.test.js @@ -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 ( + + + + ); + }; + + const Child = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return
    ; + }; + + render(, container); + expect(ref.current.className).toEqual('blue'); + + // 改变了DOM结构 + isShow.set(false); + expect(ref.current).toEqual(null); + + update(); + + expect(ref.current).toEqual(null); + }); + + +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-mixed-children.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-mixed-children.test.js new file mode 100644 index 00000000..c6d81e22 --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-mixed-children.test.js @@ -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下面有多个元素 +
    Count: {_rObj}
    + ); + }; + + render(, 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下面有多个元素 +
    Count: {_rObj.count}
    + ); + }; + + render(, container); + expect(ref1.current.innerHTML).toEqual('Count: 0'); + rObj.count.set(1); + expect(fn).toHaveBeenCalledTimes(1); + expect(ref1.current.innerHTML).toEqual('Count: 1'); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-object.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-object.test.js new file mode 100644 index 00000000..90ae29fc --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-object.test.js @@ -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
    {_rObj.color}
    ; + }; + + render(, 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
    {_rObj.data.framework}
    ; + }; + + render(, 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
    {_rObj.data.framework.js}
    ; + }; + + render(, 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
    {_rObj.rdata.framework}
    ; + }; + + render(, 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 ; + }; + + const Child = ({ color }) => { + fn2(); + + const cl = useCompute(() => { + return 'cl-' + color.get(); + }); + + return
    ; + }; + + render(, 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
    {_rObj.data.color}
    ; + }; + + render(, 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
    {_rObj.data.color}
    ; + }; + + render(, 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
    {_rObj.data.length}
    ; + }; + + render(, 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
    {_rObj.data[0].name}
    ; + }; + + render(, 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 ( +
    + {_rObj.items.get().map(item => { + return
  • {item.name}
  • ; + })} +
    + ); + }; + + render(, 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
  • {item.name}
  • ; + }; + + const App = () => { + const _rObj = useReactive({ + items: [ + { name: 'p1', id: 1 }, + { name: 'p2', id: 2 }, + ], + }); + rObj = _rObj; + + fn(); + + return ( +
    + {/*items必须要调用get()才能map*/} + {_rObj.items.get().map(item => { + return ; + })} +
    + ); + }; + + render(, 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
  • {item.name}
  • ; + }; + + const App = () => { + const _rObj = useReactive({ + items: [ + { name: 'p1', id: 1 }, + { name: 'p2', id: 2 }, + { name: 'p3', id: 3 }, + ], + }); + rObj = _rObj; + + fn(); + + return ( +
    + {_rObj.items.map(item => { + return ; + })} +
    + ); + }; + + render(, 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“数组”,孩子是Item,Item对象具有响应式', () => { + let rObj; + const ref = createRef(); + const fn = jest.fn(); + const Item = ({ item }) => { + const id = useCompute(() => { + return `id-${item.id.get()}`; + }); + + return ( +
  • + {item.name} +
  • + ); + }; + + const App = () => { + const _rObj = useReactive({ + items: [ + { name: 'p1', id: 1 }, + { name: 'p2', id: 2 }, + ], + }); + rObj = _rObj; + + fn(); + + return ( +
    + {_rObj.items.map(item => { + return ; + })} +
    + ); + }; + + render(, 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); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-primitive.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-primitive.test.js new file mode 100644 index 00000000..79f44515 --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-primitive.test.js @@ -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
    {_rObj.get()}
    ; + }; + render(, 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
    {_rObj}
    ; + }; + render(, 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
    ; + }; + render(, 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
    ; + }; + render(, 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
    ; + }; + render(, 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
    ; + }; + render(, 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 ; + }; + render(, 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 ; + }; + render(, 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 ; + }; + + const Child = () => { + const _rObj = useReactive('blue'); + rObj = _rObj; + + fn(); + + return
    ; + }; + + render(, 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
    {cp}
    ; + }; + render(, 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
    {cp}
    ; + }; + render(, container); + expect(ref.current.innerHTML).toEqual('1'); + + rObj.data.set({ num: '2' }); + expect(ref.current.innerHTML).toEqual('2'); + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-props-update.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-props-update.test.js new file mode 100644 index 00000000..e489877d --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-props-update.test.js @@ -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 ( +
    + {_rObj.color} +
    + ); + }; + + render(, 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 ( +
    + {_rObj.color} +
    + ); + }; + + render(, 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 ; + }; + render(, container); + expect(ref.current.value).toEqual('blue'); + + rObj.color.set('red'); + expect(ref.current.value).toEqual('red'); + + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/inula/scripts/__tests__/ReactivityTest/reactive-watch.test.js b/packages/inula/scripts/__tests__/ReactivityTest/reactive-watch.test.js new file mode 100644 index 00000000..63e5d257 --- /dev/null +++ b/packages/inula/scripts/__tests__/ReactivityTest/reactive-watch.test.js @@ -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
    {_rObj}
    ; + }; + + render(, 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
    {_rObj}
    ; + }; + + render(, 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
    {_rObj}
    ; + }; + + render(, 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 ( +
    + + {item => { + itemFn(); + return ( +
  • + {item.name} +
  • + ); + }} +
    +
    + ); + }; + + render(, 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); + }); +}); diff --git a/packages/inula/scripts/rollup/rollup.config.js b/packages/inula/scripts/rollup/rollup.config.js index 489ed37d..4ab91071 100644 --- a/packages/inula/scripts/rollup/rollup.config.js +++ b/packages/inula/scripts/rollup/rollup.config.js @@ -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, '../..'); diff --git a/packages/inula/src/dom/DOMOperator.ts b/packages/inula/src/dom/DOMOperator.ts index ba032254..56eef88f 100644 --- a/packages/inula/src/dom/DOMOperator.ts +++ b/packages/inula/src/dom/DOMOperator.ts @@ -188,7 +188,7 @@ export function submitDomUpdate(tag: string, vNode: VNode) { } export function clearText(dom: Element): void { - dom.innerHTML = ''; + dom.textContent = ''; } // 添加child元素 diff --git a/packages/inula/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/packages/inula/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts index 44a7ac8f..7bedfb83 100644 --- a/packages/inula/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts +++ b/packages/inula/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts @@ -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, isNativeTag: boolean, isInit: boolean): void { @@ -43,8 +51,8 @@ export function setDomProps(dom: Element, props: Record, 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, isNativeTa } // 找出两个 DOM 属性的差别,生成需要更新的属性集合 -export function compareProps(oldProps: Record, newProps: Record): Record { +export function compareProps(oldProps: Record, newProps: Record, dom: Element): Record { let updatesForStyle = {}; - const toUpdateProps = {}; + const toUpdateProps: Record = {}; const keysOfOldProps = Object.keys(oldProps); const keysOfNewProps = Object.keys(newProps); @@ -103,11 +111,14 @@ export function compareProps(oldProps: Record, newProps: Record, newProps: Record, 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( + toUpdateProps: Record, + propName: string, + propVal: V, + dom: Element, + formatter?: (value: V) => R +) { + const rawVal: any = handleReactiveProp(dom, propName, propVal); + + toUpdateProps[propName] = formatter ? formatter(rawVal) : rawVal; +} diff --git a/packages/inula/src/dom/DOMPropertiesHandler/StyleHandler.ts b/packages/inula/src/dom/DOMPropertiesHandler/StyleHandler.ts index 46496f2f..ae8bc0fc 100644 --- a/packages/inula/src/dom/DOMPropertiesHandler/StyleHandler.ts +++ b/packages/inula/src/dom/DOMPropertiesHandler/StyleHandler.ts @@ -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); } }); } diff --git a/packages/inula/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts b/packages/inula/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts index 1e6c3e23..27cc30e0 100644 --- a/packages/inula/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts +++ b/packages/inula/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts @@ -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) { diff --git a/packages/inula/src/dom/valueHandler/InputValueHandler.ts b/packages/inula/src/dom/valueHandler/InputValueHandler.ts index 6d4821d3..cc6d6f8e 100644 --- a/packages/inula/src/dom/valueHandler/InputValueHandler.ts +++ b/packages/inula/src/dom/valueHandler/InputValueHandler.ts @@ -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; diff --git a/packages/inula/src/dom/valueHandler/TextareaValueHandler.ts b/packages/inula/src/dom/valueHandler/TextareaValueHandler.ts index 4b7a178a..60015a51 100644 --- a/packages/inula/src/dom/valueHandler/TextareaValueHandler.ts +++ b/packages/inula/src/dom/valueHandler/TextareaValueHandler.ts @@ -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 值 diff --git a/packages/inula/src/external/JSXElement.ts b/packages/inula/src/external/JSXElement.ts index 89a7a3c7..31f412cf 100644 --- a/packages/inula/src/external/JSXElement.ts +++ b/packages/inula/src/external/JSXElement.ts @@ -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(); diff --git a/packages/inula/src/index.ts b/packages/inula/src/index.ts index 116ab1f2..30def4cc 100644 --- a/packages/inula/src/index.ts +++ b/packages/inula/src/index.ts @@ -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'; diff --git a/packages/inula/src/reactive/Atom.ts b/packages/inula/src/reactive/Atom.ts new file mode 100644 index 00000000..b1add441 --- /dev/null +++ b/packages/inula/src/reactive/Atom.ts @@ -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 extends AtomNode { + root: Root; + parent: RNode; + parentKey: string | symbol | null; + usedRContexts?: RContextSet; +} + +const atomKey = Symbol('atomAccessKey'); +export const atomSymbol = Symbol('ReactiveAtom'); + +// 对原始数据做响应式的时候使用 +export function Atom(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; + 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 { + trackReactiveData(this); + return this.read(); +}; + +Atom.prototype.set = function (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 { + return getRNodeVal(this); +}; diff --git a/packages/inula/src/reactive/Batch.ts b/packages/inula/src/reactive/Batch.ts new file mode 100644 index 00000000..750851a8 --- /dev/null +++ b/packages/inula/src/reactive/Batch.ts @@ -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); + } + } +} diff --git a/packages/inula/src/reactive/Computed.ts b/packages/inula/src/reactive/Computed.ts new file mode 100644 index 00000000..8c2fdfcb --- /dev/null +++ b/packages/inula/src/reactive/Computed.ts @@ -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(fn: () => T | Promise): Computed { + 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; +} + +export { computed }; diff --git a/packages/inula/src/reactive/DiffUtils.ts b/packages/inula/src/reactive/DiffUtils.ts new file mode 100644 index 00000000..a3946996 --- /dev/null +++ b/packages/inula/src/reactive/DiffUtils.ts @@ -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(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(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(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[] = [{ 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(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); +// } diff --git a/packages/inula/src/reactive/RContext.ts b/packages/inula/src/reactive/RContext.ts new file mode 100644 index 00000000..0a058f98 --- /dev/null +++ b/packages/inula/src/reactive/RContext.ts @@ -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; + +/** + * 响应式上下文,有4种情况:1、组件级。2、Block级。3、watch/computed。4、DOM级。 + */ +export class RContext { + callback: RContextCallback; + + // 记录该Context使用时的参数 + params: RContextParam; + + // 记录该RContext中使用到的Reactive中的RContextSet + reactiveDependents: Set | 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(); + } + reactive.usedRContexts.add(rContext); + + if (rContext.reactiveDependents === null) { + rContext.reactiveDependents = new Set(); + } + 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(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; + } +} + diff --git a/packages/inula/src/reactive/RContextCreator.ts b/packages/inula/src/reactive/RContextCreator.ts new file mode 100644 index 00000000..7513bbd4 --- /dev/null +++ b/packages/inula/src/reactive/RContextCreator.ts @@ -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(); + +/** + * 创建组件(函数组件或Class组件)级别Dependent + * @param renderFn 函数组件 或 Class的render + * @param vNode + */ +export function createComponentDependent(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或children),创建一个专门更新该属性的上下文,当响应式数据变化就触发该上下文的callback + * @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); +} + +/** + * 处理
    Count: {_rObj}
    + * @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); + } +} diff --git a/packages/inula/src/reactive/RNode.ts b/packages/inula/src/reactive/RNode.ts new file mode 100644 index 00000000..fe0a20f0 --- /dev/null +++ b/packages/inula/src/reactive/RNode.ts @@ -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(raw?: T): ReactiveProxy { + if (isPrimitive(raw) || raw === null || raw === undefined) { + return new Atom(raw); + } else { + const node = createRootRNode(raw); + const proxyObj = createProxy(node); + node.proxy = proxyObj; + return proxyObj as ReactiveProxy; + } +} + +export function createRootRNode(raw?: T): RootRNode { + const root: RootRNode = { + 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 { + 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); + } +} diff --git a/packages/inula/src/reactive/Reactive.ts b/packages/inula/src/reactive/Reactive.ts new file mode 100644 index 00000000..6c558004 --- /dev/null +++ b/packages/inula/src/reactive/Reactive.ts @@ -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(obj: T): ReactiveProxy { + return createReactiveObj(obj); +} diff --git a/packages/inula/src/reactive/Utils.ts b/packages/inula/src/reactive/Utils.ts new file mode 100644 index 00000000..f2fd2057 --- /dev/null +++ b/packages/inula/src/reactive/Utils.ts @@ -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 | AtomNodeFn { + 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 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(rObj: ProxyRNode | AtomNode): RNode { + return isReactiveProxy(rObj) ? rObj[GET_R_NODE] : rObj; +} + +export function getRNodeFromProxy(rObj: ProxyRNode): RNode { + return rObj[GET_R_NODE]; +} + +export function isPromise(obj: unknown): obj is Promise { + 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; +} diff --git a/packages/inula/src/reactive/Var.ts b/packages/inula/src/reactive/Var.ts new file mode 100644 index 00000000..9d1e6b1a --- /dev/null +++ b/packages/inula/src/reactive/Var.ts @@ -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, +}; diff --git a/packages/inula/src/reactive/Watch.ts b/packages/inula/src/reactive/Watch.ts new file mode 100644 index 00000000..9841a246 --- /dev/null +++ b/packages/inula/src/reactive/Watch.ts @@ -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(); + } +} diff --git a/packages/inula/src/reactive/components/Block.tsx b/packages/inula/src/reactive/components/Block.tsx new file mode 100644 index 00000000..7396c51a --- /dev/null +++ b/packages/inula/src/reactive/components/Block.tsx @@ -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函数会被重新执行,不会影响父组件。 + * + {() => { + const count = _rObj.count.get(); + return <> +
    {count}
    + ; + }} +
    + * @param children + */ +export function Block({ children }: { children: () => JSXElement }): any { + const result = calculateReactive(children); + + return result; +} diff --git a/packages/inula/src/reactive/components/For.tsx b/packages/inula/src/reactive/components/For.tsx new file mode 100644 index 00000000..d7253932 --- /dev/null +++ b/packages/inula/src/reactive/components/For.tsx @@ -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({ + 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 | 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); + } +} diff --git a/packages/inula/src/reactive/components/RText.ts b/packages/inula/src/reactive/components/RText.ts new file mode 100644 index 00000000..93b07705 --- /dev/null +++ b/packages/inula/src/reactive/components/RText.ts @@ -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,不刷新 +); diff --git a/packages/inula/src/reactive/components/Show.tsx b/packages/inula/src/reactive/components/Show.tsx new file mode 100644 index 00000000..daff4b8a --- /dev/null +++ b/packages/inula/src/reactive/components/Show.tsx @@ -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({ + 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; +} diff --git a/packages/inula/src/reactive/components/Switch.ts b/packages/inula/src/reactive/components/Switch.ts new file mode 100644 index 00000000..ee9a9117 --- /dev/null +++ b/packages/inula/src/reactive/components/Switch.ts @@ -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({ + children, + default: df, +}: { + children: JSXElement[] | Record 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; +} diff --git a/packages/inula/src/reactive/proxy/RProxyHandler.ts b/packages/inula/src/reactive/proxy/RProxyHandler.ts new file mode 100644 index 00000000..768baf5f --- /dev/null +++ b/packages/inula/src/reactive/proxy/RProxyHandler.ts @@ -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([ + 'push', + 'pop', + 'splice', + 'shift', + 'unshift', + 'reverse', + 'sort', + 'fill', + 'from', + 'copyWithin', +]); + +// 数组的遍历方法 +const LOOP_ARR_FNS = new Set(['forEach', 'map', 'every', 'some', 'filter', 'join']); + +export function createProxy(proxyNode: RNode): ProxyRNode { + return new Proxy(proxyNode, { + get, + set, + }); +} + +const GetFns: Record 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() {} diff --git a/packages/inula/src/reactive/types.ts b/packages/inula/src/reactive/types.ts new file mode 100644 index 00000000..e1c0abc5 --- /dev/null +++ b/packages/inula/src/reactive/types.ts @@ -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; + + /** + * 下面属性computed使用 + * @param {readOnly} 标识computed 是否处于写入状态 + */ + readOnly?: boolean; +} + +export type PrimitiveType = string | number | boolean; + +export type ValueType = { [K in keyof T]: any } | Record | PrimitiveType; + +export interface BaseNodeFns { + /** + * 返回响应式对象的值,自动追踪依赖 + */ + get(): T; + + /** + * 返回响应式对象的值,不追踪依赖 + */ + read(): T; +} + +export interface ProxyRNodeFn extends BaseNodeFns { + set>(value: V | ((prev: T) => V)); +} + +export interface AtomNodeFn extends BaseNodeFns { + set(value: V | ((prev: T) => V)); +} + +type PropsRecursive = T[K] extends PrimitiveType + ? AtomNode + : T[K] extends any[] + ? any[] & ProxyRNodeFn + : T extends Record + ? RecurseType + : T[K]; + +export type ProxyRNodeProps = { + [K in keyof T]: PropsRecursive>; +}; + +export type ComputedProps = { + [K in keyof T]: PropsRecursive>; +}; + +export type ProxyRNode = ProxyRNodeFn & ProxyRNodeProps; + +export type AtomNode = AtomNodeFn; + +export type Computed = BaseNodeFns & ComputedProps; + +export type ReactiveProxy = T extends PrimitiveType ? AtomNode : ProxyRNode; + +export interface BaseRNode { + // 标识Node类型 atomSymbol,nodeSymbol,computedSymbol + type: symbol; + root: Root; + children?: Map | Atom>; + usedRContexts?: RContextSet; + proxy?: any; + + diffOperator?: DiffOperator; + diffOperators?: DiffOperator[]; + states?: ArrayState[]; +} + +export interface RootRNode extends BaseRNode { + parentKey: null; + parent: null; +} + +export interface ChildrenRNode extends BaseRNode { + parentKey: string | symbol; + parent: RNode; +} + +export type RNode = RootRNode | ChildrenRNode; + +export type Reactive = RNode | Atom; + +export interface RContextParam { + vNode?: VNode; + reactive?: Reactive; +} + +export type RContextCallback = (params: RContextParam, reactive: Reactive) => void; diff --git a/packages/inula/src/renderer/TreeBuilder.ts b/packages/inula/src/renderer/TreeBuilder.ts index dff56e07..4f9a2026 100644 --- a/packages/inula/src/renderer/TreeBuilder.ts +++ b/packages/inula/src/renderer/TreeBuilder.ts @@ -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中调用setState,parent可能是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中调用setState,parent可能是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); diff --git a/packages/inula/src/renderer/Types.ts b/packages/inula/src/renderer/Types.ts index 9a80f6c6..2ebc7c74 100644 --- a/packages/inula/src/renderer/Types.ts +++ b/packages/inula/src/renderer/Types.ts @@ -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的值,类组件包含defaultProps,Lazy组件不包含 + 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> | null, // 保存hook + depContexts: Array> | null, // FunctionComponent和ClassComponent对context的依赖列表 + isDepContextChange: boolean, // context是否变更 + dirtyNodes: Array | 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> | 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 | null, // 保存要更新的节点 + delegatedEvents: Set, + + belongClassVNode: VNode | null, // 记录JSXElement所属class vNode,处理ref的时候使用 + + // 状态管理器HorizonX使用 + isStoreChange: boolean, + observers: Set | null, // 记录这个函数组件/类组件依赖哪些Observer + classComponentWillUnmount: Function | null, // HorizonX会在classComponentWillUnmount中清除对VNode的引入用 + src: Source | null, // 节点所在代码位置 + + // reactive + attrRContexts: Set | null, + compRContext: any | null, +} diff --git a/packages/inula/src/renderer/diff/nodeDiffComparator.ts b/packages/inula/src/renderer/diff/nodeDiffComparator.ts index 8de6f392..1216916c 100644 --- a/packages/inula/src/renderer/diff/nodeDiffComparator.ts +++ b/packages/inula/src/renderer/diff/nodeDiffComparator.ts @@ -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, +): 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是迭代器类型 diff --git a/packages/inula/src/renderer/hooks/HookExternal.ts b/packages/inula/src/renderer/hooks/HookExternal.ts index 44e2dc04..67a887de 100644 --- a/packages/inula/src/renderer/hooks/HookExternal.ts +++ b/packages/inula/src/renderer/hooks/HookExternal.ts @@ -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( // 兼容react-redux export const useDebugValue = () => {}; + +// reactive hooks +export function useAtom(initialValue: T): [AtomNode, (value: T) => void] { + return useAtomImpl(initialValue); +} + +export function useCompute(compute: () => T) { + return useComputedImpl(compute); +} +export function useComputed(compute: () => T) { + return useComputedImpl(compute); +} + +export function useReactive(obj: T) { + return useReactiveImpl(obj); +} + +export function useWatch(fn: () => any | Reactive, callback?: () => void) { + return useWatchImpl(fn, callback); +} diff --git a/packages/inula/src/renderer/hooks/UseWatch.ts b/packages/inula/src/renderer/hooks/UseWatch.ts new file mode 100644 index 00000000..8ef9b885 --- /dev/null +++ b/packages/inula/src/renderer/hooks/UseWatch.ts @@ -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); +} diff --git a/packages/inula/src/renderer/hooks/reactive/UseAtom.ts b/packages/inula/src/renderer/hooks/reactive/UseAtom.ts new file mode 100644 index 00000000..9c1a5b81 --- /dev/null +++ b/packages/inula/src/renderer/hooks/reactive/UseAtom.ts @@ -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(initialValue: T): [AtomNode, (value: T) => void] { + const stage = getHookStage(); + let atom: Atom; + + 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]; +} diff --git a/packages/inula/src/renderer/hooks/reactive/UseCompute.ts b/packages/inula/src/renderer/hooks/reactive/UseCompute.ts new file mode 100644 index 00000000..c3dc9d39 --- /dev/null +++ b/packages/inula/src/renderer/hooks/reactive/UseCompute.ts @@ -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(fn: () => T): Computed { + return useMemoImpl(() => { + return computed(fn); + }, []); +} diff --git a/packages/inula/src/renderer/hooks/reactive/UseReactive.ts b/packages/inula/src/renderer/hooks/reactive/UseReactive.ts new file mode 100644 index 00000000..14176890 --- /dev/null +++ b/packages/inula/src/renderer/hooks/reactive/UseReactive.ts @@ -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(obj) { + const stage = getHookStage(); + let reactiveObj: any; + + switch (stage) { + case HookStage.Init: + reactiveObj = reactive(obj); + + createHook(reactiveObj); + break; + case HookStage.Update: + reactiveObj = getCurrentHook().state as unknown as Reactive; + break; + default: + throwNotInFuncError(); + } + + // 组件销毁时,清除effect + useEffectImpl( + () => () => { + disposeReactive(reactiveObj); + }, + [] + ); + + return reactiveObj as ReactiveProxy; +} diff --git a/packages/inula/src/renderer/hooks/reactive/UseWatch.ts b/packages/inula/src/renderer/hooks/reactive/UseWatch.ts new file mode 100644 index 00000000..0661a652 --- /dev/null +++ b/packages/inula/src/renderer/hooks/reactive/UseWatch.ts @@ -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(fn: () => any | Reactive, callback?: () => void): void { + useMemoImpl(() => { + watch(fn, callback); + }, []); + + // 组件销毁时 + useEffectImpl(() => () => {}, []); +} diff --git a/packages/inula/src/renderer/render/ClassComponent.ts b/packages/inula/src/renderer/render/ClassComponent.ts index 2792425c..fc3a1620 100644 --- a/packages/inula/src/renderer/render/ClassComponent.ts +++ b/packages/inula/src/renderer/render/ClassComponent.ts @@ -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); } diff --git a/packages/inula/src/renderer/render/DomComponent.ts b/packages/inula/src/renderer/render/DomComponent.ts index 65044cb2..8a2476b6 100644 --- a/packages/inula/src/renderer/render/DomComponent.ts +++ b/packages/inula/src/renderer/render/DomComponent.ts @@ -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); } }, diff --git a/packages/inula/src/renderer/render/FunctionComponent.ts b/packages/inula/src/renderer/render/FunctionComponent.ts index ed3cd4a4..8419f581 100644 --- a/packages/inula/src/renderer/render/FunctionComponent.ts +++ b/packages/inula/src/renderer/render/FunctionComponent.ts @@ -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 ); diff --git a/packages/inula/src/renderer/render/ReactiveComponent.ts b/packages/inula/src/renderer/render/ReactiveComponent.ts new file mode 100644 index 00000000..33956375 --- /dev/null +++ b/packages/inula/src/renderer/render/ReactiveComponent.ts @@ -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)); + } +} diff --git a/packages/inula/src/renderer/render/index.ts b/packages/inula/src/renderer/render/index.ts index 3422db55..9f1c4dc1 100644 --- a/packages/inula/src/renderer/render/index.ts +++ b/packages/inula/src/renderer/render/index.ts @@ -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, }; diff --git a/packages/inula/src/renderer/submit/LifeCycleHandler.ts b/packages/inula/src/renderer/submit/LifeCycleHandler.ts index 34d34e8d..8d85661b 100644 --- a/packages/inula/src/renderer/submit/LifeCycleHandler.ts +++ b/packages/inula/src/renderer/submit/LifeCycleHandler.ts @@ -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, diff --git a/packages/inula/src/renderer/submit/Submit.ts b/packages/inula/src/renderer/submit/Submit.ts index 53230f4e..700f0f3a 100644 --- a/packages/inula/src/renderer/submit/Submit.ts +++ b/packages/inula/src/renderer/submit/Submit.ts @@ -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) { submitDeletion(node); } if (isClear) { + // submitClear2(node); submitClear(node); } } diff --git a/packages/inula/src/renderer/utils/vNodePath.ts b/packages/inula/src/renderer/utils/vNodePath.ts index e2b5fd6f..7d8b4a93 100644 --- a/packages/inula/src/renderer/utils/vNodePath.ts +++ b/packages/inula/src/renderer/utils/vNodePath.ts @@ -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) { diff --git a/packages/inula/src/renderer/vnode/VNode.ts b/packages/inula/src/renderer/vnode/VNode.ts index 1a1ba3ea..10c5978e 100644 --- a/packages/inula/src/renderer/vnode/VNode.ts +++ b/packages/inula/src/renderer/vnode/VNode.ts @@ -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的值,类组件包含defaultProps,Lazy组件不包含 - 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的值,类组件包含defaultProps,Lazy组件不包含 + 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> | null; // 保存hook - depContexts: Array> | null; // FunctionComponent和ClassComponent对context的依赖列表 - isDepContextChange: boolean; // context是否变更 - dirtyNodes: Array | 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> | 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 | null; // 保存要更新的节点 - delegatedEvents: Set; - - // @ts-ignore - [BELONG_CLASS_VNODE_KEY]: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用 - - // 状态管理器InulaX使用 - isStoreChange: boolean; - observers: Set | 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(); - this.delegatedEvents = new Set(); - 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(); - 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(); + this.delegatedEvents = new Set(); + 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(); + 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; } } diff --git a/packages/inula/src/renderer/vnode/VNodeCreator.ts b/packages/inula/src/renderer/vnode/VNodeCreator.ts index 142bac33..21b09cb2 100644 --- a/packages/inula/src/renderer/vnode/VNodeCreator.ts +++ b/packages/inula/src/renderer/vnode/VNodeCreator.ts @@ -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; } diff --git a/packages/inula/src/renderer/vnode/VNodeTags.ts b/packages/inula/src/renderer/vnode/VNodeTags.ts index dc358aef..c824ab75 100644 --- a/packages/inula/src/renderer/vnode/VNodeTags.ts +++ b/packages/inula/src/renderer/vnode/VNodeTags.ts @@ -33,3 +33,4 @@ export const SuspenseComponent = 'SuspenseComponent'; export const MemoComponent = 'MemoComponent'; export const LazyComponent = 'LazyComponent'; export const IncompleteClassComponent = 'IncompleteClassComponent'; +export const ReactiveComponent = 'ReactiveComponent'; diff --git a/packages/inula/src/renderer/vnode/VNodeUtils.ts b/packages/inula/src/renderer/vnode/VNodeUtils.ts index 2d32beaf..25cb2753 100644 --- a/packages/inula/src/renderer/vnode/VNodeUtils.ts +++ b/packages/inula/src/renderer/vnode/VNodeUtils.ts @@ -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; +}