Skip to content

Instantly share code, notes, and snippets.

@alshdavid
Last active September 21, 2020 05:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alshdavid/ed59076064bb975cbd8ff36a50a28784 to your computer and use it in GitHub Desktop.
Save alshdavid/ed59076064bb975cbd8ff36a50a28784 to your computer and use it in GitHub Desktop.
const Reactive={};(()=>{const e="[[ReactiveState]]",t=e=>{e.o.n();for(const r of e.c)t(r)},r=(e,t,r)=>Array.isArray(t)?s(e,t,r):o(e,t,r),s=(e,r,n)=>new Proxy(r,{get:(t,r)=>Array.isArray(t[r])?s(e,t[r],n):"object"==typeof t[r]?o(e,t[r],n):t[r],set:(e,r,s)=>e[r]===s||(e[r]=s,t(n),!0)}),o=(r,n,i)=>{const c={};for(const f in n)if(f!==e&&"toJSON"!==f){if(n[f][e])n[f][e].c.push(i);else if(Array.isArray(n[f]))n[f]=s(r,n[f],i);else if("object"==typeof n[f])n[f]=o(r,n[f],i);else if(i.p.includes(f))continue;c[f]=n[f],Object.defineProperty(n,f,{enumerable:!0,get:()=>c[f],set:e=>c[f]===e||(c[f]=e,t(i),!0)}),i.p.push(f)}return n};Reactive.observe=((t,r,s=[])=>{const o=[];for(const e of s)o.push(e());const n=t[e].o.s(()=>{if(0!==s.length)for(let e=0;e<s.length;e++){const n=s[e]();if(o[e]!==n)return o[e]=n,void r(t)}else r(t)});return()=>n.u()}),Reactive.create=(t=>{if(t[e])return r(t,t,t[e]);const s={o:new class{constructor(){this._s=[]}s(e){return this._s.push(e),{u:()=>this._ss=this._s.filter(t=>t!==e)}}n(e){for(const t of this._s)t(e)}},c:[],p:[]},o=r(t,t,s);return Object.defineProperty(o,e,{enumerable:!1,value:s}),Object.defineProperty(o,"toJSON",{enumerable:!1,value:function(){var t={};for(var r in this)r!==e&&(t[r]=this[r]);return t}}),o})})();
export type Callback<T = any> = (value: T) => void
export interface Subscription {
unsubscribe: () => void
}
export class Subject<T> {
private subscribers: Callback<T>[] = []
subscribe(cb: Callback<T>): Subscription {
this.subscribers.push(cb)
return {
unsubscribe: () => this.subscribers = this.subscribers.filter(v => v !== cb)
}
}
next(value: T) {
for (const cb of this.subscribers) {
cb(value)
}
}
}
export const KEY = '[[ReactiveState]]'
export type State = {
onChange: Subject<void>
children: Array<State>
patched: string[]
}
export const create = <T,>(source: T): T => {
if ((source as any)[KEY]) {
return watch(source, source, (source as any)[KEY])
}
const state: State = {
onChange: new Subject(),
children: [],
patched: []
}
const _source: any = watch(source, source, state)
Object.defineProperty(_source, KEY, {
enumerable: false,
value: state
})
Object.defineProperty(_source, 'toJSON', {
enumerable: false,
value: function () {
var result: any = {};
for (var x in this) {
if (x !== KEY) {
result[x] = this[x];
}
}
return result;
}
})
return _source
}
const notify = (state: State) => {
state.onChange.next()
for (const child of state.children) {
notify(child)
}
}
const watch = (
rootSource: any,
source: any,
node: State,
): any => {
if (Array.isArray(source)) {
return watchArray(rootSource, source, node)
}
return watchObject(rootSource, source, node)
}
const watchArray = (
rootSource: any,
source: any,
node: State,
):any => {
const result = new Proxy<any>(source, {
get(target, prop) {
if (Array.isArray(target[prop])) {
return watchArray(rootSource, target[prop], node)
} else if (typeof target[prop] === 'object') {
return watchObject(rootSource, target[prop], node)
}
return target[prop]
},
set(target, propKey, value) {
if (target[propKey] === value) {
return true;
}
target[propKey] = value;
notify(node)
return true
},
})
return result
}
const watchObject = (
rootSource: any,
source: any,
node: State,
): any => {
const proxy: any = {}
for (const key in source) {
if (key === KEY || key === 'toJSON') {
continue
}
if (source[key][KEY]) {
(source[key][KEY] as State).children.push(node)
} else if (Array.isArray(source[key])) {
source[key] = watchArray(rootSource, source[key], node)
} else if (typeof source[key] === 'object') {
source[key] = watchObject(rootSource, source[key], node)
} else if (node.patched.includes(key)) {
continue
}
proxy[key] = source[key]
Object.defineProperty(source, key, {
enumerable: true,
get: () => {
return proxy[key]
},
set: (value) => {
if (proxy[key] === value) {
return true
}
proxy[key] = value
notify(node)
return true
}
})
node.patched.push(key)
}
return source
}
export type DisposeFn = () => void
export const observe = <T,>(
source: T,
cb: (value: T) => void,
watch: Array<() => any> = []
): DisposeFn => {
const watchCache: any[] = []
for (const cb of watch) {
watchCache.push(cb())
}
const _source: any = source
const state: State = _source[KEY]
const subscription = state.onChange.subscribe(() => {
if (watch.length === 0) {
cb(source)
return
}
for (let i = 0; i < watch.length; i++) {
const update = watch[i]()
if (watchCache[i] !== update) {
watchCache[i] = update
cb(source)
return
}
}
})
return () => subscription.unsubscribe()
}
import Reactive from '@alshdavid/reactive'
class Foo {
bar = 0
}
const foo = Reactive.create(new Foo())
Reactive.observe(foo, console.log)
foo.bar = 1
setTimeout(() => foo.bar = 2, 1000)
setTimeout(() => foo.bar = 3, 2000)
import { useEffect, useMemo, useState } from 'react';
import Reactive from '@alshdavid/reactive'
export function useViewModel<T,>(
ctor: () => T,
watch: Array<() => any> = [],
): T {
const [, forceUpdate] = useState(false);
const vm = useMemo(() => Reactive.create(ctor()), [])
useEffect(() => Reactive.observe(vm, () => forceUpdate(s => !s), watch))
return vm
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment