请教 TypeScript 中有关循环引用的问题

各位大佬晚上好,我想请教一下,下面这种设计模式是否被允许?会不会存在循环引用的问题?

是这样的,一个 app: App ,持有 config: Config ,config: Config 又持有一个私有变量 prop: boolean,我想在 Config 类的外部能够直接访问并修改这个私有变量 prop ,并且修改之后可以直接调用 app: App 里的方法作出响应。

具体情境:一个应用,带有一些配置信息,当我不通过调用方法的方式而是直接修改配置信息的时候,应用也同样能够作出响应。

class Config {
  private _app: App
  private _prop: boolean = true
  get prop() {
    return this._prop
  }
  set prop(state: boolean) {
    this._prop = state
    this._app.respond()
  }
  constructor(app: App) {
    this._app = app
  }
}

class App {
  config: Config = new Config(this)
  respond() {
    console.log(`the config "prop" has changed to ${this.config.prop}`)
    // ... do something
  }
}

const app = new App()
app.config.prop = false

谢谢各位大佬!heart

typescript
177 views
Comments
登录后评论
Sign In
·

比较疑惑和担心的点在于,app 持有 config ,说明 app 实例有指向 config 实例的引用,然后在构造 config 实例的时候,又将 app 实例传了进去,说明 config 实例又有指向 app 实例的引用。这是不是就是所谓的循环引用?会不会造成内存无法释放?

·

可以这样做没问题,这里 config 的生命周期和 app 一样。

通常需要这样做的时候都会有一种更好的解耦方法,两个类交叉耦合逻辑复杂的时候很难理清,容易出 bug。比如写成订阅模式:

class Config {
  private listeners: ((prop: boolean) => void)[];
  private prop: boolean;

  constructor() {
    this.listeners = [];
    this.prop = true;
  }

  public addListener(listener: (prop: boolean) => void) {
    this.listeners.push(listener);
  }

  public getProp() {
    return this.prop;
  }

  public setProp(prop: boolean) {
    this.prop = prop;
    this.listeners.forEach(listener => listener(prop));
  }

  // remove reference
  public destroy() {
    this.listeners = [];
  }
}

class App {
  public config: Config;

  constructor() {
    this.config = new Config();
    this.config.addListener((prop) => {
        console.log('change to:', prop);
    });
  }
}

const app = new App();
app.config.setProp(false); // change to: false
·

你的目的是配置更改的时候通知 APP 做相应动作(配置监听),但通常配置更改都会触发很多动作,如果写成:

set prop(state: boolean) {
  this._prop = state
  this._app.respond()
}

后面有其他应用也需要监听配置变化,set prop 里面的逻辑就会很乱,最后变成 shit ,写成 listener 能够无限拓展,且不依赖其他功能。

·

用了订阅模式之后总感觉我正在写的是 API 接口…… joy

// 配置信息 app.conf
export class AppConf extends Conf {

  // app 启动时是否自动打开仓库
  #autoOpenVault: boolean;
  #autoOpenVaultListeners: ((autoOpenVault: boolean) => any)[];
  autoOpenVault_listen(callback: (autoOpenVault: boolean) => any): UnlistenFn {
    const id = this.#autoOpenVaultListeners.push(callback) - 1;
    return () => { delete this.#autoOpenVaultListeners[id]; };
  }
  get autoOpenVault() { return this.#autoOpenVault; }
  set autoOpenVault(autoOpenVault: boolean) {
    this.#autoOpenVault = autoOpenVault;
    for (const callback of this.#autoOpenVaultListeners) { if (typeof callback === "function") { callback(autoOpenVault); } }
  }

  // app 启动时用户指定的自动打开的仓库路径
  #alwaysOpenVaultPath: string;
  #alwaysOpenVaultPathListeners: ((alwaysOpenVaultPath: string) => any)[];
  alwaysOpenVaultPath_listen(callback: (alwaysOpenVaultPath: string) => any): UnlistenFn {
    const id = this.#alwaysOpenVaultPathListeners.push(callback) - 1;
    return () => { delete this.#alwaysOpenVaultPathListeners[id]; };
  }
  get alwaysOpenVaultPath() { return this.#alwaysOpenVaultPath; }
  set alwaysOpenVaultPath(alwaysOpenVaultPath: string) {
    this.#alwaysOpenVaultPath = alwaysOpenVaultPath;
    for (const callback of this.#alwaysOpenVaultPathListeners) { if (typeof callback === "function") { callback(alwaysOpenVaultPath); } }
  }

  // app 记录的最后一次打开的仓库路径
  #lastOpenVaultPath: string;
  #lastOpenVaultPathListeners: ((lastOpenVaultPath: string) => any)[];
  lastOpenVaultPath_listen(callback: (lastOpenVaultPath: string) => any): UnlistenFn {
    const id = this.#lastOpenVaultPathListeners.push(callback) - 1;
    return () => { delete this.#lastOpenVaultPathListeners[id]; };
  }
  get lastOpenVaultPath() { return this.#lastOpenVaultPath; }
  set lastOpenVaultPath(lastOpenVaultPath: string) {
    this.#lastOpenVaultPath = lastOpenVaultPath;
    for (const callback of this.#lastOpenVaultPathListeners) { if (typeof callback === "function") { callback(lastOpenVaultPath); } }
  }

  // app 记录的用户所拥有的仓库路径列表
  #ownVaultPaths: string[];
  #ownVaultPathsListeners: ((ownVaultPaths: string[]) => any)[];
  ownVaultPaths_listen(callback: (ownVaultPaths: string[]) => any): UnlistenFn {
    const id = this.#ownVaultPathsListeners.push(callback) - 1;
    return () => { delete this.#ownVaultPathsListeners[id]; };
  }
  get ownVaultPaths() { return this.#ownVaultPaths; }
  set ownVaultPaths(ownVaultPaths: string[]) {
    this.#ownVaultPaths = [];
    for (const item of ownVaultPaths) { if (typeof item === "string") { this.#ownVaultPaths.push(item); } }
    for (const callback of this.#ownVaultPathsListeners) { if (typeof callback === "function") { callback(ownVaultPaths); } }
  }

  // 构造函数
  constructor() {
    super();
    // 设置默认值
    this.#autoOpenVault = false;
    this.#alwaysOpenVaultPath = "";
    this.#lastOpenVaultPath = "";
    this.#ownVaultPaths = [];
    // Listeners
    this.#autoOpenVaultListeners = [];
    this.#alwaysOpenVaultPathListeners = [];
    this.#lastOpenVaultPathListeners = [];
    this.#ownVaultPathsListeners = [];
  }

}
·

改成邮局一劳永逸了……

// 取消订阅函数类型
type UnsubscribeFn = () => void;


// 邮局
export class PostOffice {

  // 登记处(绝对私有)
  #id: number;
  #records: { [eventName: string]: [number, string, Function][] };

  // 构造函数
  constructor() {
    this.#id = 0;
    this.#records = {};
  }

  // 发布
  publish(eventName: string, ...args: any) {
    if (this.#records[eventName] != undefined) {
      for (const record of this.#records[eventName]) {
        record[2].apply(this, args);
      }
    }
  }

  // 订阅
  subscribe(eventName: string, yourName: string, callback: Function): UnsubscribeFn {
    // 先拿号
    const id = this.#id;
    this.#id ++;
    // 排队订阅
    if (this.#records[eventName] == undefined) { this.#records[eventName] = []; }
    this.#records[eventName].push([id, yourName, callback]);
    // 返回一个取消订阅的函数
    return () => { this.#unsubscribe(eventName, id); };
  }

  // 只订阅一次
  once(eventName: string, yourName: string, callback: Function): UnsubscribeFn {
    // 先拿号
    const id = this.#id;
    this.#id ++;
    // 构建一次性回调函数
    const decor = (...args: any) => {
      this.#unsubscribe(eventName, id);
      callback.apply(this, args);
    }
    // 排队订阅
    if (this.#records[eventName] == undefined) { this.#records[eventName] = []; }
    this.#records[eventName].push([id, yourName, decor]);
    // 返回一个取消订阅的函数
    return () => { this.#unsubscribe(eventName, id); };
  }

  // 取消订阅(绝对私有)
  #unsubscribe(eventName: string, id: number) {
    if (this.#records[eventName] != undefined) {
      for (const index in this.#records[eventName]) {
        if (this.#records[eventName][index][0] == id) {
          this.#records[eventName].splice(parseInt(index), 1);
        }
      }
    }
  }

}


// 配置信息 app.conf
export class AppConf extends Conf {

  // app 启动时是否自动打开仓库(公开可读写)
  #autoOpenVault: boolean;
  get autoOpenVault() { return this.#autoOpenVault; }
  set autoOpenVault(autoOpenVault: boolean) {
    this.#autoOpenVault = autoOpenVault;
    this.postOffice.publish("app.conf.autoOpenVault", this.#autoOpenVault);
  }

  // app 启动时用户指定的自动打开的仓库路径(公开可读写)
  #alwaysOpenVaultPath: string;
  get alwaysOpenVaultPath() { return this.#alwaysOpenVaultPath; }
  set alwaysOpenVaultPath(alwaysOpenVaultPath: string) {
    this.#alwaysOpenVaultPath = alwaysOpenVaultPath;
    this.postOffice.publish("app.conf.alwaysOpenVaultPath", this.#alwaysOpenVaultPath);
  }

  // app 记录的最后一次打开的仓库路径(公开可读写)
  #lastOpenVaultPath: string;
  get lastOpenVaultPath() { return this.#lastOpenVaultPath; }
  set lastOpenVaultPath(lastOpenVaultPath: string) {
    this.#lastOpenVaultPath = lastOpenVaultPath;
    this.postOffice.publish("app.conf.lastOpenVaultPath", this.#lastOpenVaultPath);
  }

  // app 记录的用户所拥有的仓库路径列表(公开可读写)
  #ownVaultPaths: string[];
  get ownVaultPaths() { return this.#ownVaultPaths; }
  set ownVaultPaths(ownVaultPaths: string[]) {
    this.#ownVaultPaths = [];
    for (const item of ownVaultPaths) { if (typeof item === "string") { this.#ownVaultPaths.push(item); } }
    this.postOffice.publish("app.conf.ownVaultPaths", this.#ownVaultPaths);
  }

  // 构造函数
  constructor(postOffice: PostOffice) {
    super(postOffice);
    this.#autoOpenVault = false;
    this.#alwaysOpenVaultPath = "";
    this.#lastOpenVaultPath = "";
    this.#ownVaultPaths = [];
  }

}