Skip to content

Commit

Permalink
Merge pull request #8 from star-ll/develop
Browse files Browse the repository at this point in the history
Add Computed decorator and replace Effect.target
  • Loading branch information
star-ll authored Dec 14, 2024
2 parents ee1f14d + 4614cd6 commit 193729c
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 31 deletions.
9 changes: 9 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export class HelloWorld extends DecoElement {
cleanup();
}

@Computed()
get computedValue() {
return this.count + 1;
}
set computedValue(value: number) {
this.count = value - 1;
}

componentWillMount(): void {
console.log(this.hostElement.current);
}
Expand Down Expand Up @@ -95,6 +103,7 @@ pnpm run --filter <package, e.g. core> dev
- [X] @Event and @Listen
- [x] @Ref
- [x] @Watch
- [x] @Computed
- [x] lifecycle
- [x] State Management for redux
- [ ] Hook API
Expand Down
49 changes: 40 additions & 9 deletions packages/core/src/decorators/Component.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { bindEscapePropSet, bindComponentFlag, parseElementAttribute } from '../utils/element';
import { Fragment, jsx, render } from '@decoco/renderer';
import { jsx, render } from '@decoco/renderer';
import { escapePropSet, observe, StatePool } from '../reactive/observe';
import { ComponentEffect, Effect } from '../reactive/effect';
import { Effect, effectStack } from '../reactive/effect';
import { expToPath } from '../utils/share';
import { forbiddenStateAndPropKey } from '../utils/const';
import { callLifecycle, LifecycleCallback, LifeCycleList } from '../runtime/lifecycle';
import { createJob, queueJob } from '../runtime/scheduler';
import { doWatch } from './Watch';
import { DecoPlugin } from '../api/plugin';
import { isObjectAttribute } from '../utils/is';
import { isObjectAttribute, isUndefined } from '../utils/is';
import { warn } from '../utils/error';
import { EventEmitter } from './Event';
import { applyChange, applyDiff, diff } from 'deep-diff';
import { applyDiff } from 'deep-diff';
import clone from 'rfdc';
import { DecoratorMetaKeys } from '../enums/decorators';
import { computed } from './Computed';

export interface DecoWebComponent {
[K: string | symbol]: any;
Expand Down Expand Up @@ -105,7 +107,7 @@ function getCustomElementWrapper(target: any, { tag, style, observedAttributes }
super();
this.attachShadow({ mode: 'open' });

const componentUpdateEffect = new Effect(__updateComponent.bind(this)) as ComponentEffect;
const componentUpdateEffect = new Effect(__updateComponent.bind(this));
function __updateComponent(this: WebComponent) {
if (this.__mounted) {
const updateCallbackResult = callLifecycle(this, LifeCycleList.SHOULD_COMPONENT_UPDATE);
Expand All @@ -115,11 +117,12 @@ function getCustomElementWrapper(target: any, { tag, style, observedAttributes }
}

{
// componentUpdateEffect.effect = this.__updateComponent;
Effect.target = componentUpdateEffect;
Effect.target.targetElement = this;
effectStack.push({
effect: componentUpdateEffect,
stateNode: this,
});
this.domUpdate();
Effect.target = null;
effectStack.pop();
}

if (this.__mounted) {
Expand All @@ -138,6 +141,7 @@ function getCustomElementWrapper(target: any, { tag, style, observedAttributes }
this.initRefs();
this.initState();
this.initProps();
this.initComputed();
this.initWatch();
this.initEventAndListen();
this.initStore();
Expand Down Expand Up @@ -236,6 +240,8 @@ function getCustomElementWrapper(target: any, { tag, style, observedAttributes }
const attr = this.getAttribute(name);
observe(this, name, this.hasAttribute(name) && !isObjectAttribute(attr) ? attr : this[name], {
isProp: true,
deep: true,
autoDeepReactive: true,
});

// prop map to html attribute
Expand All @@ -245,6 +251,31 @@ function getCustomElementWrapper(target: any, { tag, style, observedAttributes }
});
}

initComputed() {
const computedKeys = Reflect.getMetadata(DecoratorMetaKeys.computedKeys, this);
if (!computedKeys) {
return;
}

for (const key of computedKeys.values()) {
const descriptor = Object.getOwnPropertyDescriptor(target.prototype, key);

if (isUndefined(descriptor?.get)) {
warn(`computed property ${String(key)} has no getter`);
continue;
}

const getter = descriptor.get.bind(this);
const setter = descriptor.set?.bind(this);

const computedDescriptor = computed.call(this, key, {
get: getter,
set: setter,
});
Object.defineProperty(this, key, { ...computedDescriptor, get: computedDescriptor.get });
}
}

initWatch() {
const watchers = Reflect.getMetadata('watchers', this);
const statePool = Reflect.getMetadata('statePool', this);
Expand Down
56 changes: 56 additions & 0 deletions packages/core/src/decorators/Computed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Effect, effectStack } from 'src/reactive/effect';
import { DecoratorMetaKeys } from '../enums/decorators';
import { StatePool } from 'src/reactive/observe';

export default function Computed() {
return function computed(target: any, propertyKey: string) {
const computedKeys = Reflect.getMetadata(DecoratorMetaKeys.computedKeys, target) || new Set();
computedKeys.add(propertyKey);
Reflect.defineMetadata(DecoratorMetaKeys.computedKeys, computedKeys, target);
};
}

export function computed(this: any, name: string, descriptor: { get: () => any; set?: (value: any) => any }) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const target = this;
let dirty = true;
let value: any;
const statePool = Reflect.getMetadata('statePool', this) as StatePool;
const getter = descriptor.get;
const setter = descriptor.set;

descriptor.get = function () {
if (effectStack.current && effectStack.current.stateNode === target) {
const effect = effectStack.current.effect;
effect.captureSelf(target, name);
}

const effect = new Effect(() => {
dirty = true;
statePool.notify(target, name);
});

if (dirty) {
effectStack.push({
effect,
stateNode: target,
});
value = getter();
effectStack.pop();

dirty = false;
}

return value;
};

if (setter) {
descriptor.set = function (value: unknown) {
setter.call(target, value);
dirty = true;
statePool.notify(target, name);
};
}

return descriptor;
}
3 changes: 2 additions & 1 deletion packages/core/src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import Component from './Component';
import Prop from './Prop';
import State from './State';
import Computed from './Computed';
import Watch from './Watch';
import Ref, { RefType } from './Ref';
import Store from './Store';
import { Event, Listen, type EventEmitter as EventEmitterType } from './Event';

type EventEmitter = EventEmitterType | undefined;

export { Component, Prop, State, Watch, Ref, Event, Listen, Store, type EventEmitter, type RefType };
export { Component, Prop, State, Computed, Watch, Ref, Event, Listen, Store, type EventEmitter, type RefType };
3 changes: 3 additions & 0 deletions packages/core/src/enums/decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum DecoratorMetaKeys {
computedKeys = 'computedKeys',
}
49 changes: 40 additions & 9 deletions packages/core/src/reactive/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@ import { DecoWebComponent } from '../decorators/Component';
export type EffectOptions = {
value?: any;
oldValue?: any;
cleanup?: Function;
scheduler?: Function;
cleanup?: (...args: unknown[]) => void;
scheduler?: (...args: unknown[]) => void;
};
export type EffectTarget = ((value: unknown, oldValue?: unknown, cleanup?: Function) => any) | Function;

export type ComponentEffect = Effect & { targetElement: DecoWebComponent };
export type EffectTarget =
| ((value: unknown, oldValue?: unknown, cleanup?: (...args: unknown[]) => void) => any)
| Function;

let uid = 1;

export class Effect {
static target: ComponentEffect | null = null;

id = uid++;
effect: (...args: any[]) => unknown;
scheduler?: Function;
cleanup: Set<Function> = new Set();
scheduler?: (...args: unknown[]) => void;
cleanup: Set<(...args: unknown[]) => void> = new Set();

constructor(effect: (...args: any[]) => unknown, options: EffectOptions = {}) {
const { scheduler, cleanup } = options;
Expand Down Expand Up @@ -47,3 +45,36 @@ export class Effect {
}
}
}

export type EffectStackItem = {
effect: Effect;
stateNode: DecoWebComponent;
};

export class EffectStack {
private static instance: EffectStack;
private stack: EffectStackItem[] = [];

private constructor() {}

static getInstance(): EffectStack {
if (!EffectStack.instance) {
EffectStack.instance = new EffectStack();
}
return EffectStack.instance;
}

push(effect: EffectStackItem) {
this.stack.push(effect);
}

pop() {
return this.stack.pop();
}

get current() {
return this.stack[this.stack.length - 1];
}
}

export const effectStack = Object.freeze(EffectStack.getInstance());
13 changes: 6 additions & 7 deletions packages/core/src/reactive/observe.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createJob, queueJob } from '../runtime/scheduler';
import { ObserverOptions } from '../types';
import { isArray, isObject, isPlainObject } from '../utils/is';
import { Effect } from './effect';
import { Effect, effectStack } from './effect';
import { warn } from '../utils/error';

const proxyMap = new WeakMap<object, object>();
Expand All @@ -18,10 +18,9 @@ export function createReactive(targetElement: any, target: unknown, options: Obs

const proxyTarget = new Proxy(target, {
get(target, key, receiver) {
if (Effect.target && Effect.target.targetElement === targetElement) {
const effect = Effect.target;
const targetElement = Effect.target.targetElement;
effect.captureSelf(target, key, targetElement);
if (effectStack.current && effectStack.current.stateNode === targetElement) {
const effect = effectStack.current.effect;
effect.captureSelf(target, key, effectStack.current.stateNode);
}

return Reflect.get(target, key, receiver);
Expand Down Expand Up @@ -83,8 +82,8 @@ export function observe(
return Object.defineProperties(target, {
[name]: {
get() {
if (Effect.target && Effect.target.targetElement === target) {
const effect = Effect.target;
if (effectStack.current && effectStack.current.stateNode === target) {
const effect = effectStack.current.effect;
effect.captureSelf(target, name);
}

Expand Down
50 changes: 50 additions & 0 deletions packages/examples/src/api/computed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Component, DecoElement, Computed, State } from '@decoco/core';

@Component('test-computed')
export class ComputedTest extends DecoElement {
@State()
state = {
value: 0,
};

@State()
list: { id: number; value: string }[] = [];

@Computed()
get computedValue() {
const global = window as any;
global.computeCount = (global.computeCount || 0) + 1; // record computed execute count

const result = this.list.find((i) => i.id === this.state.value)?.value || `value is ${this.state.value}`;
return result;
}
set computedValue(value: string) {
console.log('set computedValue', value);
this.state.value = Number(value);
}

constructor() {
super();

setTimeout(() => {
this.list = new Array(10).fill(0).map((_, index) => ({ id: index, value: 'list index is ' + index }));
}, 1000);
}

render() {
const getComputedValue = () => {
console.log('computedValue=' + this.computedValue);
};
return (
<div>
<div>computedValue: {this.computedValue}</div>
<div>state.value: {this.state.value}</div>
<button onClick={() => this.state.value++}>state.value++</button>
<button onClick={getComputedValue}>getComputedValue</button>
<button onClick={() => (this.computedValue = String(this.state.value + 1))}>
change computedValue
</button>
</div>
);
}
}
4 changes: 4 additions & 0 deletions packages/examples/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const routes: { [name: string]: Route } = {
name: 'test-base-store',
component: () => import('./api/store'),
},
'/api/computed': {
name: 'test-computed',
component: () => import('./api/computed'),
},
'/plugins/auto-inject': {
name: 'test-auto-inject-plugin',
component: () => import('./plugins/rollup-plugin-auto-inject-component/index'),
Expand Down
Loading

0 comments on commit 193729c

Please sign in to comment.