parent
fa7915fd0d
commit
1e08541b20
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
printWidth: 120, // 一行120字符数,如果超过会进行换行
|
||||||
|
tabWidth: 2, // tab等2个空格
|
||||||
|
useTabs: false, // 用空格缩进行
|
||||||
|
semi: true, // 行尾使用分号
|
||||||
|
singleQuote: true, // 字符串使用单引号
|
||||||
|
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
|
||||||
|
jsxSingleQuote: false, // 在JSX中使用双引号
|
||||||
|
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
|
||||||
|
bracketSpacing: true, // 对象的括号间增加空格
|
||||||
|
bracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
|
||||||
|
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
|
||||||
|
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
|
||||||
|
endOfLine: 'lf', // 仅限换行(\n)
|
||||||
|
};
|
|
@ -0,0 +1,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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
'@babel/preset-env',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'@babel/preset-typescript',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
};
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
coverageDirectory: 'coverage',
|
||||||
|
resetModules: true,
|
||||||
|
|
||||||
|
rootDir: process.cwd(),
|
||||||
|
|
||||||
|
setupFilesAfterEnv: [require.resolve('./tests/jest/jestSetting.js')],
|
||||||
|
|
||||||
|
testEnvironment: 'jest-environment-jsdom-sixteen',
|
||||||
|
|
||||||
|
testMatch: [
|
||||||
|
'<rootDir>/tests/**/*.test.js',
|
||||||
|
'<rootDir>/tests/**/*.test.ts',
|
||||||
|
'<rootDir>/tests/**/*.test.tsx',
|
||||||
|
],
|
||||||
|
|
||||||
|
timers: 'fake',
|
||||||
|
};
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "inula-novdom",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "no vdom runtime",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest --config=jest.config.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"inula-reactive": "workspace:^0.0.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openGauss 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, untrack } from 'inula-reactive';
|
||||||
|
|
||||||
|
// TODO 需要优化为精细更新
|
||||||
|
export function For<T>(props: { each: any; children?: (value: any, index: number) => any }) {
|
||||||
|
let list = props.each,
|
||||||
|
mapFn = props.children,
|
||||||
|
items = [],
|
||||||
|
mapped = [],
|
||||||
|
disposers = [],
|
||||||
|
len = 0;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
let newItems = list.get() || [];
|
||||||
|
|
||||||
|
untrack(() => {
|
||||||
|
let i, j;
|
||||||
|
|
||||||
|
let newLen = newItems.length,
|
||||||
|
newIndices,
|
||||||
|
newIndicesNext,
|
||||||
|
temp,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
newEnd,
|
||||||
|
item;
|
||||||
|
|
||||||
|
if (newLen === 0) {
|
||||||
|
// 没新数据
|
||||||
|
if (len !== 0) {
|
||||||
|
disposers = [];
|
||||||
|
items = [];
|
||||||
|
mapped = [];
|
||||||
|
len = 0;
|
||||||
|
}
|
||||||
|
} else if (len === 0) {
|
||||||
|
// 上一次没有数据
|
||||||
|
mapped = new Array(newLen);
|
||||||
|
for (j = 0; j < newLen; j++) {
|
||||||
|
items[j] = newItems[j];
|
||||||
|
mapped[j] = mapFn(list[j]);
|
||||||
|
}
|
||||||
|
len = newLen;
|
||||||
|
} else { // 都有数据
|
||||||
|
// temp = new Array(newLen);
|
||||||
|
//
|
||||||
|
// // 从前往后,判断相等。但是这种前度比较经常是不生效的,比如数组的值相同,指针不一样
|
||||||
|
// for (start = 0, end = Math.min(len, newLen); start < end && items[start] === newItems[start]; start++);
|
||||||
|
//
|
||||||
|
// // 从后往前
|
||||||
|
// for (
|
||||||
|
// end = len - 1, newEnd = newLen - 1;
|
||||||
|
// end >= start && newEnd >= start && items[end] === newItems[newEnd]; // 值相等
|
||||||
|
// end--, newEnd--
|
||||||
|
// ) {
|
||||||
|
// temp[newEnd] = mapped[end]; // 把dom取出来
|
||||||
|
// }
|
||||||
|
// // 从start -> newEnd就是不相等的
|
||||||
|
//
|
||||||
|
// newIndices = new Map();
|
||||||
|
// newIndicesNext = new Array(newEnd + 1);
|
||||||
|
// for (j = newEnd; j >= start; j--) {
|
||||||
|
// item = newItems[j];
|
||||||
|
// i = newIndices.get(item);
|
||||||
|
// newIndicesNext[j] = i === undefined ? -1 : i; // item数据可能指针相同,重复。因为是倒序遍历,所以i就是相同数据下一个位置。
|
||||||
|
// newIndices.set(item, j); // 新数据,放到map中
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 遍历旧数据
|
||||||
|
// for (i = start; i <= end; i++) {
|
||||||
|
// item = items[i];
|
||||||
|
// j = newIndices.get(item); // j 是相同数据的第一个
|
||||||
|
// // 旧行数据,在新数据中存在,在j位置
|
||||||
|
// if (j !== undefined && j !== -1) {
|
||||||
|
// temp[j] = mapped[i]; // 把就dom放到新的j位置
|
||||||
|
// j = newIndicesNext[j];
|
||||||
|
// newIndices.set(item, j); // 修改map里面的位置,改为下一个
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 往mapped中放入start - newLen的dom数据
|
||||||
|
// for (j = start; j < newLen; j++) { // 按新数据来遍历
|
||||||
|
// if (j in temp) {
|
||||||
|
// mapped[j] = temp[j]; // 直接取旧的
|
||||||
|
// } else {
|
||||||
|
// mapped[j] = mapFn(list[j]); // 创建新的dom
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 0 - start 数据没有变动
|
||||||
|
// mapped = mapped.slice(0, (len = newLen)); // 如果newLen小于len,就截断
|
||||||
|
// items = newItems.slice(0);
|
||||||
|
|
||||||
|
// 假设新旧相同行数据已经更新
|
||||||
|
if (newLen > len) {
|
||||||
|
for (let i = len; i < newLen; i++) {
|
||||||
|
mapped[i] = mapFn(list[i]); // 创建新的dom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0 - start 数据没有变动
|
||||||
|
mapped = mapped.slice(0, (len = newLen)); // 如果newLen小于len,就截断
|
||||||
|
items = newItems.slice(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapped;
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openGauss 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 { isReactiveObj } from 'inula-reactive';
|
||||||
|
|
||||||
|
export function Show<T>({
|
||||||
|
if: rIf,
|
||||||
|
else: rElse,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
if: any | (() => T);
|
||||||
|
else?: any;
|
||||||
|
children: any;
|
||||||
|
}): any {
|
||||||
|
return () => {
|
||||||
|
const ifValue: any = calculateReactive(rIf);
|
||||||
|
|
||||||
|
let child: any = null;
|
||||||
|
if (ifValue) {
|
||||||
|
child = typeof children === 'function' ? children() : children;
|
||||||
|
} else {
|
||||||
|
child = typeof rElse === 'function' ? rElse() : rElse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return child;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果是函数就执行,如果是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;
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openGauss 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 { insert } from './dom';
|
||||||
|
|
||||||
|
export function createComponent<T>(Comp, props) {
|
||||||
|
return Comp(props || ({} as T));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render(code, element, init, options = {}) {
|
||||||
|
let disposer;
|
||||||
|
|
||||||
|
createRoot(dispose => {
|
||||||
|
disposer = dispose;
|
||||||
|
if (element === document) {
|
||||||
|
code();
|
||||||
|
} else {
|
||||||
|
insert(element, code(), element.firstChild ? null : undefined, init);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposer();
|
||||||
|
element.textContent = '';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let Owner;
|
||||||
|
let Listener;
|
||||||
|
|
||||||
|
function createRoot(fn) {
|
||||||
|
const listener = Listener;
|
||||||
|
const owner = Owner;
|
||||||
|
const unowned = fn.length === 0;
|
||||||
|
const current = owner;
|
||||||
|
const root = {
|
||||||
|
owned: null,
|
||||||
|
cleanups: null,
|
||||||
|
context: current ? current.context : null,
|
||||||
|
owner: current,
|
||||||
|
};
|
||||||
|
const updateFn = () => {
|
||||||
|
// fn(() => cleanNode(root));
|
||||||
|
fn(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
Owner = root;
|
||||||
|
Listener = null;
|
||||||
|
try {
|
||||||
|
return runUpdates(updateFn, true);
|
||||||
|
} finally {
|
||||||
|
Listener = listener;
|
||||||
|
Owner = owner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Updates, Effects;
|
||||||
|
let ExecCount = 0;
|
||||||
|
|
||||||
|
function runUpdates(fn, init) {
|
||||||
|
if (Updates) return fn();
|
||||||
|
let wait = false;
|
||||||
|
if (!init) Updates = [];
|
||||||
|
if (Effects) {
|
||||||
|
wait = true;
|
||||||
|
} else {
|
||||||
|
Effects = [];
|
||||||
|
}
|
||||||
|
ExecCount++;
|
||||||
|
// try {
|
||||||
|
const res = fn();
|
||||||
|
// completeUpdates(wait);
|
||||||
|
return res;
|
||||||
|
// } catch (err) {
|
||||||
|
// if (!wait) Effects = null;
|
||||||
|
// Updates = null;
|
||||||
|
// // handleError(err);
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,295 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openGauss 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 'inula-reactive';
|
||||||
|
import { isReactiveObj } from 'inula-reactive';
|
||||||
|
|
||||||
|
export function template(html) {
|
||||||
|
let node;
|
||||||
|
const create = () => {
|
||||||
|
const t = document.createElement('template');
|
||||||
|
t.innerHTML = html;
|
||||||
|
return t.content.firstChild;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fn = () => (node || (node = create())).cloneNode(true);
|
||||||
|
fn.cloneNode = fn;
|
||||||
|
return fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insert(parent, accessor, marker, initial) {
|
||||||
|
if (marker !== undefined && !initial) {
|
||||||
|
initial = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReactiveObj(accessor)) {
|
||||||
|
watchRender(current => {
|
||||||
|
return insertExpression(parent, accessor.get(), current, marker);
|
||||||
|
}, initial);
|
||||||
|
} else {
|
||||||
|
return insertExpression(parent, accessor, initial, marker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchRender(fn, prevValue) {
|
||||||
|
let nextValue = prevValue;
|
||||||
|
watch(() => {
|
||||||
|
nextValue = fn(nextValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertExpression(parent, value, current, marker, unwrapArray) {
|
||||||
|
while (typeof current === 'function') current = current();
|
||||||
|
if (value === current) return value;
|
||||||
|
|
||||||
|
const t = typeof value,
|
||||||
|
multi = marker !== undefined;
|
||||||
|
|
||||||
|
if (t === 'string' || t === 'number') {
|
||||||
|
if (t === 'number') value = value.toString();
|
||||||
|
if (multi) {
|
||||||
|
let node = current[0];
|
||||||
|
if (node && node.nodeType === 3) {
|
||||||
|
node.data = value;
|
||||||
|
} else {
|
||||||
|
node = document.createTextNode(value);
|
||||||
|
}
|
||||||
|
current = cleanChildren(parent, current, marker, node);
|
||||||
|
} else {
|
||||||
|
if (current !== '' && typeof current === 'string') {
|
||||||
|
current = parent.firstChild.data = value;
|
||||||
|
} else current = parent.textContent = value;
|
||||||
|
}
|
||||||
|
} else if (value == null || t === 'boolean') {
|
||||||
|
current = cleanChildren(parent, current, marker);
|
||||||
|
} else if (t === 'function') {
|
||||||
|
// 在watch里面执行
|
||||||
|
watch(() => {
|
||||||
|
let v = value();
|
||||||
|
while (isReactiveObj(v)) {
|
||||||
|
v = v.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
current = insertExpression(parent, v, current, marker);
|
||||||
|
});
|
||||||
|
return () => current;
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
// return [() => {}, () => {}, ...]
|
||||||
|
const array = [];
|
||||||
|
const currentArray = current && Array.isArray(current);
|
||||||
|
if (normalizeIncomingArray(array, value, current, unwrapArray)) {
|
||||||
|
watchRender(() => (current = insertExpression(parent, array, current, marker, true)));
|
||||||
|
return () => current;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array.length === 0) {
|
||||||
|
// 当前没有节点
|
||||||
|
current = cleanChildren(parent, current, marker);
|
||||||
|
if (multi) return current;
|
||||||
|
} else if (currentArray) {
|
||||||
|
if (current.length === 0) {
|
||||||
|
appendNodes(parent, array, marker); // 原来没有节点
|
||||||
|
} else {
|
||||||
|
reconcileArrays(parent, current, array); // 原本有节点,现在也有节点
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current && cleanChildren(parent);
|
||||||
|
appendNodes(parent, array);
|
||||||
|
}
|
||||||
|
current = array;
|
||||||
|
} else if (value.nodeType) {
|
||||||
|
if (Array.isArray(current)) {
|
||||||
|
if (multi) return (current = cleanChildren(parent, current, marker, value));
|
||||||
|
cleanChildren(parent, current, null, value);
|
||||||
|
} else if (current == null || current === '' || !parent.firstChild) {
|
||||||
|
parent.appendChild(value);
|
||||||
|
} else {
|
||||||
|
parent.replaceChild(value, parent.firstChild);
|
||||||
|
}
|
||||||
|
current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanChildren(parent, current, marker, replacement) {
|
||||||
|
if (marker === undefined) {
|
||||||
|
return (parent.textContent = '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = replacement || document.createTextNode('');
|
||||||
|
if (current.length) {
|
||||||
|
let inserted = false;
|
||||||
|
for (let i = current.length - 1; i >= 0; i--) {
|
||||||
|
const el = current[i];
|
||||||
|
if (node !== el) {
|
||||||
|
const isParent = el.parentNode === parent;
|
||||||
|
if (!inserted && !i) {
|
||||||
|
isParent ? parent.replaceChild(node, el) : parent.insertBefore(node, marker);
|
||||||
|
} else {
|
||||||
|
isParent && el.remove();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inserted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parent.insertBefore(node, marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [node];
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendNodes(parent, array, marker = null) {
|
||||||
|
for (let i = 0, len = array.length; i < len; i++) {
|
||||||
|
parent.insertBefore(array[i], marker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拆解数组,如:[[a, b], [c, d], ...] to [a, b, c, d]
|
||||||
|
function normalizeIncomingArray(normalized, array, unwrap) {
|
||||||
|
let dynamic = false;
|
||||||
|
for (let i = 0, len = array.length; i < len; i++) {
|
||||||
|
let item = array[i],
|
||||||
|
t;
|
||||||
|
if (item == null || item === true || item === false) {
|
||||||
|
// matches null, undefined, true or false
|
||||||
|
// skip
|
||||||
|
} else if (Array.isArray(item)) {
|
||||||
|
dynamic = normalizeIncomingArray(normalized, item) || dynamic;
|
||||||
|
} else if ((t = typeof item) === 'string' || t === 'number') {
|
||||||
|
normalized.push(document.createTextNode(item));
|
||||||
|
} else if (t === 'function') {
|
||||||
|
if (unwrap) {
|
||||||
|
while (typeof item === 'function') item = item();
|
||||||
|
dynamic = normalizeIncomingArray(normalized, Array.isArray(item) ? item : [item]) || dynamic;
|
||||||
|
} else {
|
||||||
|
normalized.push(item);
|
||||||
|
dynamic = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
normalized.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dynamic;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原本有节点,现在也有节点
|
||||||
|
export default function reconcileArrays(parentNode, oldChildren, newChildren) {
|
||||||
|
let nLength = newChildren.length,
|
||||||
|
oEnd = oldChildren.length,
|
||||||
|
nEnd = nLength,
|
||||||
|
oStart = 0,
|
||||||
|
nStart = 0,
|
||||||
|
after = oldChildren[oEnd - 1].nextSibling,
|
||||||
|
map = null;
|
||||||
|
|
||||||
|
while (oStart < oEnd || nStart < nEnd) {
|
||||||
|
// 从前到后对比相同内容
|
||||||
|
if (oldChildren[oStart] === newChildren[nStart]) {
|
||||||
|
oStart++;
|
||||||
|
nStart++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 从后往前对比相同内容
|
||||||
|
while (oldChildren[oEnd - 1] === newChildren[nEnd - 1]) {
|
||||||
|
oEnd--;
|
||||||
|
nEnd--;
|
||||||
|
}
|
||||||
|
// append
|
||||||
|
if (oEnd === oStart) {
|
||||||
|
// 旧节点全部和新节点相同(不是完全相同, 如:旧 abcd 新 abefcd)
|
||||||
|
const node = nEnd < nLength ? (nStart ? newChildren[nStart - 1].nextSibling : newChildren[nEnd - nStart]) : after;
|
||||||
|
|
||||||
|
while (nStart < nEnd) {
|
||||||
|
parentNode.insertBefore(newChildren[nStart++], node);
|
||||||
|
}
|
||||||
|
// remove
|
||||||
|
} else if (nEnd === nStart) {
|
||||||
|
// 新节点全部和新节点相同(不是完全相同, 如:旧 abefcd 新 abcd)
|
||||||
|
while (oStart < oEnd) {
|
||||||
|
if (!map || !map.has(oldChildren[oStart])) {
|
||||||
|
oldChildren[oStart].remove();
|
||||||
|
}
|
||||||
|
oStart++;
|
||||||
|
}
|
||||||
|
// swap backward
|
||||||
|
} else if (oldChildren[oStart] === newChildren[nEnd - 1] && newChildren[nStart] === oldChildren[oEnd - 1]) {
|
||||||
|
// 如:旧 ab ef cd 新 ab fe cd
|
||||||
|
const node = oldChildren[--oEnd].nextSibling;
|
||||||
|
parentNode.insertBefore(newChildren[nStart++], oldChildren[oStart++].nextSibling); // 如:旧 abe f fcd
|
||||||
|
parentNode.insertBefore(newChildren[--nEnd], node); // 如:旧 abeff e cd
|
||||||
|
|
||||||
|
oldChildren[oEnd] = newChildren[nEnd];
|
||||||
|
// fallback to map
|
||||||
|
} else {
|
||||||
|
// 如:旧 ab feww cd 新 ab hgeht cd
|
||||||
|
if (!map) {
|
||||||
|
map = new Map();
|
||||||
|
let i = nStart;
|
||||||
|
|
||||||
|
while (i < nEnd) {
|
||||||
|
map.set(newChildren[i], i++); // 收集 hgeht
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = map.get(oldChildren[oStart]);
|
||||||
|
if (index != null) {
|
||||||
|
// 如:e就在newChildren中
|
||||||
|
if (nStart < index && index < nEnd) {
|
||||||
|
// 且位置在新节点 之间
|
||||||
|
let i = oStart,
|
||||||
|
sequence = 1,
|
||||||
|
t;
|
||||||
|
|
||||||
|
while (++i < oEnd && i < nEnd) {
|
||||||
|
// 如:旧 ab feww cd 新 ab hgeht cd, e 的 sequence 是 2 ?
|
||||||
|
if ((t = map.get(oldChildren[i])) == null || t !== index + sequence) break;
|
||||||
|
sequence++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sequence > index - nStart) {
|
||||||
|
const node = oldChildren[oStart];
|
||||||
|
while (nStart < index) {
|
||||||
|
parentNode.insertBefore(newChildren[nStart++], node);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parentNode.replaceChild(newChildren[nStart++], oldChildren[oStart++]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oStart++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oldChildren[oStart++].remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAttribute(node, name, value) {
|
||||||
|
if (value == null) {
|
||||||
|
node.removeAttribute(name);
|
||||||
|
} else {
|
||||||
|
node.setAttribute(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function className(node, value) {
|
||||||
|
if (value == null) {
|
||||||
|
node.removeAttribute('class');
|
||||||
|
} else {
|
||||||
|
node.className = value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openGauss 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const $$EVENTS = "_$DX_DELEGATE";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在 document上注册事件
|
||||||
|
* @param eventNames
|
||||||
|
* @param document
|
||||||
|
*/
|
||||||
|
export function delegateEvents(eventNames, document = window.document) {
|
||||||
|
const e = document[$$EVENTS] || (document[$$EVENTS] = new Set());
|
||||||
|
for (let i = 0, l = eventNames.length; i < l; i++) {
|
||||||
|
const name = eventNames[i];
|
||||||
|
if (!e.has(name)) {
|
||||||
|
e.add(name);
|
||||||
|
document.addEventListener(name, eventHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function clearDelegatedEvents(document = window.document) {
|
||||||
|
if (document[$$EVENTS]) {
|
||||||
|
for (let name of document[$$EVENTS].keys()) document.removeEventListener(name, eventHandler);
|
||||||
|
delete document[$$EVENTS];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function eventHandler(e) {
|
||||||
|
const key = `$$${e.type}`;
|
||||||
|
let node = (e.composedPath && e.composedPath()[0]) || e.target;
|
||||||
|
if (e.target !== node) {
|
||||||
|
Object.defineProperty(e, "target", {
|
||||||
|
configurable: true,
|
||||||
|
value: node
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Object.defineProperty(e, "currentTarget", {
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
return node || document;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 冒泡执行事件
|
||||||
|
while (node) {
|
||||||
|
const handler = node[key];
|
||||||
|
if (handler && !node.disabled) {
|
||||||
|
const data = node[`${key}Data`];
|
||||||
|
data !== undefined ? handler.call(node, data, e) : handler.call(node, e);
|
||||||
|
if (e.cancelBubble) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = node._$host || node.parentNode || node.host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addEventListener(node, name, handler, delegate) {
|
||||||
|
if (delegate) {
|
||||||
|
if (Array.isArray(handler)) {
|
||||||
|
node[`$$${name}`] = handler[0];
|
||||||
|
node[`$$${name}Data`] = handler[1];
|
||||||
|
} else {
|
||||||
|
node[`$$${name}`] = handler;
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(handler)) {
|
||||||
|
const handlerFn = handler[0];
|
||||||
|
node.addEventListener(name, (handler[0] = e => handlerFn.call(node, handler[1], e)));
|
||||||
|
} else {
|
||||||
|
node.addEventListener(name, handler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
global.container = null;
|
||||||
|
global.beforeEach(() => {
|
||||||
|
// 创建一个 DOM 元素作为渲染目标
|
||||||
|
global.container = document.createElement('div');
|
||||||
|
document.body.appendChild(global.container);
|
||||||
|
});
|
||||||
|
|
||||||
|
global.afterEach(() => {
|
||||||
|
global.container.remove();
|
||||||
|
global.container = null;
|
||||||
|
});
|
|
@ -0,0 +1,572 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { computed, reactive, watch } from 'inula-reactive';
|
||||||
|
import { template as _$template, insert as _$insert, setAttribute as _$setAttribute } from '../src/dom';
|
||||||
|
import { createComponent as _$createComponent, render } from '../src/core';
|
||||||
|
import { delegateEvents as _$delegateEvents, addEventListener as _$addEventListener } from '../src/event';
|
||||||
|
|
||||||
|
import { Show } from '../src/components/Show';
|
||||||
|
import { For } from '../src/components/For';
|
||||||
|
|
||||||
|
describe('test no-vdom', () => {
|
||||||
|
it('简单的使用signal', () => {
|
||||||
|
/**
|
||||||
|
* 源码:
|
||||||
|
* const CountingComponent = () => {
|
||||||
|
* const [count, setCount] = useSignal(0);
|
||||||
|
*
|
||||||
|
* return <div id="count">Count value is {count()}.</div>;
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* render(() => <CountingComponent />, container);
|
||||||
|
*/
|
||||||
|
|
||||||
|
let g_count;
|
||||||
|
|
||||||
|
// 编译后:
|
||||||
|
const _tmpl$ = /*#__PURE__*/ _$template(`<div id="count">Count value is <!>.`);
|
||||||
|
const CountingComponent = () => {
|
||||||
|
const count = reactive(0);
|
||||||
|
g_count = count;
|
||||||
|
|
||||||
|
return (() => {
|
||||||
|
const _el$ = _tmpl$(),
|
||||||
|
_el$2 = _el$.firstChild,
|
||||||
|
_el$4 = _el$2.nextSibling,
|
||||||
|
_el$3 = _el$4.nextSibling;
|
||||||
|
_$insert(_el$, count, _el$4);
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
render(() => _$createComponent(CountingComponent, {}), container);
|
||||||
|
|
||||||
|
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 0<!---->.');
|
||||||
|
|
||||||
|
g_count.set(c => c + 1);
|
||||||
|
|
||||||
|
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return数组,click事件', () => {
|
||||||
|
/**
|
||||||
|
* 源码:
|
||||||
|
* const CountingComponent = () => {
|
||||||
|
* const [count, setCount] = createSignal(0);
|
||||||
|
* const add = () => {
|
||||||
|
* setCount((c) => c + 1);
|
||||||
|
* }
|
||||||
|
* return <>
|
||||||
|
* <div id="count">Count value is {count()}.</div>
|
||||||
|
* <div><button onClick={add}>add</button></div>
|
||||||
|
* </>;
|
||||||
|
* };
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 编译后:
|
||||||
|
const _tmpl$ = /*#__PURE__*/ _$template(`<div id="count">Count value is <!>.`),
|
||||||
|
_tmpl$2 = /*#__PURE__*/ _$template(`<div><button id="btn">add`);
|
||||||
|
const CountingComponent = () => {
|
||||||
|
const count = reactive(0);
|
||||||
|
const add = () => {
|
||||||
|
count.set(c => c + 1);
|
||||||
|
};
|
||||||
|
return [
|
||||||
|
(() => {
|
||||||
|
const _el$ = _tmpl$(),
|
||||||
|
_el$2 = _el$.firstChild,
|
||||||
|
_el$4 = _el$2.nextSibling,
|
||||||
|
_el$3 = _el$4.nextSibling;
|
||||||
|
_$insert(_el$, count, _el$4);
|
||||||
|
return _el$;
|
||||||
|
})(),
|
||||||
|
(() => {
|
||||||
|
const _el$5 = _tmpl$2(),
|
||||||
|
_el$6 = _el$5.firstChild;
|
||||||
|
_el$6.$$click = add;
|
||||||
|
return _el$5;
|
||||||
|
})(),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
render(() => _$createComponent(CountingComponent, {}), container);
|
||||||
|
|
||||||
|
_$delegateEvents(['click']);
|
||||||
|
|
||||||
|
container.querySelector('#btn').click();
|
||||||
|
|
||||||
|
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return 自定义组件', () => {
|
||||||
|
/**
|
||||||
|
* 源码:
|
||||||
|
* const CountValue = (props) => {
|
||||||
|
* return <div>Count value is {props.count} .</div>;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const CountingComponent = () => {
|
||||||
|
* const [count, setCount] = createSignal(0);
|
||||||
|
* const add = () => {
|
||||||
|
* setCount((c) => c + 1);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* return <div>
|
||||||
|
* <CountValue count={count} />
|
||||||
|
* <div><button onClick={add}>add</button></div>
|
||||||
|
* </div>;
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* render(() => <CountingComponent />, document.getElementById("app"));
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 编译后:
|
||||||
|
const _tmpl$ = /*#__PURE__*/ _$template(`<div id="count">Count value is <!>.`),
|
||||||
|
_tmpl$2 = /*#__PURE__*/ _$template(`<div><div><button id="btn">add`);
|
||||||
|
const CountValue = props => {
|
||||||
|
return (() => {
|
||||||
|
const _el$ = _tmpl$(),
|
||||||
|
_el$2 = _el$.firstChild,
|
||||||
|
_el$4 = _el$2.nextSibling,
|
||||||
|
_el$3 = _el$4.nextSibling;
|
||||||
|
_$insert(_el$, () => props.count, _el$4);
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
const CountingComponent = () => {
|
||||||
|
const count = reactive(0);
|
||||||
|
const add = () => {
|
||||||
|
count.set(c => c + 1);
|
||||||
|
};
|
||||||
|
return (() => {
|
||||||
|
const _el$5 = _tmpl$2(),
|
||||||
|
_el$6 = _el$5.firstChild,
|
||||||
|
_el$7 = _el$6.firstChild;
|
||||||
|
_$insert(
|
||||||
|
_el$5,
|
||||||
|
_$createComponent(CountValue, {
|
||||||
|
count: count,
|
||||||
|
}),
|
||||||
|
_el$6
|
||||||
|
);
|
||||||
|
_el$7.$$click = add;
|
||||||
|
return _el$5;
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
render(() => _$createComponent(CountingComponent, {}), container);
|
||||||
|
_$delegateEvents(['click']);
|
||||||
|
|
||||||
|
container.querySelector('#btn').click();
|
||||||
|
|
||||||
|
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('使用Show组件', () => {
|
||||||
|
/**
|
||||||
|
* 源码:
|
||||||
|
* const CountValue = (props) => {
|
||||||
|
* return <div id="count">Count value is {props.count()}.</div>;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const CountingComponent = () => {
|
||||||
|
* const [count, setCount] = createSignal(0);
|
||||||
|
* const add = () => {
|
||||||
|
* setCount((c) => c + 1);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* return <div>
|
||||||
|
* <Show when={count() > 0} fallback={<CountValue count={999} />}>
|
||||||
|
* <CountValue count={count} />
|
||||||
|
* </Show>
|
||||||
|
* <div><button id="btn" onClick={add}>add</button></div>
|
||||||
|
* </div>;
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* render(() => <CountingComponent />, document.getElementById("app"));
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 编译后:
|
||||||
|
const _tmpl$ = /*#__PURE__*/ _$template(`<div id="count">Count value is <!>.`),
|
||||||
|
_tmpl$2 = /*#__PURE__*/ _$template(`<div><div><button id="btn">add`);
|
||||||
|
const CountValue = props => {
|
||||||
|
return (() => {
|
||||||
|
const _el$ = _tmpl$(),
|
||||||
|
_el$2 = _el$.firstChild,
|
||||||
|
_el$4 = _el$2.nextSibling,
|
||||||
|
_el$3 = _el$4.nextSibling;
|
||||||
|
_$insert(_el$, () => props.count, _el$4);
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
const CountingComponent = () => {
|
||||||
|
const count = reactive(0);
|
||||||
|
const add = () => {
|
||||||
|
count.set(c => c + 1);
|
||||||
|
};
|
||||||
|
return (() => {
|
||||||
|
const _el$5 = _tmpl$2(),
|
||||||
|
_el$6 = _el$5.firstChild,
|
||||||
|
_el$7 = _el$6.firstChild;
|
||||||
|
_$insert(
|
||||||
|
_el$5,
|
||||||
|
_$createComponent(Show, {
|
||||||
|
get if() {
|
||||||
|
return computed(() => count.get() > 0);
|
||||||
|
},
|
||||||
|
get else() {
|
||||||
|
return _$createComponent(CountValue, {
|
||||||
|
count: 999,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
get children() {
|
||||||
|
return _$createComponent(CountValue, {
|
||||||
|
count: count,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
_el$6
|
||||||
|
);
|
||||||
|
_el$7.$$click = add;
|
||||||
|
return _el$5;
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
render(() => _$createComponent(CountingComponent, {}), container);
|
||||||
|
_$delegateEvents(['click']);
|
||||||
|
|
||||||
|
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 999<!---->.');
|
||||||
|
|
||||||
|
container.querySelector('#btn').click();
|
||||||
|
|
||||||
|
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('使用For组件', () => {
|
||||||
|
/**
|
||||||
|
* 源码:
|
||||||
|
* const Todo = (props) => {
|
||||||
|
* return <div>Count value is {props.todo.title}.</div>;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const CountingComponent = () => {
|
||||||
|
* const [state, setState] = createStore({
|
||||||
|
* counter: 2,
|
||||||
|
* todoList: [
|
||||||
|
* { id: 23, title: 'Birds' },
|
||||||
|
* { id: 27, title: 'Fish' }
|
||||||
|
* ]
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* const add = () => {
|
||||||
|
* setState('todoList', () => {
|
||||||
|
* return [
|
||||||
|
* { id: 23, title: 'Birds' },
|
||||||
|
* { id: 27, title: 'Fish' },
|
||||||
|
* { id: 27, title: 'Cat' }
|
||||||
|
* ];
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const push = () => {
|
||||||
|
* state.todoList.push({
|
||||||
|
* id: 27,
|
||||||
|
* title: 'Pig',
|
||||||
|
* },);
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* return <div>
|
||||||
|
* <div id="todos">
|
||||||
|
* <For each={state.todoList}>
|
||||||
|
* {todo => <><Todo todo={todo} /><Todo todo={todo} /></>}
|
||||||
|
* </For>
|
||||||
|
* </div>
|
||||||
|
* <div><button id="btn" onClick={add}>add</button></div>
|
||||||
|
* <div><button id="btn-push" onClick={push}>push</button></div>
|
||||||
|
* </div>;
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* render(() => <CountingComponent />, document.getElementById("app"));
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 编译后:
|
||||||
|
const _tmpl$ = /*#__PURE__*/_$template(`<div>Count value is <!>.`),
|
||||||
|
_tmpl$2 = /*#__PURE__*/_$template(`<div><div id="todos"></div><div><button id="btn">add</button></div><div><button id="btn-push">push`);
|
||||||
|
const Todo = props => {
|
||||||
|
return (() => {
|
||||||
|
const _el$ = _tmpl$(),
|
||||||
|
_el$2 = _el$.firstChild,
|
||||||
|
_el$4 = _el$2.nextSibling,
|
||||||
|
_el$3 = _el$4.nextSibling;
|
||||||
|
_$insert(_el$, () => props.todo.title, _el$4);
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
const CountingComponent = () => {
|
||||||
|
const state = reactive({
|
||||||
|
counter: 2,
|
||||||
|
todoList: [
|
||||||
|
{
|
||||||
|
id: 23,
|
||||||
|
title: 'Birds',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 27,
|
||||||
|
title: 'Fish',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const add = () => {
|
||||||
|
state.todoList.set(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 23,
|
||||||
|
title: 'Birds',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 27,
|
||||||
|
title: 'Fish',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 27,
|
||||||
|
title: 'Cat',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const push = () => {
|
||||||
|
state.todoList.push({
|
||||||
|
id: 27,
|
||||||
|
title: 'Pig',
|
||||||
|
},);
|
||||||
|
};
|
||||||
|
return (() => {
|
||||||
|
const _el$5 = _tmpl$2(),
|
||||||
|
_el$6 = _el$5.firstChild,
|
||||||
|
_el$7 = _el$6.nextSibling,
|
||||||
|
_el$8 = _el$7.firstChild,
|
||||||
|
_el$9 = _el$7.nextSibling,
|
||||||
|
_el$10 = _el$9.firstChild;
|
||||||
|
_$insert(
|
||||||
|
_el$6,
|
||||||
|
_$createComponent(For, {
|
||||||
|
get each() {
|
||||||
|
return state.todoList;
|
||||||
|
},
|
||||||
|
children: todo => [
|
||||||
|
_$createComponent(Todo, {
|
||||||
|
todo: todo,
|
||||||
|
}),
|
||||||
|
_$createComponent(Todo, {
|
||||||
|
todo: todo,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
_el$8.$$click = add;
|
||||||
|
_el$10.$$click = push;
|
||||||
|
return _el$5;
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
render(() => _$createComponent(CountingComponent, {}), container);
|
||||||
|
_$delegateEvents(['click']);
|
||||||
|
|
||||||
|
expect(container.querySelector('#todos').innerHTML).toEqual(
|
||||||
|
'<div>Count value is Birds<!---->.</div><div>Count value is Birds<!---->.</div><div>Count value is Fish<!---->.</div><div>Count value is Fish<!---->.</div>'
|
||||||
|
);
|
||||||
|
|
||||||
|
container.querySelector('#btn').click();
|
||||||
|
|
||||||
|
expect(container.querySelector('#todos').innerHTML).toEqual(
|
||||||
|
'<div>Count value is Birds<!---->.</div><div>Count value is Birds<!---->.</div><div>Count value is Fish<!---->.</div><div>Count value is Fish<!---->.</div><div>Count value is Cat<!---->.</div><div>Count value is Cat<!---->.</div>'
|
||||||
|
);
|
||||||
|
|
||||||
|
container.querySelector('#btn-push').click();
|
||||||
|
|
||||||
|
expect(container.querySelector('#todos').innerHTML).toEqual(
|
||||||
|
'<div>Count value is Birds<!---->.</div><div>Count value is Birds<!---->.</div><div>Count value is Fish<!---->.</div><div>Count value is Fish<!---->.</div><div>Count value is Cat<!---->.</div><div>Count value is Cat<!---->.</div><div>Count value is Pig<!---->.</div><div>Count value is Pig<!---->.</div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('使用effect, setAttribute, addEventListener', () => {
|
||||||
|
/**
|
||||||
|
* 源码:
|
||||||
|
* const A = ['pretty', 'large', 'big', 'small', 'tall', 'short', 'long', 'handsome', 'plain', 'quaint', 'clean',
|
||||||
|
* 'elegant', 'easy', 'angry', 'crazy', 'helpful', 'mushy', 'odd', 'unsightly', 'adorable', 'important', 'inexpensive',
|
||||||
|
* 'cheap', 'expensive', 'fancy'];
|
||||||
|
*
|
||||||
|
* const random = (max: any) => Math.round(Math.random() * 1000) % max;
|
||||||
|
*
|
||||||
|
* let nextId = 1;
|
||||||
|
*
|
||||||
|
* function buildData(count: number) {
|
||||||
|
* let data = new Array(count);
|
||||||
|
*
|
||||||
|
* for (let i = 0; i < count; i++) {
|
||||||
|
* data[i] = {
|
||||||
|
* id: nextId++,
|
||||||
|
* label: `${A[random(A.length)]}`,
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* return data;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const Row = (props) => {
|
||||||
|
* const selected = createMemo(() => {
|
||||||
|
* return props.item.selected ? 'danger' : '';
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* return (
|
||||||
|
* <tr class={selected()}>
|
||||||
|
* <td class="col-md-1">{props.item.label}</td>
|
||||||
|
* </tr>
|
||||||
|
* )
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* const RowList = (props) => {
|
||||||
|
* return <For each={props.list}>
|
||||||
|
* {(item) => <Row item={item}/>}
|
||||||
|
* </For>;
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* const Button = (props) => (
|
||||||
|
* <div class="col-sm-6">
|
||||||
|
* <button type="button" id={props.id} onClick={props.cb}>{props.title}</button>
|
||||||
|
* </div>
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* const Main = () => {
|
||||||
|
* const [state, setState] = createStore({data: [{id: 1, label: '111', selected: false}, {id: 2, label: '222', selected: false}], num: 2});
|
||||||
|
*
|
||||||
|
* function run() {
|
||||||
|
* setState('data', buildData(5));
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* return (
|
||||||
|
* <div>
|
||||||
|
* <div>
|
||||||
|
* <div>
|
||||||
|
* <div><h1>Horizon-reactive-novnode</h1></div>
|
||||||
|
* <div>
|
||||||
|
* <div>
|
||||||
|
* <Button id="run" title="Create 1,000 rows" cb={run}/>
|
||||||
|
* </div>
|
||||||
|
* </div>
|
||||||
|
* </div>
|
||||||
|
* </div>
|
||||||
|
* <table>
|
||||||
|
* <tbody id="tbody"><RowList list={state.data}/></tbody>
|
||||||
|
* </table>
|
||||||
|
* </div>
|
||||||
|
* );
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* render(() => <Main />, document.getElementById("app"));
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 编译后:
|
||||||
|
const _tmpl$ = /*#__PURE__*/_$template(`<tr><td class="col-md-1">`),
|
||||||
|
_tmpl$2 = /*#__PURE__*/_$template(`<div class="col-sm-6"><button type="button">`),
|
||||||
|
_tmpl$3 = /*#__PURE__*/_$template(`<div><div><div><div><h1>Horizon-reactive-novnode</h1></div><div><div></div></div></div></div><table><tbody id="tbody">`);
|
||||||
|
const A = ['pretty', 'large', 'big', 'small', 'tall', 'short', 'long', 'handsome', 'plain', 'quaint', 'clean', 'elegant', 'easy', 'angry', 'crazy', 'helpful', 'mushy', 'odd', 'unsightly', 'adorable', 'important', 'inexpensive', 'cheap', 'expensive', 'fancy'];
|
||||||
|
const random = max => Math.round(Math.random() * 1000) % max;
|
||||||
|
let nextId = 1;
|
||||||
|
function buildData(count) {
|
||||||
|
let data = new Array(count);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
data[i] = {
|
||||||
|
id: nextId++,
|
||||||
|
label: `${A[random(A.length)]}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
const Row = props => {
|
||||||
|
const selected = computed(() => {
|
||||||
|
return props.item.selected.get() ? "danger" : "";
|
||||||
|
});
|
||||||
|
|
||||||
|
return (() => {
|
||||||
|
const _el$ = _tmpl$(),
|
||||||
|
_el$2 = _el$.firstChild;
|
||||||
|
_$insert(_el$2, () => props.item.label);
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
const RowList = props => {
|
||||||
|
return _$createComponent(For, {
|
||||||
|
get each() {
|
||||||
|
return props.list;
|
||||||
|
},
|
||||||
|
children: item => _$createComponent(Row, {
|
||||||
|
item: item
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const Button = props => (() => {
|
||||||
|
const _el$3 = _tmpl$2(),
|
||||||
|
_el$4 = _el$3.firstChild;
|
||||||
|
_$addEventListener(_el$4, "click", props.cb, true);
|
||||||
|
_$insert(_el$4, () => props.title);
|
||||||
|
watch(() => _$setAttribute(_el$4, "id", props.id));
|
||||||
|
return _el$3;
|
||||||
|
})();
|
||||||
|
const Main = () => {
|
||||||
|
const state = reactive({
|
||||||
|
list: [{
|
||||||
|
id: 1,
|
||||||
|
label: '111'
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
label: '222'
|
||||||
|
}],
|
||||||
|
num: 2
|
||||||
|
});
|
||||||
|
function run() {
|
||||||
|
state.list.set(buildData(5));
|
||||||
|
}
|
||||||
|
return (() => {
|
||||||
|
const _el$5 = _tmpl$3(),
|
||||||
|
_el$6 = _el$5.firstChild,
|
||||||
|
_el$7 = _el$6.firstChild,
|
||||||
|
_el$8 = _el$7.firstChild,
|
||||||
|
_el$9 = _el$8.nextSibling,
|
||||||
|
_el$10 = _el$9.firstChild,
|
||||||
|
_el$11 = _el$6.nextSibling,
|
||||||
|
_el$12 = _el$11.firstChild;
|
||||||
|
_$insert(_el$10, _$createComponent(Button, {
|
||||||
|
id: "run",
|
||||||
|
title: "Create 1,000 rows",
|
||||||
|
cb: run
|
||||||
|
}));
|
||||||
|
_$insert(_el$12, _$createComponent(RowList, {
|
||||||
|
get list() {
|
||||||
|
return state.list;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return _el$5;
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
render(() => _$createComponent(Main, {}), container);
|
||||||
|
_$delegateEvents(["click"]);
|
||||||
|
|
||||||
|
expect(container.querySelector('#tbody').innerHTML).toEqual(
|
||||||
|
'<tr><td class="col-md-1">111</td></tr><tr><td class="col-md-1">222</td></tr>'
|
||||||
|
);
|
||||||
|
|
||||||
|
container.querySelector('#run').click();
|
||||||
|
|
||||||
|
expect(container.querySelector('#tbody').children.length).toEqual(5);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createComputed as computed, createReactive as reactive, createWatch as watch} from './src/RNodeCreator';
|
||||||
|
import { isReactiveObj } from './src/Utils';
|
||||||
|
import { RNode, untrack } from './src/RNode';
|
||||||
|
|
||||||
|
export interface Index {
|
||||||
|
reactive<T>(initialValue: T): RNode<T>;
|
||||||
|
|
||||||
|
watch(fn: () => void): void;
|
||||||
|
|
||||||
|
computed<T>(fn: () => T): RNode<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
reactive,
|
||||||
|
watch,
|
||||||
|
computed,
|
||||||
|
isReactiveObj,
|
||||||
|
untrack
|
||||||
|
}
|
|
@ -2,8 +2,11 @@
|
||||||
"name": "inula-reactive",
|
"name": "inula-reactive",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "reactive core",
|
"description": "reactive core",
|
||||||
"main": "index.js",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest --config=jest.config.js"
|
"test": "jest --config=jest.config.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"inula-reactive": "workspace:^0.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,23 +14,22 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createProxy } from './proxy/RProxyHandler';
|
import { createProxy } from './proxy/RProxyHandler';
|
||||||
import { getRNodeVal, preciseCompare, setRNodeVal } from './RNodeAccessor';
|
import { getRNodeVal, setRNodeVal } from './RNodeAccessor';
|
||||||
|
import { preciseCompare } from './comparison/InDepthComparison';
|
||||||
import { isObject } from './Utils';
|
import { isObject } from './Utils';
|
||||||
|
|
||||||
/** current capture context for identifying @reactive sources (other reactive elements) and cleanups
|
|
||||||
* - active while evaluating a reactive function body */
|
|
||||||
let CurrentReaction: RNode<any> | undefined = undefined;
|
|
||||||
let CurrentGets: RNode<any>[] | null = null;
|
|
||||||
let CurrentGetsIndex = 0;
|
|
||||||
|
|
||||||
/** A list of non-clean 'effect' nodes that will be updated when stabilize() is called */
|
let runningRNode: RNode<any> | undefined = undefined; // 当前正执行的RNode
|
||||||
const EffectQueue: RNode<any>[] = [];
|
let calledGets: RNode<any>[] | null = null;
|
||||||
|
let sameGetsIndex = 0; // 记录前后两次运行RNode时,调用get顺序没有变化的节点
|
||||||
|
|
||||||
export const CacheClean = 0; // reactive value is valid, no need to recompute
|
const Effects: RNode<any>[] = [];
|
||||||
export const CacheCheck = 1; // reactive value might be stale, check parent nodes to decide whether to recompute
|
|
||||||
export const CacheDirty = 2; // reactive value is invalid, parents have changed, valueneeds to be recomputed
|
export const Fresh = 0; // 新数据不用更新
|
||||||
export type CacheState = typeof CacheClean | typeof CacheCheck | typeof CacheDirty;
|
export const Check = 1; // 需要向上遍历检查,可能parents是dirty
|
||||||
type CacheNonClean = typeof CacheCheck | typeof CacheDirty;
|
export const Dirty = 2; // 数据是脏的,需要重复运行fn函数
|
||||||
|
export type State = typeof Fresh | typeof Check | typeof Dirty;
|
||||||
|
type NonClean = typeof Check | typeof Dirty;
|
||||||
|
|
||||||
export interface RNodeOptions {
|
export interface RNodeOptions {
|
||||||
root?: Root<any> | null;
|
root?: Root<any> | null;
|
||||||
|
@ -57,19 +56,20 @@ export class RNode<T = any> {
|
||||||
private _value: T;
|
private _value: T;
|
||||||
private fn?: () => T;
|
private fn?: () => T;
|
||||||
|
|
||||||
|
// 维护数据结构
|
||||||
root: Root<T> | null;
|
root: Root<T> | null;
|
||||||
|
|
||||||
parent: RNode | null = null;
|
parent: RNode | null = null;
|
||||||
key: KEY | null;
|
key: KEY | null;
|
||||||
children: Map<KEY, RNode> | null = null;
|
children: Map<KEY, RNode> | null = null;
|
||||||
|
|
||||||
proxy: any = null;
|
proxy: any = null;
|
||||||
|
|
||||||
|
extend: any; // 用于扩展,放一些自定义属性
|
||||||
|
|
||||||
private observers: RNode[] | null = null; // 被谁用
|
private observers: RNode[] | null = null; // 被谁用
|
||||||
private sources: RNode[] | null = null; // 使用谁
|
private sources: RNode[] | null = null; // 使用谁
|
||||||
|
|
||||||
private state: CacheState;
|
private state: State;
|
||||||
private isSignal = false;
|
|
||||||
private isEffect = false;
|
private isEffect = false;
|
||||||
private isComputed = false;
|
private isComputed = false;
|
||||||
private isProxy = false;
|
private isProxy = false;
|
||||||
|
@ -78,7 +78,6 @@ export class RNode<T = any> {
|
||||||
equals = defaultEquality;
|
equals = defaultEquality;
|
||||||
|
|
||||||
constructor(fnOrValue: (() => T) | T, options?: RNodeOptions) {
|
constructor(fnOrValue: (() => T) | T, options?: RNodeOptions) {
|
||||||
this.isSignal = options?.isSignal || false;
|
|
||||||
this.isEffect = options?.isEffect || false;
|
this.isEffect = options?.isEffect || false;
|
||||||
this.isProxy = options?.isProxy || false;
|
this.isProxy = options?.isProxy || false;
|
||||||
this.isComputed = options?.isComputed || false;
|
this.isComputed = options?.isComputed || false;
|
||||||
|
@ -86,15 +85,15 @@ export class RNode<T = any> {
|
||||||
if (typeof fnOrValue === 'function') {
|
if (typeof fnOrValue === 'function') {
|
||||||
this.fn = fnOrValue as () => T;
|
this.fn = fnOrValue as () => T;
|
||||||
this._value = undefined as any;
|
this._value = undefined as any;
|
||||||
this.state = CacheDirty;
|
this.state = Dirty;
|
||||||
|
|
||||||
if (this.isEffect) {
|
if (this.isEffect) {
|
||||||
EffectQueue.push(this);
|
Effects.push(this);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.fn = undefined;
|
this.fn = undefined;
|
||||||
this._value = fnOrValue;
|
this._value = fnOrValue;
|
||||||
this.state = CacheClean;
|
this.state = Fresh;
|
||||||
}
|
}
|
||||||
|
|
||||||
// large object scene
|
// large object scene
|
||||||
|
@ -120,18 +119,23 @@ export class RNode<T = any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get(): T {
|
get(): T {
|
||||||
if (CurrentReaction) {
|
if (runningRNode) {
|
||||||
if (!CurrentGets && CurrentReaction.sources && CurrentReaction.sources[CurrentGetsIndex] == this) {
|
// 前后两次运行RNode,从左到右对比,如果调用get的RNode相同就calledGetsIndex加1
|
||||||
CurrentGetsIndex++;
|
if (!calledGets && runningRNode.sources && runningRNode.sources[sameGetsIndex] == this) {
|
||||||
|
sameGetsIndex++;
|
||||||
} else {
|
} else {
|
||||||
if (!CurrentGets) {
|
if (!calledGets) {
|
||||||
CurrentGets = [this];
|
calledGets = [this];
|
||||||
} else {
|
} else {
|
||||||
CurrentGets.push(this);
|
calledGets.push(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
read(): T {
|
||||||
if (this.fn) {
|
if (this.fn) {
|
||||||
this.updateIfNecessary();
|
this.updateIfNecessary();
|
||||||
}
|
}
|
||||||
|
@ -139,23 +143,17 @@ export class RNode<T = any> {
|
||||||
return this.getValue();
|
return this.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
set(fnOrValue: T | (() => T)): void {
|
set(fnOrValue: T | ((prev: T) => T)): void {
|
||||||
if (typeof fnOrValue === 'function') {
|
|
||||||
const fn = fnOrValue as () => T;
|
|
||||||
if (fn !== this.fn) {
|
|
||||||
this.stale(CacheDirty);
|
|
||||||
}
|
|
||||||
this.fn = fn;
|
|
||||||
} else {
|
|
||||||
if (this.fn) {
|
if (this.fn) {
|
||||||
this.removeParentObservers(0);
|
this.removeParentObservers(0);
|
||||||
this.sources = null;
|
this.sources = null;
|
||||||
this.fn = undefined;
|
this.fn = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = fnOrValue as T;
|
|
||||||
const prevValue = this.getValue();
|
const prevValue = this.getValue();
|
||||||
|
|
||||||
|
const value = typeof fnOrValue === 'function' ? fnOrValue(prevValue) : fnOrValue;
|
||||||
|
|
||||||
const isObj = isObject(value);
|
const isObj = isObject(value);
|
||||||
const isPrevObj = isObject(prevValue);
|
const isPrevObj = isObject(prevValue);
|
||||||
|
|
||||||
|
@ -163,6 +161,8 @@ export class RNode<T = any> {
|
||||||
if (isObj && isPrevObj) {
|
if (isObj && isPrevObj) {
|
||||||
preciseCompare(this, value, prevValue, false);
|
preciseCompare(this, value, prevValue, false);
|
||||||
|
|
||||||
|
this.setDirty();
|
||||||
|
|
||||||
this.setValue(value);
|
this.setValue(value);
|
||||||
} else {
|
} else {
|
||||||
if (!this.equals(prevValue, value)) {
|
if (!this.equals(prevValue, value)) {
|
||||||
|
@ -171,8 +171,20 @@ export class RNode<T = any> {
|
||||||
this.setValue(value);
|
this.setValue(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 运行EffectQueue
|
||||||
|
runEffects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setByArrayModified(value: T) {
|
||||||
|
const prevValue = this.getValue();
|
||||||
|
|
||||||
|
preciseCompare(this, value, prevValue, true);
|
||||||
|
|
||||||
|
this.setDirty();
|
||||||
|
|
||||||
|
this.setValue(value);
|
||||||
|
|
||||||
// 运行EffectQueue
|
// 运行EffectQueue
|
||||||
runEffects();
|
runEffects();
|
||||||
}
|
}
|
||||||
|
@ -181,22 +193,23 @@ export class RNode<T = any> {
|
||||||
if (this.observers) {
|
if (this.observers) {
|
||||||
for (let i = 0; i < this.observers.length; i++) {
|
for (let i = 0; i < this.observers.length; i++) {
|
||||||
const observer = this.observers[i];
|
const observer = this.observers[i];
|
||||||
observer.stale(CacheDirty);
|
observer.stale(Dirty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private stale(state: CacheNonClean): void {
|
private stale(state: NonClean): void {
|
||||||
if (this.state < state) {
|
if (state > this.state) {
|
||||||
// If we were previously clean, then we know that we may need to update to get the new value
|
if (this.state === Fresh && this.isEffect) {
|
||||||
if (this.state === CacheClean && this.isEffect) {
|
Effects.push(this);
|
||||||
EffectQueue.push(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
|
||||||
|
// 孩子设置为Check
|
||||||
if (this.observers) {
|
if (this.observers) {
|
||||||
for (let i = 0; i < this.observers.length; i++) {
|
for (let i = 0; i < this.observers.length; i++) {
|
||||||
this.observers[i].stale(CacheCheck);
|
this.observers[i].stale(Check);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -205,14 +218,13 @@ export class RNode<T = any> {
|
||||||
private update(): void {
|
private update(): void {
|
||||||
const prevValue = this.getValue();
|
const prevValue = this.getValue();
|
||||||
|
|
||||||
/* Evalute the reactive function body, dynamically capturing any other reactives used */
|
const prevReaction = runningRNode;
|
||||||
const prevReaction = CurrentReaction;
|
const prevGets = calledGets;
|
||||||
const prevGets = CurrentGets;
|
const prevGetsIndex = sameGetsIndex;
|
||||||
const prevIndex = CurrentGetsIndex;
|
|
||||||
|
|
||||||
CurrentReaction = this;
|
runningRNode = this;
|
||||||
CurrentGets = null as any; // prevent TS from thinking CurrentGets is null below
|
calledGets = null as any;
|
||||||
CurrentGetsIndex = 0;
|
sameGetsIndex = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.cleanups.length) {
|
if (this.cleanups.length) {
|
||||||
|
@ -220,27 +232,28 @@ export class RNode<T = any> {
|
||||||
this.cleanups = [];
|
this.cleanups = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 执行 reactive 函数
|
||||||
if (this.isComputed) {
|
if (this.isComputed) {
|
||||||
this.root = { $: this.fn!() };
|
this.root = { $: this.fn!() };
|
||||||
} else {
|
} else {
|
||||||
this._value = this.fn!();
|
this._value = this.fn!();
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the sources have changed, update source & observer links
|
if (calledGets) {
|
||||||
if (CurrentGets) {
|
|
||||||
// remove all old sources' .observers links to us
|
// remove all old sources' .observers links to us
|
||||||
this.removeParentObservers(CurrentGetsIndex);
|
this.removeParentObservers(sameGetsIndex);
|
||||||
|
|
||||||
// update source up links
|
// update source up links
|
||||||
if (this.sources && CurrentGetsIndex > 0) {
|
if (this.sources && sameGetsIndex > 0) {
|
||||||
this.sources.length = CurrentGetsIndex + CurrentGets.length;
|
this.sources.length = sameGetsIndex + calledGets.length;
|
||||||
for (let i = 0; i < CurrentGets.length; i++) {
|
for (let i = 0; i < calledGets.length; i++) {
|
||||||
this.sources[CurrentGetsIndex + i] = CurrentGets[i];
|
this.sources[sameGetsIndex + i] = calledGets[i];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.sources = CurrentGets;
|
this.sources = calledGets;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = CurrentGetsIndex; i < this.sources.length; i++) {
|
for (let i = sameGetsIndex; i < this.sources.length; i++) {
|
||||||
// Add ourselves to the end of the parent .observers array
|
// Add ourselves to the end of the parent .observers array
|
||||||
const source = this.sources[i];
|
const source = this.sources[i];
|
||||||
if (!source.observers) {
|
if (!source.observers) {
|
||||||
|
@ -249,37 +262,40 @@ export class RNode<T = any> {
|
||||||
source.observers.push(this);
|
source.observers.push(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.sources && CurrentGetsIndex < this.sources.length) {
|
} else if (this.sources && sameGetsIndex < this.sources.length) {
|
||||||
// remove all old sources' .observers links to us
|
// remove all old sources' .observers links to us
|
||||||
this.removeParentObservers(CurrentGetsIndex);
|
this.removeParentObservers(sameGetsIndex);
|
||||||
this.sources.length = CurrentGetsIndex;
|
this.sources.length = sameGetsIndex;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
CurrentGets = prevGets;
|
calledGets = prevGets;
|
||||||
CurrentReaction = prevReaction;
|
runningRNode = prevReaction;
|
||||||
CurrentGetsIndex = prevIndex;
|
sameGetsIndex = prevGetsIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handles diamond depenendencies if we're the parent of a diamond.
|
// 处理“钻石”问题
|
||||||
if (!this.equals(prevValue, this.getValue()) && this.observers) {
|
if (!this.equals(prevValue, this.getValue()) && this.observers) {
|
||||||
// We've changed value, so mark our children as dirty so they'll reevaluate
|
// 设置孩子为dirty
|
||||||
for (let i = 0; i < this.observers.length; i++) {
|
for (let i = 0; i < this.observers.length; i++) {
|
||||||
const observer = this.observers[i];
|
const observer = this.observers[i];
|
||||||
observer.state = CacheDirty;
|
observer.state = Dirty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We've rerun with the latest values from all of our sources.
|
this.state = Fresh;
|
||||||
// This means that we no longer need to update until a signal changes
|
|
||||||
this.state = CacheClean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** update() if dirty, or a parent turns out to be dirty. */
|
|
||||||
|
/**
|
||||||
|
* 1、如果this是check,就去找dirty的parent
|
||||||
|
* 2、执行dirty的parent后,会
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private updateIfNecessary(): void {
|
private updateIfNecessary(): void {
|
||||||
if (this.state === CacheCheck) {
|
if (this.state === Check) {
|
||||||
for (const source of this.sources!) {
|
for (const source of this.sources!) {
|
||||||
source.updateIfNecessary(); // updateIfNecessary() can change this.state
|
source.updateIfNecessary(); // updateIfNecessary() can change this.state
|
||||||
if ((this.state as CacheState) === CacheDirty) {
|
if ((this.state as State) === Dirty) {
|
||||||
// Stop the loop here so we won't trigger updates on other parents unnecessarily
|
// Stop the loop here so we won't trigger updates on other parents unnecessarily
|
||||||
// If our computation changes to no longer use some sources, we don't
|
// If our computation changes to no longer use some sources, we don't
|
||||||
// want to update() a source we used last time, but now don't use.
|
// want to update() a source we used last time, but now don't use.
|
||||||
|
@ -288,21 +304,19 @@ export class RNode<T = any> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we were already dirty or marked dirty by the step above, update.
|
if (this.state === Dirty) {
|
||||||
if (this.state === CacheDirty) {
|
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
// By now, we're clean
|
this.state = Fresh;
|
||||||
this.state = CacheClean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeParentObservers(index: number): void {
|
private removeParentObservers(index: number): void {
|
||||||
if (!this.sources) return;
|
if (!this.sources) return;
|
||||||
for (let i = index; i < this.sources.length; i++) {
|
for (let i = index; i < this.sources.length; i++) {
|
||||||
const source: RNode<any> = this.sources[i]; // We don't actually delete sources here because we're replacing the entire array soon
|
const source: RNode<any> = this.sources[i];
|
||||||
const swap = source.observers!.findIndex(v => v === this);
|
const idx = source.observers!.findIndex(v => v === this);
|
||||||
source.observers![swap] = source.observers![source.observers!.length - 1];
|
source.observers![idx] = source.observers![source.observers!.length - 1];
|
||||||
source.observers!.pop();
|
source.observers!.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -314,11 +328,15 @@ export class RNode<T = any> {
|
||||||
private setValue(value: any) {
|
private setValue(value: any) {
|
||||||
this.isProxy ? setRNodeVal(this, value) : (this._value = value);
|
this.isProxy ? setRNodeVal(this, value) : (this._value = value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private qupdate() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onCleanup<T = any>(fn: (oldValue: T) => void): void {
|
export function onCleanup<T = any>(fn: (oldValue: T) => void): void {
|
||||||
if (CurrentReaction) {
|
if (runningRNode) {
|
||||||
CurrentReaction.cleanups.push(fn);
|
runningRNode.cleanups.push(fn);
|
||||||
} else {
|
} else {
|
||||||
console.error('onCleanup must be called from within a @reactive function');
|
console.error('onCleanup must be called from within a @reactive function');
|
||||||
}
|
}
|
||||||
|
@ -326,8 +344,23 @@ export function onCleanup<T = any>(fn: (oldValue: T) => void): void {
|
||||||
|
|
||||||
/** run all non-clean effect nodes */
|
/** run all non-clean effect nodes */
|
||||||
export function runEffects(): void {
|
export function runEffects(): void {
|
||||||
for (let i = 0; i < EffectQueue.length; i++) {
|
for (let i = 0; i < Effects.length; i++) {
|
||||||
EffectQueue[i].get();
|
Effects[i].get();
|
||||||
|
}
|
||||||
|
Effects.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不进行响应式数据的使用追踪
|
||||||
|
export function untrack(fn) {
|
||||||
|
if (runningRNode === null) {
|
||||||
|
return fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
const preRContext = runningRNode;
|
||||||
|
runningRNode = null;
|
||||||
|
try {
|
||||||
|
return fn();
|
||||||
|
} finally {
|
||||||
|
runningRNode = preRContext;
|
||||||
}
|
}
|
||||||
EffectQueue.length = 0;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { RNode } from './RNode';
|
import { RNode } from './RNode';
|
||||||
import { isFunction, isObject} from './Utils';
|
import { isFunction } from './Utils';
|
||||||
import { isArray } from './Utils';
|
|
||||||
import { arrayDiff, DiffOperator, Operation } from './comparison/DiffUtils';
|
|
||||||
import { ArrayState } from './types';
|
|
||||||
import { getOrCreateChildRNode } from './RNodeCreator';
|
|
||||||
|
|
||||||
export function getRNodeVal(node: RNode<any>): any {
|
export function getRNodeVal(node: RNode<any>): any {
|
||||||
let currentNode = node;
|
let currentNode = node;
|
||||||
|
@ -57,213 +54,17 @@ export function setRNodeVal(rNode: RNode<any>, value: unknown): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归触发依赖这reactive数据的所有RContext
|
export function setExtendProp(rNode: RNode, key: string, value: any) {
|
||||||
export function preciseCompare(rNode: RNode<any>, value: any, prevValue: any, isFromArrModify?: boolean) {
|
rNode.extend = rNode.extend || {};
|
||||||
preciseCompareChildren(rNode, value, prevValue, isFromArrModify);
|
rNode.extend[key] = value;
|
||||||
|
|
||||||
// 触发父数据的RContext,不希望触发组件刷新(只触发computed和watch)
|
|
||||||
// TODO 暂时删除
|
|
||||||
// triggerParents(reactive.parent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当value和prevValue都是对象或数组时,才触发
|
export function getExtendProp(rNode: RNode, key: string, defaultValue: any) {
|
||||||
function preciseCompareChildren(rNode: RNode, value: any, prevValue: any, isFromArrModify?: boolean): boolean {
|
rNode.extend = rNode.extend || {};
|
||||||
// 可以精准更新
|
if (key in rNode.extend) {
|
||||||
let canPreciseUpdate = true;
|
return rNode.extend[key];
|
||||||
|
|
||||||
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) {
|
|
||||||
// const idx = String(opt.index);
|
|
||||||
// switch (opt.action) {
|
|
||||||
// // 从已有RNode中取值
|
|
||||||
// case Operation.Nop: {
|
|
||||||
// const childRNode = rNode.children?.get(idx);
|
|
||||||
//
|
|
||||||
// // children没有使用时,可以为undefined或没有该child
|
|
||||||
// if (childRNode !== undefined) {
|
|
||||||
// childRNode.key = String(childIndex);
|
|
||||||
// states.push(ArrayState.Fresh);
|
|
||||||
// childIndex++;
|
|
||||||
//
|
|
||||||
// // 删除旧的,重设新值。处理场景:元素还在,但是在数组中的位置变化了。
|
|
||||||
// rNode.children?.delete(String(opt.index));
|
|
||||||
// rNode.children?.set(childRNode.key, childRNode);
|
|
||||||
// }
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// // 从Value中新建RNode
|
|
||||||
// case Operation.Insert: {
|
|
||||||
// getOrCreateChildRNode(rNode, idx);
|
|
||||||
// states.push(ArrayState.NotFresh);
|
|
||||||
// childIndex++;
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case Operation.Delete: {
|
|
||||||
// rNode.children?.delete(idx);
|
|
||||||
// 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);
|
|
||||||
getOrCreateChildRNode(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 = preciseCompareChildren(childRNode as RNode, val, prevVal);
|
|
||||||
}
|
|
||||||
} else if (!isObj && !isPrevObj) {
|
|
||||||
// val和prevVal都不是对象或数组
|
|
||||||
canPreciseUpdate = true;
|
|
||||||
} else {
|
} else {
|
||||||
// 类型不同(一个是对象或数组,另外一个不是)
|
rNode.extend[key] = defaultValue;
|
||||||
canPreciseUpdate = false;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 有childRNode,说明这个数据被使引用过
|
|
||||||
if (childRNode) {
|
|
||||||
childRNode.setDirty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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] = preciseCompareChildren(childRNode, val, prevVal);
|
|
||||||
}
|
|
||||||
} else if (!isObj && !isPrevObj) {
|
|
||||||
// val和prevVal都不是对象或数组
|
|
||||||
canPreciseUpdates[i] = true;
|
|
||||||
} else {
|
|
||||||
// 类型不同(一个是对象或数组,另外一个不是)
|
|
||||||
canPreciseUpdates[i] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 有childRNode,说明这个数据被使引用过
|
|
||||||
if (childRNode) {
|
|
||||||
childRNode.setDirty();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
canPreciseUpdates[i] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return canPreciseUpdates;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
import { isPrimitive } from './Utils';
|
import { isPrimitive } from './Utils';
|
||||||
import { RNode } from './RNode';
|
import { RNode } from './RNode';
|
||||||
|
import { ProxyRNode } from './Types';
|
||||||
|
|
||||||
export type Reactive<T = any> = RNode<T> | Atom<T>;
|
export type Reactive<T = any> = RNode<T> | Atom<T>;
|
||||||
|
|
||||||
|
@ -43,11 +44,7 @@ export function createWatch<T>(fn: T) {
|
||||||
rNode.get();
|
rNode.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOrCreateChildProxy(
|
export function getOrCreateChildProxy(value: unknown, parent: RNode<any>, key: string | symbol): ProxyRNode<any> {
|
||||||
value: unknown,
|
|
||||||
parent: RNode<any>,
|
|
||||||
key: string | symbol
|
|
||||||
): Atom | ProxyRNode<any> {
|
|
||||||
const child = getOrCreateChildRNode(parent, key);
|
const child = getOrCreateChildRNode(parent, key);
|
||||||
|
|
||||||
return child.proxy;
|
return child.proxy;
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { createComputed, createReactive, createWatch } from './RNodeCreator';
|
|
||||||
import { RNode } from './RNode';
|
|
||||||
|
|
||||||
export interface Reactive {
|
|
||||||
reactive<T>(initialValue: T): RNode<T>;
|
|
||||||
|
|
||||||
watch(fn: () => void): void;
|
|
||||||
|
|
||||||
computed<T>(fn: () => T): RNode<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const reactive: Reactive = {
|
|
||||||
reactive: createReactive,
|
|
||||||
watch: createWatch,
|
|
||||||
computed: createComputed,
|
|
||||||
};
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
import { RNode } from './RNode';
|
import { RNode } from './RNode';
|
||||||
|
|
||||||
export function isReactively(obj: any) {
|
export function isReactiveObj(obj: any) {
|
||||||
return obj instanceof RNode;
|
return obj instanceof RNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,6 @@ export enum Operation {
|
||||||
Nop = 0,
|
Nop = 0,
|
||||||
Insert = 1,
|
Insert = 1,
|
||||||
Delete = 2,
|
Delete = 2,
|
||||||
// 数组长度相同
|
|
||||||
Update = 3,
|
|
||||||
Exchange = 4,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Diff {
|
export interface Diff {
|
||||||
|
@ -68,7 +65,7 @@ function isArrayEqual<T>(str1: T[], str2: T[], start: number, end: number): bool
|
||||||
* @param target 目标数组
|
* @param target 目标数组
|
||||||
* @returns 返回一个 Diff 数组,表示从原始数组转换为目标数组需要进行的操作
|
* @returns 返回一个 Diff 数组,表示从原始数组转换为目标数组需要进行的操作
|
||||||
*/
|
*/
|
||||||
export function arrayDiff<T>(origin: T[], target: T[]): DiffOperator {
|
export function diffArray<T>(origin: T[], target: T[]): DiffOperator {
|
||||||
// 使用二分查找计算共同前缀与后缀
|
// 使用二分查找计算共同前缀与后缀
|
||||||
const prefixLen = longestCommonPrefix(origin, target);
|
const prefixLen = longestCommonPrefix(origin, target);
|
||||||
const suffixLen = longestCommonPrefix([...origin].reverse(), [...target].reverse());
|
const suffixLen = longestCommonPrefix([...origin].reverse(), [...target].reverse());
|
||||||
|
@ -178,4 +175,3 @@ export function arrayDiff<T>(origin: T[], target: T[]): DiffOperator {
|
||||||
opts: diffs,
|
opts: diffs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,233 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {isArray, isObject} from '../Utils';
|
||||||
|
import { diffArray, DiffOperator, Operation } from './ArrayDiff';
|
||||||
|
import { ArrayState } from '../Types';
|
||||||
|
import { getOrCreateChildRNode } from '../RNodeCreator';
|
||||||
|
import { RNode } from '../RNode';
|
||||||
|
import {getExtendProp, setExtendProp} from "../RNodeAccessor";
|
||||||
|
|
||||||
|
// 递归触发依赖这reactive数据的所有RContext
|
||||||
|
export function preciseCompare(rNode: RNode<any>, value: any, prevValue: any, isFromArrModify?: boolean) {
|
||||||
|
preciseCompareChildren(rNode, value, prevValue, isFromArrModify);
|
||||||
|
|
||||||
|
// 触发父数据的RContext,不希望触发组件刷新(只触发computed和watch)
|
||||||
|
// TODO 暂时删除
|
||||||
|
// triggerParents(reactive.parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当value和prevValue都是对象或数组时,才触发
|
||||||
|
function preciseCompareChildren(rNode: RNode, value: any, prevValue: any, isFromArrModify?: boolean): boolean {
|
||||||
|
// 可以精准更新
|
||||||
|
let canPreciseUpdate = true;
|
||||||
|
|
||||||
|
const isArr = isArray(value);
|
||||||
|
const isPrevArr = isArray(prevValue);
|
||||||
|
|
||||||
|
// 1、变化来自数组的Modify方法(某些行可能完全不变)
|
||||||
|
if (isFromArrModify) {
|
||||||
|
// 获取数组间差异,operator只能增删不能修改,修改会导致Effect不会随数据的位置变化
|
||||||
|
const diffOperator = diffArray(prevValue, value);
|
||||||
|
const states: ArrayState[] = [];
|
||||||
|
|
||||||
|
let childIndex = 0;
|
||||||
|
|
||||||
|
for (const opt of diffOperator.opts) {
|
||||||
|
const idx = String(opt.index);
|
||||||
|
switch (opt.action) {
|
||||||
|
// 从已有RNode中取值
|
||||||
|
case Operation.Nop: {
|
||||||
|
const childRNode = rNode.children?.get(idx);
|
||||||
|
|
||||||
|
// children没有使用时,可以为undefined或没有该child
|
||||||
|
if (childRNode !== undefined) {
|
||||||
|
childRNode.key = String(childIndex);
|
||||||
|
states.push(ArrayState.Fresh);
|
||||||
|
childIndex++;
|
||||||
|
|
||||||
|
// 删除旧的,重设新值。处理场景:元素还在,但是在数组中的位置变化了。
|
||||||
|
rNode.children?.delete(String(opt.index));
|
||||||
|
rNode.children?.set(childRNode.key, childRNode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 从Value中新建RNode
|
||||||
|
case Operation.Insert: {
|
||||||
|
getOrCreateChildRNode(rNode, idx);
|
||||||
|
states.push(ArrayState.NotFresh);
|
||||||
|
childIndex++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Operation.Delete: {
|
||||||
|
rNode.children?.delete(idx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffOperators = getExtendProp(rNode, 'diffOperators', []);
|
||||||
|
|
||||||
|
diffOperators.push(diffOperator);
|
||||||
|
|
||||||
|
// 记录:新数据,哪些需要处理,哪些不需要
|
||||||
|
setExtendProp(rNode, 'states', states);
|
||||||
|
|
||||||
|
// 数组长度不同,确定会产生变化,调用callDependents一次
|
||||||
|
// callRContexts(rNode);
|
||||||
|
rNode.setDirty();
|
||||||
|
|
||||||
|
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);
|
||||||
|
getOrCreateChildRNode(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 = preciseCompareChildren(childRNode as RNode, val, prevVal);
|
||||||
|
}
|
||||||
|
} else if (!isObj && !isPrevObj) {
|
||||||
|
// val和prevVal都不是对象或数组
|
||||||
|
canPreciseUpdate = true;
|
||||||
|
} else {
|
||||||
|
// 类型不同(一个是对象或数组,另外一个不是)
|
||||||
|
canPreciseUpdate = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有childRNode,说明这个数据被使引用过
|
||||||
|
if (childRNode) {
|
||||||
|
childRNode.setDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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] = preciseCompareChildren(childRNode, val, prevVal);
|
||||||
|
}
|
||||||
|
} else if (!isObj && !isPrevObj) {
|
||||||
|
// val和prevVal都不是对象或数组
|
||||||
|
canPreciseUpdates[i] = true;
|
||||||
|
} else {
|
||||||
|
// 类型不同(一个是对象或数组,另外一个不是)
|
||||||
|
canPreciseUpdates[i] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有childRNode,说明这个数据被引用过
|
||||||
|
if (childRNode) {
|
||||||
|
childRNode.setDirty();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
canPreciseUpdates[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return canPreciseUpdates;
|
||||||
|
}
|
|
@ -101,7 +101,7 @@ function get(rNode: RNode, key: string | symbol): any {
|
||||||
const value = rawObj.slice();
|
const value = rawObj.slice();
|
||||||
const ret = value[key](...args);
|
const ret = value[key](...args);
|
||||||
// 调用了数组的修改方法,默认值有变化
|
// 调用了数组的修改方法,默认值有变化
|
||||||
// setRNodeVal(rNode, value, true, true);
|
rNode.setByArrayModified(value);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
@ -137,7 +137,7 @@ function getFn(node: RNode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function readFn(node: RNode) {
|
function readFn(node: RNode) {
|
||||||
return getRNodeVal(node);
|
return node.read();
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete()调用的处理
|
// delete()调用的处理
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
import {reactive as r} from '../src/Reactive';
|
import { reactive, computed, watch } from '../index';
|
||||||
|
|
||||||
describe('test reactive', () => {
|
describe('test reactive', () => {
|
||||||
it('two signals, one computed', () => {
|
it('two signals, one computed', () => {
|
||||||
const a = r.reactive(7);
|
const a = reactive(7);
|
||||||
const b = r.reactive(1);
|
const b = reactive(1);
|
||||||
let callCount = 0;
|
let callCount = 0;
|
||||||
|
|
||||||
const c = r.computed(() => {
|
const c = computed(() => {
|
||||||
callCount++;
|
callCount++;
|
||||||
return a.get() * b.get();
|
return a.get() * b.get();
|
||||||
});
|
});
|
||||||
|
|
||||||
r.watch(() => {
|
watch(() => {
|
||||||
console.log(a.get());
|
console.log(a.get());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(a.read()).toBe(7);
|
||||||
|
|
||||||
a.set(2);
|
a.set(2);
|
||||||
expect(c.get()).toBe(2);
|
expect(c.read()).toBe(2);
|
||||||
|
|
||||||
b.set(3);
|
b.set(3);
|
||||||
expect(c.get()).toBe(6);
|
expect(c.get()).toBe(6);
|
||||||
|
@ -27,13 +29,13 @@ describe('test reactive', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reactive is a obj', () => {
|
it('reactive is a obj', () => {
|
||||||
const rObj = r.reactive({count: 1});
|
const rObj = reactive({ count: 1 });
|
||||||
|
|
||||||
const double = r.computed(() => {
|
const double = computed(() => {
|
||||||
return 2 * rObj.count.get();
|
return 2 * rObj.count.get();
|
||||||
});
|
});
|
||||||
|
|
||||||
r.watch(() => {
|
watch(() => {
|
||||||
console.log('count: ', rObj.count.get(), 'double: ', double.get());
|
console.log('count: ', rObj.count.get(), 'double: ', double.get());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -46,20 +48,20 @@ describe('test reactive', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reactive is a array', () => {
|
it('reactive is a array', () => {
|
||||||
const rObj = r.reactive({
|
const rObj = reactive({
|
||||||
items: [
|
items: [
|
||||||
{name: 'p1', id: 1},
|
{ name: 'p1', id: 1 },
|
||||||
{name: 'p2', id: 2},
|
{ name: 'p2', id: 2 },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const doubleId = r.computed(() => {
|
const doubleId = computed(() => {
|
||||||
return 2 * rObj.items[0].id.get();
|
return 2 * rObj.items[0].id.get();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(doubleId.get()).toBe(2);
|
expect(doubleId.get()).toBe(2);
|
||||||
|
|
||||||
rObj.items.set([{name: 'p11', id: 11}]);
|
rObj.items.set([{ name: 'p11', id: 11 }]);
|
||||||
|
|
||||||
expect(doubleId.get()).toBe(22);
|
expect(doubleId.get()).toBe(22);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue