!115 新建一个inula-reactive包,专门开发响应式能力
Merge pull request !115 from 陈超涛/reactive
This commit is contained in:
commit
fa7915fd0d
|
@ -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,31 @@
|
||||||
|
/*
|
||||||
|
* 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(),
|
||||||
|
|
||||||
|
testEnvironment: 'jest-environment-jsdom-sixteen',
|
||||||
|
|
||||||
|
testMatch: [
|
||||||
|
'<rootDir>/tests/**/*.test.js',
|
||||||
|
'<rootDir>/tests/**/*.test.ts',
|
||||||
|
'<rootDir>/tests/**/*.test.tsx',
|
||||||
|
],
|
||||||
|
|
||||||
|
timers: 'fake',
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "inula-reactive",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "reactive core",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest --config=jest.config.js"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,333 @@
|
||||||
|
/*
|
||||||
|
* 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 { createProxy } from './proxy/RProxyHandler';
|
||||||
|
import { getRNodeVal, preciseCompare, setRNodeVal } from './RNodeAccessor';
|
||||||
|
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>[] = [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
export interface RNodeOptions {
|
||||||
|
root?: Root<any> | null;
|
||||||
|
isSignal?: boolean;
|
||||||
|
isEffect?: boolean;
|
||||||
|
isComputed?: boolean;
|
||||||
|
isProxy?: boolean;
|
||||||
|
parent?: RNode<any> | null;
|
||||||
|
key?: KEY | null;
|
||||||
|
equals?: (a: any, b: any) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Root<T> {
|
||||||
|
$?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KEY = string | symbol;
|
||||||
|
|
||||||
|
function defaultEquality(a: any, b: any) {
|
||||||
|
return a === b;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
private observers: RNode[] | null = null; // 被谁用
|
||||||
|
private sources: RNode[] | null = null; // 使用谁
|
||||||
|
|
||||||
|
private state: CacheState;
|
||||||
|
private isSignal = false;
|
||||||
|
private isEffect = false;
|
||||||
|
private isComputed = false;
|
||||||
|
private isProxy = false;
|
||||||
|
|
||||||
|
cleanups: ((oldValue: T) => void)[] = [];
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (typeof fnOrValue === 'function') {
|
||||||
|
this.fn = fnOrValue as () => T;
|
||||||
|
this._value = undefined as any;
|
||||||
|
this.state = CacheDirty;
|
||||||
|
|
||||||
|
if (this.isEffect) {
|
||||||
|
EffectQueue.push(this);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.fn = undefined;
|
||||||
|
this._value = fnOrValue;
|
||||||
|
this.state = CacheClean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// large object scene
|
||||||
|
if (this.isProxy) {
|
||||||
|
this.proxy = createProxy(this);
|
||||||
|
this.parent = options?.parent || null;
|
||||||
|
this.key = options?.key as KEY;
|
||||||
|
this.root = options?.root || null;
|
||||||
|
|
||||||
|
if (this.parent && !this.parent.children) {
|
||||||
|
this.parent.children = new Map();
|
||||||
|
this.parent.children.set(this.key, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): T {
|
||||||
|
return this.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(v: T) {
|
||||||
|
this.set(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(): T {
|
||||||
|
if (CurrentReaction) {
|
||||||
|
if (!CurrentGets && CurrentReaction.sources && CurrentReaction.sources[CurrentGetsIndex] == this) {
|
||||||
|
CurrentGetsIndex++;
|
||||||
|
} else {
|
||||||
|
if (!CurrentGets) {
|
||||||
|
CurrentGets = [this];
|
||||||
|
} else {
|
||||||
|
CurrentGets.push(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fn) {
|
||||||
|
this.updateIfNecessary();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (this.fn) {
|
||||||
|
this.removeParentObservers(0);
|
||||||
|
this.sources = null;
|
||||||
|
this.fn = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = fnOrValue as T;
|
||||||
|
const prevValue = this.getValue();
|
||||||
|
|
||||||
|
const isObj = isObject(value);
|
||||||
|
const isPrevObj = isObject(prevValue);
|
||||||
|
|
||||||
|
// 新旧数据都是 对象或数组
|
||||||
|
if (isObj && isPrevObj) {
|
||||||
|
preciseCompare(this, value, prevValue, false);
|
||||||
|
|
||||||
|
this.setValue(value);
|
||||||
|
} else {
|
||||||
|
if (!this.equals(prevValue, value)) {
|
||||||
|
this.setDirty();
|
||||||
|
|
||||||
|
this.setValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行EffectQueue
|
||||||
|
runEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
setDirty() {
|
||||||
|
if (this.observers) {
|
||||||
|
for (let i = 0; i < this.observers.length; i++) {
|
||||||
|
const observer = this.observers[i];
|
||||||
|
observer.stale(CacheDirty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = state;
|
||||||
|
if (this.observers) {
|
||||||
|
for (let i = 0; i < this.observers.length; i++) {
|
||||||
|
this.observers[i].stale(CacheCheck);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
CurrentReaction = this;
|
||||||
|
CurrentGets = null as any; // prevent TS from thinking CurrentGets is null below
|
||||||
|
CurrentGetsIndex = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.cleanups.length) {
|
||||||
|
this.cleanups.forEach(c => c(this._value));
|
||||||
|
this.cleanups = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isComputed) {
|
||||||
|
this.root = { $: this.fn!() };
|
||||||
|
} else {
|
||||||
|
this._value = this.fn!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the sources have changed, update source & observer links
|
||||||
|
if (CurrentGets) {
|
||||||
|
// remove all old sources' .observers links to us
|
||||||
|
this.removeParentObservers(CurrentGetsIndex);
|
||||||
|
// 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];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.sources = CurrentGets;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = CurrentGetsIndex; i < this.sources.length; i++) {
|
||||||
|
// Add ourselves to the end of the parent .observers array
|
||||||
|
const source = this.sources[i];
|
||||||
|
if (!source.observers) {
|
||||||
|
source.observers = [this];
|
||||||
|
} else {
|
||||||
|
source.observers.push(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.sources && CurrentGetsIndex < this.sources.length) {
|
||||||
|
// remove all old sources' .observers links to us
|
||||||
|
this.removeParentObservers(CurrentGetsIndex);
|
||||||
|
this.sources.length = CurrentGetsIndex;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
CurrentGets = prevGets;
|
||||||
|
CurrentReaction = prevReaction;
|
||||||
|
CurrentGetsIndex = prevIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
for (let i = 0; i < this.observers.length; i++) {
|
||||||
|
const observer = this.observers[i];
|
||||||
|
observer.state = CacheDirty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** update() if dirty, or a parent turns out to be dirty. */
|
||||||
|
private updateIfNecessary(): void {
|
||||||
|
if (this.state === CacheCheck) {
|
||||||
|
for (const source of this.sources!) {
|
||||||
|
source.updateIfNecessary(); // updateIfNecessary() can change this.state
|
||||||
|
if ((this.state as CacheState) === CacheDirty) {
|
||||||
|
// 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.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we were already dirty or marked dirty by the step above, update.
|
||||||
|
if (this.state === CacheDirty) {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// By now, we're clean
|
||||||
|
this.state = CacheClean;
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
source.observers!.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getValue() {
|
||||||
|
return this.isProxy ? getRNodeVal(this) : this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setValue(value: any) {
|
||||||
|
this.isProxy ? setRNodeVal(this, value) : (this._value = value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onCleanup<T = any>(fn: (oldValue: T) => void): void {
|
||||||
|
if (CurrentReaction) {
|
||||||
|
CurrentReaction.cleanups.push(fn);
|
||||||
|
} else {
|
||||||
|
console.error('onCleanup must be called from within a @reactive function');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** run all non-clean effect nodes */
|
||||||
|
export function runEffects(): void {
|
||||||
|
for (let i = 0; i < EffectQueue.length; i++) {
|
||||||
|
EffectQueue[i].get();
|
||||||
|
}
|
||||||
|
EffectQueue.length = 0;
|
||||||
|
}
|
|
@ -0,0 +1,269 @@
|
||||||
|
/*
|
||||||
|
* 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 { 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';
|
||||||
|
|
||||||
|
export function getRNodeVal(node: RNode<any>): any {
|
||||||
|
let currentNode = node;
|
||||||
|
const keys: (string | symbol)[] = [];
|
||||||
|
while (currentNode.key !== null && currentNode.parent !== null) {
|
||||||
|
keys.push(currentNode.key);
|
||||||
|
currentNode = currentNode.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rawObj = node.root?.$;
|
||||||
|
for (let i = keys.length - 1; i >= 0; i--) {
|
||||||
|
if (keys[i] !== undefined && rawObj) {
|
||||||
|
rawObj = rawObj[keys[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setRNodeVal(rNode: RNode<any>, value: unknown): void {
|
||||||
|
const parent = rNode.parent;
|
||||||
|
const key = rNode.key!;
|
||||||
|
const isRoot = parent === null;
|
||||||
|
let prevValue: unknown;
|
||||||
|
let newValue: unknown;
|
||||||
|
|
||||||
|
if (isRoot) {
|
||||||
|
prevValue = rNode.root!.$;
|
||||||
|
newValue = isFunction<(...prev: any) => any>(value) ? value(prevValue) : value;
|
||||||
|
rNode.root!.$ = newValue;
|
||||||
|
} else {
|
||||||
|
const parentVal = getRNodeVal(parent!);
|
||||||
|
prevValue = parentVal[key];
|
||||||
|
newValue = isFunction<(...prev: any) => any>(value) ? value(prevValue) : value;
|
||||||
|
parentVal[key] = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归触发依赖这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) {
|
||||||
|
// // 获取数组间差异,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 {
|
||||||
|
// 类型不同(一个是对象或数组,另外一个不是)
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* 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 { isPrimitive } from './Utils';
|
||||||
|
import { RNode } from './RNode';
|
||||||
|
|
||||||
|
export type Reactive<T = any> = RNode<T> | Atom<T>;
|
||||||
|
|
||||||
|
export function createReactive<T extends any>(raw?: T): ReactiveProxy<T> {
|
||||||
|
if (isPrimitive(raw) || raw === null || raw === undefined) {
|
||||||
|
return new RNode(raw, { isSignal: true });
|
||||||
|
} else {
|
||||||
|
const node = new RNode(null, {
|
||||||
|
isProxy: true,
|
||||||
|
root: { $: raw },
|
||||||
|
});
|
||||||
|
return node.proxy as ReactiveProxy<T>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createComputed<T>(fn: T) {
|
||||||
|
const rNode = new RNode(fn, { isProxy: true, isComputed: true });
|
||||||
|
return rNode.proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWatch<T>(fn: T) {
|
||||||
|
const rNode = new RNode(fn, {
|
||||||
|
isEffect: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
rNode.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOrCreateChildProxy(
|
||||||
|
value: unknown,
|
||||||
|
parent: RNode<any>,
|
||||||
|
key: string | symbol
|
||||||
|
): Atom | ProxyRNode<any> {
|
||||||
|
const child = getOrCreateChildRNode(parent, key);
|
||||||
|
|
||||||
|
return child.proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOrCreateChildRNode(node: RNode<any>, key: string | symbol): RNode<any> {
|
||||||
|
let child = node.children?.get(key);
|
||||||
|
|
||||||
|
if (!child) {
|
||||||
|
child = new RNode(null, {
|
||||||
|
isProxy: true,
|
||||||
|
parent: node,
|
||||||
|
key: key,
|
||||||
|
root: node.root,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
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,
|
||||||
|
};
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum ArrayState {
|
||||||
|
Fresh = 0,
|
||||||
|
NotFresh = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PrimitiveType = string | number | boolean;
|
||||||
|
|
||||||
|
export type ValueType<T> = { [K in keyof T]: any } | Record<string, any> | PrimitiveType;
|
||||||
|
|
||||||
|
export interface BaseNodeFns<T> {
|
||||||
|
/**
|
||||||
|
* 返回响应式对象的值,自动追踪依赖
|
||||||
|
*/
|
||||||
|
get(): T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回响应式对象的值,不追踪依赖
|
||||||
|
*/
|
||||||
|
read(): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProxyRNodeFn<T> extends BaseNodeFns<T> {
|
||||||
|
set<V = ValueType<T>>(value: V | ((prev: T) => V));
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AtomNodeFn<T> extends BaseNodeFns<T> {
|
||||||
|
set<V extends PrimitiveType>(value: V | ((prev: T) => V));
|
||||||
|
}
|
||||||
|
|
||||||
|
type PropsRecursive<T, K extends keyof T, RecurseType> = T[K] extends PrimitiveType
|
||||||
|
? AtomNode<T[K]>
|
||||||
|
: T[K] extends any[]
|
||||||
|
? any[] & ProxyRNodeFn<T[K]>
|
||||||
|
: T extends Record<string, any>
|
||||||
|
? RecurseType
|
||||||
|
: T[K];
|
||||||
|
|
||||||
|
export type ProxyRNodeProps<T> = {
|
||||||
|
[K in keyof T]: PropsRecursive<T, K, ProxyRNode<T[K]>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ComputedProps<T> = {
|
||||||
|
[K in keyof T]: PropsRecursive<T, K, BaseNodeFns<T[K]>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProxyRNode<T> = ProxyRNodeFn<T> & ProxyRNodeProps<T>;
|
||||||
|
|
||||||
|
export type AtomNode<T> = AtomNodeFn<T>;
|
||||||
|
|
||||||
|
export type Computed<T> = BaseNodeFns<T> & ComputedProps<T>;
|
||||||
|
|
||||||
|
export type ReactiveProxy<T> = T extends PrimitiveType ? AtomNode<T> : ProxyRNode<T>;
|
||||||
|
|
||||||
|
export interface BaseRNode<T> {
|
||||||
|
// 标识Node类型 atomSymbol,nodeSymbol,computedSymbol
|
||||||
|
type: symbol;
|
||||||
|
root: Root<T>;
|
||||||
|
children?: Map<string | symbol, RNode<T> | Atom<T>>;
|
||||||
|
usedRContexts?: RContextSet;
|
||||||
|
proxy?: any;
|
||||||
|
|
||||||
|
diffOperator?: DiffOperator;
|
||||||
|
diffOperators?: DiffOperator[];
|
||||||
|
states?: ArrayState[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RootRNode<T> extends BaseRNode<T> {
|
||||||
|
parentKey: null;
|
||||||
|
parent: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChildrenRNode<T> extends BaseRNode<T> {
|
||||||
|
parentKey: string | symbol;
|
||||||
|
parent: RNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RNode<T = any> = RootRNode<T> | ChildrenRNode<T>;
|
||||||
|
|
||||||
|
export type Reactive<T = any> = RNode<T> | Atom<T>;
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* 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 { RNode } from './RNode';
|
||||||
|
|
||||||
|
export function isReactively(obj: any) {
|
||||||
|
return obj instanceof RNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObject(obj: unknown): boolean {
|
||||||
|
const type = typeof obj;
|
||||||
|
return obj != null && (type === 'object' || type === 'function');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPrimitive(obj: unknown): boolean {
|
||||||
|
const type = typeof obj;
|
||||||
|
return obj != null && type !== 'object' && type !== 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFunction<T extends (...prev: any) => any>(obj: unknown): obj is T {
|
||||||
|
return typeof obj === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isArray(obj: any): boolean {
|
||||||
|
return Object.prototype.toString.call(obj) === '[object Array]';
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum Operation {
|
||||||
|
// 数组长度不同
|
||||||
|
Nop = 0,
|
||||||
|
Insert = 1,
|
||||||
|
Delete = 2,
|
||||||
|
// 数组长度相同
|
||||||
|
Update = 3,
|
||||||
|
Exchange = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Diff {
|
||||||
|
action: Operation;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiffOperator {
|
||||||
|
isOnlyNop: boolean;
|
||||||
|
opts: Diff[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function longestCommonPrefix<T>(arr1: T[], arr2: T[]): number {
|
||||||
|
if (!arr1.length || !arr2.length || arr1[0] !== arr1[0]) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let low = 0;
|
||||||
|
let start = 0;
|
||||||
|
// 最短数组的长度
|
||||||
|
let high = Math.min(arr1.length, arr2.length);
|
||||||
|
while (low <= high) {
|
||||||
|
const mid = (high + low) >> 1;
|
||||||
|
if (isArrayEqual(arr1, arr2, start, mid)) {
|
||||||
|
low = mid + 1;
|
||||||
|
start = mid;
|
||||||
|
} else {
|
||||||
|
high = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return low - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isArrayEqual<T>(str1: T[], str2: T[], start: number, end: number): boolean {
|
||||||
|
for (let j = start; j < end; j++) {
|
||||||
|
if (str1[j] !== str2[j]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param origin 原始数组
|
||||||
|
* @param target 目标数组
|
||||||
|
* @returns 返回一个 Diff 数组,表示从原始数组转换为目标数组需要进行的操作
|
||||||
|
*/
|
||||||
|
export function arrayDiff<T>(origin: T[], target: T[]): DiffOperator {
|
||||||
|
// 使用二分查找计算共同前缀与后缀
|
||||||
|
const prefixLen = longestCommonPrefix(origin, target);
|
||||||
|
const suffixLen = longestCommonPrefix([...origin].reverse(), [...target].reverse());
|
||||||
|
// 删除原数组与目标数组的共同前缀与后缀
|
||||||
|
const optimizedOrigin = origin.slice(prefixLen, origin.length - suffixLen);
|
||||||
|
const optimizedTarget = target.slice(prefixLen, target.length - suffixLen);
|
||||||
|
|
||||||
|
const originLen = optimizedOrigin.length;
|
||||||
|
const targetLen = optimizedTarget.length;
|
||||||
|
|
||||||
|
const dp: number[][] = Array.from(Array(originLen + 1), () => {
|
||||||
|
return Array(targetLen + 1).fill(0);
|
||||||
|
});
|
||||||
|
const pathMatrix: Operation[][] = Array.from(Array(originLen + 1), () => {
|
||||||
|
return Array(targetLen + 1).fill('');
|
||||||
|
});
|
||||||
|
|
||||||
|
let diffs: Diff[] = [];
|
||||||
|
|
||||||
|
// 计算最长公共子序列
|
||||||
|
for (let i = 1; i < originLen + 1; i++) {
|
||||||
|
for (let j = 1; j < targetLen + 1; j++) {
|
||||||
|
if (optimizedOrigin[i - 1] === optimizedTarget[j - 1]) {
|
||||||
|
dp[i][j] = dp[i - 1][j - 1] + 1;
|
||||||
|
// 如果相等,则表示不需要进行任何操作
|
||||||
|
pathMatrix[i][j] = Operation.Nop;
|
||||||
|
} else if (dp[i - 1][j] > dp[i][j - 1]) {
|
||||||
|
dp[i][j] = dp[i - 1][j];
|
||||||
|
// 如果不相等,则需要进行删除操作
|
||||||
|
pathMatrix[i][j] = Operation.Delete;
|
||||||
|
} else {
|
||||||
|
dp[i][j] = dp[i][j - 1];
|
||||||
|
// 如果不相等,则需要进行插入操作
|
||||||
|
pathMatrix[i][j] = Operation.Insert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasDelete = false;
|
||||||
|
let hasInsert = false;
|
||||||
|
|
||||||
|
// 计算操作序列
|
||||||
|
function diff(oLen: number, tLen: number) {
|
||||||
|
const stack: Record<string, number>[] = [{ i: oLen, j: tLen }];
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const obj = stack.pop();
|
||||||
|
const { i, j } = obj!;
|
||||||
|
if (i === 0 || j === 0) {
|
||||||
|
if (i !== 0) {
|
||||||
|
diffs.unshift(
|
||||||
|
...optimizedOrigin.slice(0, i).map((item, idx) => ({
|
||||||
|
action: Operation.Delete,
|
||||||
|
index: idx,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
hasDelete = true;
|
||||||
|
}
|
||||||
|
if (j !== 0) {
|
||||||
|
diffs.unshift(
|
||||||
|
...optimizedTarget.slice(0, j).map((item, idx) => ({
|
||||||
|
action: Operation.Insert,
|
||||||
|
index: idx,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
hasInsert = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathMatrix[i][j] === Operation.Nop) {
|
||||||
|
stack.push({ i: i - 1, j: j - 1 });
|
||||||
|
// 如果不需要进行任何操作,则表示是公共元素,将其添加到 diffs 中
|
||||||
|
diffs.unshift({ action: Operation.Nop, index: i - 1 });
|
||||||
|
} else if (pathMatrix[i][j] === Operation.Delete) {
|
||||||
|
stack.push({ i: i - 1, j });
|
||||||
|
// 如果需要进行删除操作,则将其添加到 diffs 中
|
||||||
|
diffs.unshift({ action: Operation.Delete, index: i - 1 });
|
||||||
|
hasDelete = true;
|
||||||
|
} else if (pathMatrix[i][j] === Operation.Insert) {
|
||||||
|
stack.push({ i, j: j - 1 });
|
||||||
|
// 如果需要进行插入操作,则将其添加到 diffs 中
|
||||||
|
diffs.unshift({ action: Operation.Insert, index: j - 1 });
|
||||||
|
hasInsert = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算操作序列
|
||||||
|
diff(originLen, targetLen);
|
||||||
|
|
||||||
|
diffs.map(i => (i.index += prefixLen));
|
||||||
|
|
||||||
|
const prefixOpts = Array.from(Array(prefixLen), (_, index) => index).map(idx => ({
|
||||||
|
action: Operation.Nop,
|
||||||
|
index: idx,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const suffixOpts = Array.from(Array(suffixLen), (_, index) => index).map(idx => ({
|
||||||
|
action: Operation.Nop,
|
||||||
|
index: origin.length - suffixLen + idx,
|
||||||
|
}));
|
||||||
|
|
||||||
|
diffs = prefixOpts.concat(diffs, suffixOpts);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOnlyNop: !hasDelete && !hasInsert,
|
||||||
|
opts: diffs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
/*
|
||||||
|
* 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 { getOrCreateChildProxy } from '../RNodeCreator';
|
||||||
|
import { getRNodeVal } from '../RNodeAccessor';
|
||||||
|
import { isArray } from '../Utils';
|
||||||
|
import { RNode } from '../RNode';
|
||||||
|
|
||||||
|
const GET = 'get';
|
||||||
|
const SET = 'set';
|
||||||
|
const READ = 'read';
|
||||||
|
const DELETE = 'delete';
|
||||||
|
const ONCHANGE = 'onChange';
|
||||||
|
export const GET_R_NODE = '$$getRNode';
|
||||||
|
const PROTOTYPE = 'prototype';
|
||||||
|
|
||||||
|
// 数组的修改方法
|
||||||
|
const MODIFY_ARR_FNS = new Set<string | symbol>([
|
||||||
|
'push',
|
||||||
|
'pop',
|
||||||
|
'splice',
|
||||||
|
'shift',
|
||||||
|
'unshift',
|
||||||
|
'reverse',
|
||||||
|
'sort',
|
||||||
|
'fill',
|
||||||
|
'from',
|
||||||
|
'copyWithin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 数组的遍历方法
|
||||||
|
const LOOP_ARR_FNS = new Set<string | symbol>(['forEach', 'map', 'every', 'some', 'filter', 'join']);
|
||||||
|
|
||||||
|
export function createProxy<T extends any>(proxyNode: RNode) {
|
||||||
|
return new Proxy(proxyNode, {
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const FNS: Record<typeof GET | typeof READ | typeof DELETE | typeof ONCHANGE, (args: RNode) => any> = {
|
||||||
|
[GET]: getFn,
|
||||||
|
[READ]: readFn,
|
||||||
|
[DELETE]: deleteFn,
|
||||||
|
[ONCHANGE]: onChangeFn,
|
||||||
|
};
|
||||||
|
|
||||||
|
function get(rNode: RNode, key: string | symbol): any {
|
||||||
|
// 处理 get, read, delete, onchange 方法
|
||||||
|
const fn = FNS[key];
|
||||||
|
if (fn) {
|
||||||
|
return () => fn(rNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用set()方法
|
||||||
|
if (key === SET) {
|
||||||
|
return function (val: any) {
|
||||||
|
rNode.set(val);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === GET_R_NODE) {
|
||||||
|
return rNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawObj = getRNodeVal(rNode);
|
||||||
|
|
||||||
|
// const value = rawObj !== undefined ? Reflect.get(rawObj, key) : rawObj;
|
||||||
|
const value = rawObj !== undefined ? rawObj[key] : rawObj;
|
||||||
|
|
||||||
|
// 对于prototype不做代理
|
||||||
|
if (key === PROTOTYPE) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray(rawObj) && key === 'length') {
|
||||||
|
// 标记依赖
|
||||||
|
// trackReactiveData(rNode);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理数组的方法
|
||||||
|
if (typeof value === 'function') {
|
||||||
|
if (isArray(rawObj)) {
|
||||||
|
// 处理数组的修改方法
|
||||||
|
if (MODIFY_ARR_FNS.has(key)) {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
// 调用数组方法的时候,前后是相同的引用,所以需要先浅拷贝数组,并在浅拷贝的数组上进行操作
|
||||||
|
const value = rawObj.slice();
|
||||||
|
const ret = value[key](...args);
|
||||||
|
// 调用了数组的修改方法,默认值有变化
|
||||||
|
// setRNodeVal(rNode, value, true, true);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
} else if (LOOP_ARR_FNS.has(key)) {
|
||||||
|
// 处理数组的遍历方法
|
||||||
|
// 标记被使用了
|
||||||
|
// trackReactiveData(rNode);
|
||||||
|
|
||||||
|
return function (callBackFn: any, thisArg?: any) {
|
||||||
|
function cb(_: any, index: number, array: any[]) {
|
||||||
|
const idx = String(index);
|
||||||
|
const itemProxy = getOrCreateChildProxy(array[idx], rNode, idx);
|
||||||
|
return callBackFn(itemProxy, index, array);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawObj[key](cb, thisArg);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value.bind(rawObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getOrCreateChildProxy(value, rNode, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(proxyNode: any, key: string, value: any, receiver: any): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get()调用的处理
|
||||||
|
function getFn(node: RNode) {
|
||||||
|
return node.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFn(node: RNode) {
|
||||||
|
return getRNodeVal(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete()调用的处理
|
||||||
|
function deleteFn() {}
|
||||||
|
|
||||||
|
// onChange()调用的处理
|
||||||
|
function onChangeFn() {}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import {reactive as r} from '../src/Reactive';
|
||||||
|
|
||||||
|
describe('test reactive', () => {
|
||||||
|
it('two signals, one computed', () => {
|
||||||
|
const a = r.reactive(7);
|
||||||
|
const b = r.reactive(1);
|
||||||
|
let callCount = 0;
|
||||||
|
|
||||||
|
const c = r.computed(() => {
|
||||||
|
callCount++;
|
||||||
|
return a.get() * b.get();
|
||||||
|
});
|
||||||
|
|
||||||
|
r.watch(() => {
|
||||||
|
console.log(a.get());
|
||||||
|
});
|
||||||
|
|
||||||
|
a.set(2);
|
||||||
|
expect(c.get()).toBe(2);
|
||||||
|
|
||||||
|
b.set(3);
|
||||||
|
expect(c.get()).toBe(6);
|
||||||
|
|
||||||
|
expect(callCount).toBe(2);
|
||||||
|
c.read();
|
||||||
|
expect(callCount).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reactive is a obj', () => {
|
||||||
|
const rObj = r.reactive({count: 1});
|
||||||
|
|
||||||
|
const double = r.computed(() => {
|
||||||
|
return 2 * rObj.count.get();
|
||||||
|
});
|
||||||
|
|
||||||
|
r.watch(() => {
|
||||||
|
console.log('count: ', rObj.count.get(), 'double: ', double.get());
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(double.read()).toBe(2);
|
||||||
|
|
||||||
|
rObj.count.set(2);
|
||||||
|
|
||||||
|
expect(rObj.count.read()).toBe(2);
|
||||||
|
expect(double.read()).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reactive is a array', () => {
|
||||||
|
const rObj = r.reactive({
|
||||||
|
items: [
|
||||||
|
{name: 'p1', id: 1},
|
||||||
|
{name: 'p2', id: 2},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const doubleId = r.computed(() => {
|
||||||
|
return 2 * rObj.items[0].id.get();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(doubleId.get()).toBe(2);
|
||||||
|
|
||||||
|
rObj.items.set([{name: 'p11', id: 11}]);
|
||||||
|
|
||||||
|
expect(doubleId.get()).toBe(22);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue