typescript 如何在这个系统中当嵌套属性子集发生变化时获得回调?

e5njpo68  于 2023-06-24  发布在  TypeScript
关注(0)|答案(1)|浏览(140)

假设我有这样的“类型”:

{
  a: {
    b: {
      c: {
        d: string
        e: boolean
      }
    },
    x: string
    y: number
    z: string
  }
}

在每个对象节点上,如果所有的子节点都被“解析”为一个值,我希望得到通知。例如:

const a = new TreeObject()
a.watch((a) => console.log('a resolved', a))

const b = a.createObject('b')
b.watch((b) => console.log('b resolved', b))

const c = b.createObject('c')
c.watch((c) => console.log('c resolved', c))

const d = c.createLiteral('d')
d.watch((d) => console.log('d resolved', d))

const e = c.createLiteral('e')
e.watch((e) => console.log('e resolved', e))

const x = a.createLiteral('x')
x.watch((x) => console.log('x resolved', x))

const y = a.createLiteral('y')
y.watch((y) => console.log('y resolved', y))

d.set('foo')
// logs:
// d resolved

e.set('bar')
// logs:
// e resolved
// c resolved
// b resolved

y.set('hello')
// logs:
// y resolved

x.set('world')
// logs:
// x resolved
// a resolved

这是基本情况。更复杂的情况,也就是我一直试图解决的问题,是匹配属性的子集,像这样:

// receive 'b' only if b.c.d is resolved.
// '3' for 3 args
a.watch3('b', {
  c: {
    d: true
  }
}, () => {
  console.log('b with b.c.d resolved')
})

每个属性节点可以有多个“观察者”,如下所示:

a.watch3('b', { c: { d: true } }, () => {
  console.log('get b with b.c.d resolved')
})

a.watch3('b', { c: { e: true } }, () => {
  console.log('get b with b.c.e resolved')
})

a.watch2('x', () => {
  console.log('get x when resolved')
})

// now if were were to start from scratch setting properties fresh:
x.set('foo')
// logs:
// get x when resolved

e.set('bar')
// logs:
// get b with b.c.e resolved

你怎么能把这个布置得这么整齐呢?我一直在尝试很长一段时间,以包裹我的头围绕它,但没有走远(如看到在这个TSPlayground。

type Matcher = {
  [key: string]: true | Matcher
}

type Callback = () => void

class TreeObject {
  properties: Record<string, unknown>

  callbacks: Record<string, Array<{ matcher?: Matcher, callback: Callback }>>

  parent?: TreeObject

  resolved: Array<Callback>

  constructor(parent?: TreeObject) {
    this.properties = {}
    this.callbacks = {}
    this.parent = parent
    this.resolved = []
  }

  createObject(name: string) {
    const tree = new TreeObject(this)
    this.properties[name] = tree
    return tree
  }
  
  createLiteral(name: string) {
    const tree = new TreeLiteral(this, () => {
      // somehow start keeping track of decrementing what we have matched so far
      // and when it is fully decremented, trigger the callback up the chain.
    })
    this.properties[name] = tree
    return tree
  }

  watch3(name: string, matcher: Matcher, callback: Callback) {
    const list = this.callbacks[name] ??= []
    list.push({ matcher, callback })
  }

  watch2(name: string, callback: Callback) {
    const list = this.callbacks[name] ??= []
    list.push({ callback })
  }

  watch(callback: Callback) {
    this.resolved.push(callback)
  }
}

class TreeLiteral {
  value: any

  parent: TreeObject

  callback: () => void

  resolved: Array<Callback>

  constructor(parent: TreeObject, callback: () => void) {
    this.value = undefined
    this.parent = parent
    this.callback = callback
    this.resolved = []
  }

  set(value: any) {
    this.value = value
    this.resolved.forEach(resolve => resolve())
    this.callback()
  }

  watch(callback: Callback) {
    this.resolved.push(callback)
  }
}

const a = new TreeObject()
a.watch(() => console.log('a resolved'))

const b = a.createObject('b')
b.watch(() => console.log('b resolved'))

const c = b.createObject('c')
c.watch(() => console.log('c resolved'))

const d = c.createLiteral('d')
d.watch(() => console.log('d resolved'))

const e = c.createLiteral('e')
e.watch(() => console.log('e resolved'))

const x = a.createLiteral('x')
x.watch(() => console.log('x resolved'))

const y = a.createLiteral('y')
y.watch(() => console.log('y resolved'))

d.set('foo')
// logs:
// d resolved

e.set('bar')
// logs:
// e resolved
// c resolved
// b resolved

y.set('hello')
// logs:
// y resolved

x.set('world')
// logs:
// x resolved
// a resolved

如何定义watch3和相关方法来接受它们的“匹配器”和回调,并在匹配器的所有属性都满足时正确调用回调?
它变得棘手,因为你可以在两个方向上工作:
1.在添加监视器/侦听器之前,该值可能已经在过去 * 被解析。在这种情况下,仍然应该立即通知它。
1.在添加了监视器之后,可以在将来 * 解析该值。只有在完成后才应通知。
请注意,“matcher”语法有点像GraphQL查询,在这里,您只需构建一个对象树,叶子设置为true

7kqas0il

7kqas0il1#

一些初步想法:

  • 根据我的理解,a.watch3('b', {c: {d: true}}, cb)的第一个参数是一个必须与其中一个属性匹配的名称,Matcher对象应该“Map”到该属性的值。但是我建议将Matcher对象与 current 对象(this)Map,并将b放入Matcher中,这样你就可以省略第一个参数:
a.watch3({b: {c: {d: true}}}, cb);
  • 我会对所有签名使用一个watch方法,其中回调总是第一个参数,Matcher对象是可选的第二个参数。在我看来,名称论证没有必要(上一点)。
  • 我假设一个回调只能被调用一次。这是一个在以下场景中变得重要的假设:
const a = new TreeObject();
const b = a.createObject('b');
const c = b.createObject('c');
const d = c.createLiteral('d');

a.watch(() => console.log("a resolved"));

d.set('foo'); // This triggers "a resolved"

// This makes a unresolved again
const e = c.createLiteral('e');
// but resolving a again will not trigger the same callback
e.set('bar'); // This does not trigger "a resolved" anymore
// Let's do that again: unresolve a...
const f = c.createLiteral('f');
// But now we add a new callback before the resolution occurs:
a.watch(() => console.log("a resolved AGAIN"));
f.set('baz'); // This triggers "a resolved AGAIN" (only)

这个假设意味着回调一旦被调用就可以/必须被 * 未注册 *。

  • 如果在还没有文字时注册回调,则该对象将被视为 * 尚未 * 解析-要成为解析对象,(下游)对象结构中必须至少有一个文字,并且所有下游文字必须已接收值(或子集,如果提供Matcher对象)
  • 如果提供了一个Matcher对象引用了一个不(完全)存在的结构,那么注册的回调将不会被调用,直到该结构已经 * 完全 * 构建,并且相应的文字已经接收到值。因此,我们需要一种“未决匹配器”属性,每当创建一个缺失的属性时,都需要检查该属性,以使一个或多个匹配器能够应用于该新属性。
  • 如果Matcher对象有一个true值,而实际的对象结构有一个更深层次的嵌套对象结构,而不是一个文字,那么true将被解释为“所有低于此点”必须有接收值。
  • 如果Matcher对象有一个对象,而实际对象有一个文字,则该匹配器将永远不会被解析。
  • 我更新了这个答案,以便匹配器在端点节点上变成标准的观察者(没有匹配器),只要有可能(当相应的结构完成时),这样所有的都可以用计数器来管理,这些计数器从字面量向上更新到根。当计数器变为零时,意味着所有必要的项都已解决。这里一个重要的细节是,匹配器对象将为它的每个端点创建自己的回调,当这些回调被调用时,它将跟踪一个单独的计数器。当该值变为零时,将调用原始回调。

下面是如何编码的:

type Matcher = true | {
    [key: string]: Matcher
};

type Callback = () => void;

type Listener = { callback: Callback, matcher: Matcher };

type TreeNode = TreeObject | TreeLiteral;

abstract class TreeElement  {
    #parent?: TreeObject;
    #unresolvedCount = 0;
    #hasLiterals = false;
    #callbacks: Array<Callback> = [];
    
    constructor(parent?: TreeObject) {
        this.#parent = parent;
    }

    notify(isResolved: boolean) { // bubbles up from a TreeLiteral, when created and when resolved
        if (isResolved) {
            this.#unresolvedCount--;
            if (this.#unresolvedCount == 0) {
                for (const cb of this.#callbacks.splice(0)) {
                    cb();
                }
            }
        } else {
            this.#unresolvedCount++;
            this.#hasLiterals = true;
        }
        this.#parent?.notify(isResolved); // bubble up
    }
    
    watch(callback: Callback) {
        if (this.#hasLiterals && this.#unresolvedCount == 0) {
            callback();
        } else {
            this.#callbacks.push(callback);
        }
    }

}

class TreeObject extends TreeElement {
    #properties: Record<string, TreeNode> = {};
    #pendingMatchers: Record<string, Array<Listener>> = {};

    #attach(name: string, child: TreeNode) {
        this.#properties[name] = child;
        // If this name is used by one or more pending matchers, remove them as pending,
        //   and watch the nested matcher(s) on the newly created child.
        if (this.#pendingMatchers[name]) {
            for (const {callback, matcher} of this.#pendingMatchers[name].splice(0)) {
                child.watch(callback, matcher);
            }
        }
    }

    createObject(name: string) {
        if (this.#properties[name]) throw new Error(`Cannot create ${name}: it is already used`);
        const obj = new TreeObject(this);
        this.#attach(name, obj);
        return obj;
    }

    createLiteral(name: string) {
        if (this.#properties[name]) throw new Error(`Cannot create ${name}: it is already used`);
        const obj = new TreeLiteral(this);
        this.#attach(name, obj);
        return obj;
    }

    watch(callback: Callback, matcher: Matcher=true) {
        if (matcher === true) {
            super.watch(callback);
        } else {
            let counter = Object.keys(matcher).length;
            // Create a new callback that will call the original callback when all toplevel
            //   entries specified by the matcher have been resolved.
            const newCallback = () => {
                counter--;
                if (counter == 0) {
                    callback();
                }
            };
            for (const key of Object.keys(matcher)) {
                if (this.#properties[key]) {
                    this.#properties[key].watch(newCallback, matcher[key]);
                } else { // suspend the watch until the structure is there
                    (this.#pendingMatchers[key] ??= []).push({
                        callback: newCallback,
                        // Copy the matcher so the caller cannot mutate our matcher
                        matcher: JSON.parse(JSON.stringify(matcher[key]))
                    });
                }
            }
        }

    }
}

class TreeLiteral extends TreeElement {
    #literalValue: any;

    constructor(parent?: TreeObject) {
        super(parent);
        this.notify(false); // Notifiy to the ancestors that there is a new literal
    }

    set(value: any) {
        this.#literalValue = value;
        this.notify(true); // Notifiy to the ancestors that this literal resolved
    }

    valueOf() {
        return this.#literalValue;
    }

    watch(callback: Callback, matcher: Matcher=true) {
        if (matcher === true) {
            super.watch(callback);
        } // else, the matcher references an endpoint that will never be created
    }
}

在TS Playground上用一些测试功能看看吧

相关问题