1、新建一个inula-novdom模块

2、修改inula-reactive
This commit is contained in:
HoikanChen 2024-01-19 21:00:15 +08:00
parent fa7915fd0d
commit 1e08541b20
23 changed files with 1792 additions and 359 deletions

View File

@ -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
};

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
module.exports = {
presets: [
[
'@babel/preset-env',
],
[
'@babel/preset-typescript',
]
]
};

View File

@ -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',
};

View File

@ -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"
}
}

View File

View File

@ -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;
};
}

View File

@ -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;
}

View File

@ -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);
// }
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
});

View File

@ -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);
});
});

View File

@ -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
}

View File

@ -2,8 +2,11 @@
"name": "inula-reactive",
"version": "0.0.1",
"description": "reactive core",
"main": "index.js",
"main": "index.ts",
"scripts": {
"test": "jest --config=jest.config.js"
},
"dependencies": {
"inula-reactive": "workspace:^0.0.1"
}
}

View File

@ -14,23 +14,22 @@
*/
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';
/** 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 */
const EffectQueue: RNode<any>[] = [];
let runningRNode: RNode<any> | undefined = undefined; // 当前正执行的RNode
let calledGets: RNode<any>[] | null = null;
let sameGetsIndex = 0; // 记录前后两次运行RNode时调用get顺序没有变化的节点
export const CacheClean = 0; // reactive value is valid, no need to recompute
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 type CacheState = typeof CacheClean | typeof CacheCheck | typeof CacheDirty;
type CacheNonClean = typeof CacheCheck | typeof CacheDirty;
const Effects: RNode<any>[] = [];
export const Fresh = 0; // 新数据不用更新
export const Check = 1; // 需要向上遍历检查可能parents是dirty
export const Dirty = 2; // 数据是脏的需要重复运行fn函数
export type State = typeof Fresh | typeof Check | typeof Dirty;
type NonClean = typeof Check | typeof Dirty;
export interface RNodeOptions {
root?: Root<any> | null;
@ -57,19 +56,20 @@ export class RNode<T = any> {
private _value: T;
private fn?: () => T;
// 维护数据结构
root: Root<T> | null;
parent: RNode | null = null;
key: KEY | null;
children: Map<KEY, RNode> | null = null;
proxy: any = null;
extend: any; // 用于扩展,放一些自定义属性
private observers: RNode[] | null = null; // 被谁用
private sources: RNode[] | null = null; // 使用谁
private state: CacheState;
private isSignal = false;
private state: State;
private isEffect = false;
private isComputed = false;
private isProxy = false;
@ -78,7 +78,6 @@ export class RNode<T = any> {
equals = defaultEquality;
constructor(fnOrValue: (() => T) | T, options?: RNodeOptions) {
this.isSignal = options?.isSignal || false;
this.isEffect = options?.isEffect || false;
this.isProxy = options?.isProxy || false;
this.isComputed = options?.isComputed || false;
@ -86,15 +85,15 @@ export class RNode<T = any> {
if (typeof fnOrValue === 'function') {
this.fn = fnOrValue as () => T;
this._value = undefined as any;
this.state = CacheDirty;
this.state = Dirty;
if (this.isEffect) {
EffectQueue.push(this);
Effects.push(this);
}
} else {
this.fn = undefined;
this._value = fnOrValue;
this.state = CacheClean;
this.state = Fresh;
}
// large object scene
@ -120,18 +119,23 @@ export class RNode<T = any> {
}
get(): T {
if (CurrentReaction) {
if (!CurrentGets && CurrentReaction.sources && CurrentReaction.sources[CurrentGetsIndex] == this) {
CurrentGetsIndex++;
if (runningRNode) {
// 前后两次运行RNode从左到右对比如果调用get的RNode相同就calledGetsIndex加1
if (!calledGets && runningRNode.sources && runningRNode.sources[sameGetsIndex] == this) {
sameGetsIndex++;
} else {
if (!CurrentGets) {
CurrentGets = [this];
if (!calledGets) {
calledGets = [this];
} else {
CurrentGets.push(this);
calledGets.push(this);
}
}
}
return this.read();
}
read(): T {
if (this.fn) {
this.updateIfNecessary();
}
@ -139,23 +143,17 @@ export class RNode<T = any> {
return this.getValue();
}
set(fnOrValue: T | (() => T)): void {
if (typeof fnOrValue === 'function') {
const fn = fnOrValue as () => T;
if (fn !== this.fn) {
this.stale(CacheDirty);
}
this.fn = fn;
} else {
set(fnOrValue: T | ((prev: T) => T)): void {
if (this.fn) {
this.removeParentObservers(0);
this.sources = null;
this.fn = undefined;
}
const value = fnOrValue as T;
const prevValue = this.getValue();
const value = typeof fnOrValue === 'function' ? fnOrValue(prevValue) : fnOrValue;
const isObj = isObject(value);
const isPrevObj = isObject(prevValue);
@ -163,6 +161,8 @@ export class RNode<T = any> {
if (isObj && isPrevObj) {
preciseCompare(this, value, prevValue, false);
this.setDirty();
this.setValue(value);
} else {
if (!this.equals(prevValue, value)) {
@ -171,8 +171,20 @@ export class RNode<T = any> {
this.setValue(value);
}
}
// 运行EffectQueue
runEffects();
}
setByArrayModified(value: T) {
const prevValue = this.getValue();
preciseCompare(this, value, prevValue, true);
this.setDirty();
this.setValue(value);
// 运行EffectQueue
runEffects();
}
@ -181,22 +193,23 @@ export class RNode<T = any> {
if (this.observers) {
for (let i = 0; i < this.observers.length; i++) {
const observer = this.observers[i];
observer.stale(CacheDirty);
observer.stale(Dirty);
}
}
}
private stale(state: CacheNonClean): void {
if (this.state < state) {
// If we were previously clean, then we know that we may need to update to get the new value
if (this.state === CacheClean && this.isEffect) {
EffectQueue.push(this);
private stale(state: NonClean): void {
if (state > this.state) {
if (this.state === Fresh && this.isEffect) {
Effects.push(this);
}
this.state = state;
// 孩子设置为Check
if (this.observers) {
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 {
const prevValue = this.getValue();
/* Evalute the reactive function body, dynamically capturing any other reactives used */
const prevReaction = CurrentReaction;
const prevGets = CurrentGets;
const prevIndex = CurrentGetsIndex;
const prevReaction = runningRNode;
const prevGets = calledGets;
const prevGetsIndex = sameGetsIndex;
CurrentReaction = this;
CurrentGets = null as any; // prevent TS from thinking CurrentGets is null below
CurrentGetsIndex = 0;
runningRNode = this;
calledGets = null as any;
sameGetsIndex = 0;
try {
if (this.cleanups.length) {
@ -220,27 +232,28 @@ export class RNode<T = any> {
this.cleanups = [];
}
// 执行 reactive 函数
if (this.isComputed) {
this.root = { $: this.fn!() };
} else {
this._value = this.fn!();
}
// if the sources have changed, update source & observer links
if (CurrentGets) {
if (calledGets) {
// remove all old sources' .observers links to us
this.removeParentObservers(CurrentGetsIndex);
this.removeParentObservers(sameGetsIndex);
// update source up links
if (this.sources && CurrentGetsIndex > 0) {
this.sources.length = CurrentGetsIndex + CurrentGets.length;
for (let i = 0; i < CurrentGets.length; i++) {
this.sources[CurrentGetsIndex + i] = CurrentGets[i];
if (this.sources && sameGetsIndex > 0) {
this.sources.length = sameGetsIndex + calledGets.length;
for (let i = 0; i < calledGets.length; i++) {
this.sources[sameGetsIndex + i] = calledGets[i];
}
} 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
const source = this.sources[i];
if (!source.observers) {
@ -249,37 +262,40 @@ export class RNode<T = any> {
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
this.removeParentObservers(CurrentGetsIndex);
this.sources.length = CurrentGetsIndex;
this.removeParentObservers(sameGetsIndex);
this.sources.length = sameGetsIndex;
}
} finally {
CurrentGets = prevGets;
CurrentReaction = prevReaction;
CurrentGetsIndex = prevIndex;
calledGets = prevGets;
runningRNode = prevReaction;
sameGetsIndex = prevGetsIndex;
}
// handles diamond depenendencies if we're the parent of a diamond.
// 处理“钻石”问题
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++) {
const observer = this.observers[i];
observer.state = CacheDirty;
observer.state = Dirty;
}
}
// We've rerun with the latest values from all of our sources.
// This means that we no longer need to update until a signal changes
this.state = CacheClean;
this.state = Fresh;
}
/** update() if dirty, or a parent turns out to be dirty. */
/**
* 1this是checkdirty的parent
* 2dirty的parent后
* @private
*/
private updateIfNecessary(): void {
if (this.state === CacheCheck) {
if (this.state === Check) {
for (const source of this.sources!) {
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
// 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.
@ -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 === CacheDirty) {
if (this.state === Dirty) {
this.update();
}
// By now, we're clean
this.state = CacheClean;
this.state = Fresh;
}
private removeParentObservers(index: number): void {
if (!this.sources) return;
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 swap = source.observers!.findIndex(v => v === this);
source.observers![swap] = source.observers![source.observers!.length - 1];
const source: RNode<any> = this.sources[i];
const idx = source.observers!.findIndex(v => v === this);
source.observers![idx] = source.observers![source.observers!.length - 1];
source.observers!.pop();
}
}
@ -314,11 +328,15 @@ export class RNode<T = any> {
private setValue(value: any) {
this.isProxy ? setRNodeVal(this, value) : (this._value = value);
}
private qupdate() {
}
}
export function onCleanup<T = any>(fn: (oldValue: T) => void): void {
if (CurrentReaction) {
CurrentReaction.cleanups.push(fn);
if (runningRNode) {
runningRNode.cleanups.push(fn);
} else {
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 */
export function runEffects(): void {
for (let i = 0; i < EffectQueue.length; i++) {
EffectQueue[i].get();
for (let i = 0; i < Effects.length; i++) {
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;
}

View File

@ -14,11 +14,8 @@
*/
import { RNode } from './RNode';
import { isFunction, isObject} from './Utils';
import { isArray } from './Utils';
import { arrayDiff, DiffOperator, Operation } from './comparison/DiffUtils';
import { ArrayState } from './types';
import { getOrCreateChildRNode } from './RNodeCreator';
import { isFunction } from './Utils';
export function getRNodeVal(node: RNode<any>): any {
let currentNode = node;
@ -57,213 +54,17 @@ export function setRNodeVal(rNode: RNode<any>, value: unknown): void {
}
}
// 递归触发依赖这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);
export function setExtendProp(rNode: RNode, key: string, value: any) {
rNode.extend = rNode.extend || {};
rNode.extend[key] = value;
}
// 当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) {
// // 获取数组间差异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;
export function getExtendProp(rNode: RNode, key: string, defaultValue: any) {
rNode.extend = rNode.extend || {};
if (key in rNode.extend) {
return rNode.extend[key];
} else {
// 类型不同(一个是对象或数组,另外一个不是)
canPreciseUpdate = false;
}
// 有childRNode说明这个数据被使引用过
if (childRNode) {
childRNode.setDirty();
rNode.extend[key] = defaultValue;
return defaultValue;
}
}
});
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;
}

View File

@ -15,6 +15,7 @@
import { isPrimitive } from './Utils';
import { RNode } from './RNode';
import { ProxyRNode } from './Types';
export type Reactive<T = any> = RNode<T> | Atom<T>;
@ -43,11 +44,7 @@ export function createWatch<T>(fn: T) {
rNode.get();
}
export function getOrCreateChildProxy(
value: unknown,
parent: RNode<any>,
key: string | symbol
): Atom | ProxyRNode<any> {
export function getOrCreateChildProxy(value: unknown, parent: RNode<any>, key: string | symbol): ProxyRNode<any> {
const child = getOrCreateChildRNode(parent, key);
return child.proxy;

View File

@ -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,
};

View File

@ -15,7 +15,7 @@
import { RNode } from './RNode';
export function isReactively(obj: any) {
export function isReactiveObj(obj: any) {
return obj instanceof RNode;
}

View File

@ -18,9 +18,6 @@ export enum Operation {
Nop = 0,
Insert = 1,
Delete = 2,
// 数组长度相同
Update = 3,
Exchange = 4,
}
export interface Diff {
@ -68,7 +65,7 @@ function isArrayEqual<T>(str1: T[], str2: T[], start: number, end: number): bool
* @param target
* @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 suffixLen = longestCommonPrefix([...origin].reverse(), [...target].reverse());
@ -178,4 +175,3 @@ export function arrayDiff<T>(origin: T[], target: T[]): DiffOperator {
opts: diffs,
};
}

View File

@ -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;
}

View File

@ -101,7 +101,7 @@ function get(rNode: RNode, key: string | symbol): any {
const value = rawObj.slice();
const ret = value[key](...args);
// 调用了数组的修改方法,默认值有变化
// setRNodeVal(rNode, value, true, true);
rNode.setByArrayModified(value);
return ret;
};
@ -137,7 +137,7 @@ function getFn(node: RNode) {
}
function readFn(node: RNode) {
return getRNodeVal(node);
return node.read();
}
// delete()调用的处理

View File

@ -1,22 +1,24 @@
import {reactive as r} from '../src/Reactive';
import { reactive, computed, watch } from '../index';
describe('test reactive', () => {
it('two signals, one computed', () => {
const a = r.reactive(7);
const b = r.reactive(1);
const a = reactive(7);
const b = reactive(1);
let callCount = 0;
const c = r.computed(() => {
const c = computed(() => {
callCount++;
return a.get() * b.get();
});
r.watch(() => {
watch(() => {
console.log(a.get());
});
expect(a.read()).toBe(7);
a.set(2);
expect(c.get()).toBe(2);
expect(c.read()).toBe(2);
b.set(3);
expect(c.get()).toBe(6);
@ -27,13 +29,13 @@ describe('test reactive', () => {
});
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();
});
r.watch(() => {
watch(() => {
console.log('count: ', rObj.count.get(), 'double: ', double.get());
});
@ -46,14 +48,14 @@ describe('test reactive', () => {
});
it('reactive is a array', () => {
const rObj = r.reactive({
const rObj = reactive({
items: [
{ name: 'p1', id: 1 },
{ name: 'p2', id: 2 },
],
});
const doubleId = r.computed(() => {
const doubleId = computed(() => {
return 2 * rObj.items[0].id.get();
});