假设我有这样一个"类型":

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

在每个对象 node ,如果所有子对象都被"解析"为一个值,我希望得到通知.因此,例如:

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')
})

每个属性 node 可以有多个"观察者",如下所示:

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

你怎么才能巧妙地安排好呢?我已经try 了很长时间,但没有走远(就像这TS playground个人所看到的那样).

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. 在您添加观察者/监听器之前,该值可能已经解析为in the past.在这种情况下,仍然应该立即通知它.
  2. 在添加观察者之后,该值可以解析为in the future.只有在完成后才应通知它.

注意,"Matcher"语法有点像GraphQL查询,您只需构建一棵对象树,将所需对象的叶子设置为true即可.

推荐答案

以下是一些初步 idea :

  • 根据我的理解,第一个参数a.watch3('b', {c: {d: true}}, cb)的名称必须与其中一个属性匹配,Matcher对象应该"映射"到该属性的值.但是,我建议将Matcher对象映射到current对象(this),并将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)
    

    这一假设意味着,一旦被调用,回调可以/必须是unregistered.

  • 如果在还没有文本的情况下注册了回调,则该对象将被视为not yet个已解析对象--要被解析,(下游)对象 struct 中必须至少有一个文本,并且所有下游文本都必须已接收值(如果提供了Matcher对象,则为其子集)

  • 如果提供的Matcher对象引用的 struct 不是(完全)存在的,则在该 struct completely构建并且相应的文字已经接收值之前,不会调用已注册的回调.因此,我们需要一种"待处理匹配器"属性,只要创建了丢失的属性,就需要判断该属性,以使一个或多个匹配器能够应用于该新属性.

  • 如果Matcher对象的值为true,其中实际的对象 struct 具有更深的嵌套对象 struct ,而不是文字,则该true将被解释为"所有低于此点"必须已接收到值.

  • 如果Matcher对象有一个对象,其中实际对象有一个文本,则该匹配器将永远不会得到解析.

  • 我更新了这个答案,以便只要有可能(当相应的 struct 完成时),匹配器就会变成端点 node 上的标准监视器(没有匹配器),这样就可以使用从文字到根的upstream 更新的计数器来管理所有匹配器.当计数器变为零时,意味着所有必需的项都被解决了.这里的一个重要细节是,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上使用一些测试函数来查看它

Typescript相关问答推荐

无法从应用程序内的库导入组件NX Expo React Native

如何在方法中定义TypeScript返回类型,以根据参数化类型推断变量的存在?

typescribe不能使用值来索引对象类型,该对象类型满足具有该值的另一个类型'

在Typescribe中,extends工作,但当指定派生给super时出错""

如何访问Content UI的DatePicker/中的sx props of year picker?'<>

如何在使用`Next—intl`和`next/link`时导航

泛型函数即使没有提供类型

为什么TypeScript假设文档对象始终可用?

仅针对某些状态代码重试HTTP请求

在TypeScrip的数组中判断模式类型

使用2个泛型类型参数透明地处理函数中的联合类型

路由链接始终返回到

在HighChats列时间序列图中,十字准线未按预期工作

->;Boolean类型的键不能分配给类型Never

API文件夹内的工作员getAuth()帮助器返回:{UserID:空}

我可以使用typeof来强制执行某些字符串吗

是否可以通过映射类型将函数参数约束为预定义类型?

如何在Reaction 18、Reaction-Rout6中的导航栏中获取路由参数

在类型脚本中创建显式不安全的私有类成员访问函数

将类型脚本函数返回类型提取到VSCode中的接口