自动处理 ko 邮箱订阅

Automatic disposal of ko postbox subscriptons

我有一个包含 2 个主要部分的单页应用程序:

1.-A top bar that has dynamic content(there is a shopping cart)
2.-A dynamic component that gets loaded based on the url.

有几次组件使用邮箱进行通信,问题是一旦组件本身被处置,在 中创建的订阅就不会 。我知道我可以手动向每个组件添加一个处理函数,然后在内部处理订阅,但是有没有办法对所有组件自动执行此操作?

我确实知道如何循环所有属性并检查它们是否是订阅,但我需要一种方法以某种方式将此行为附加到所有组件,而无需手动将此处置函数附加到所有组件。

我知道邮箱带有一个重置方法,我可以在我的路由库中调用,但我不想那样做,因为那样顶部栏也会失去它的订阅。

为了给你一些视角,主索引页面是这样的:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Participant Dashboard</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="">
    <meta name="author" content="">
    <!-- styles -->
    <link href="../css/bs3/bootstrap.css" rel="stylesheet">
    <link href="../css/bs3/override-bs3.css" rel="stylesheet">
    <script src="../scripts/global/requireConfig.js"></script>
    <script data-main="../consumer/scripts/require-config" src="../scripts/require.js"></script>

</head>
<body>
<top-bar params="routeParams:currentPageArguments"></top-bar>

<div data-bind="component: { name: currentPage, params: currentPageArguments }">
</div>


</body>
</html>

这是我的自定义组件加载器:

    function registerConventionLoader() {
        var koNamingConventionLoader = {
            getConfig: function (name, callback) {
                var widgetName;
                var widgetConfig = common.findComponentConfig(name);
                if (widgetConfig != null) {
                    widgetName = name.substr(widgetConfig.Prefix.length);
                    var widgetNamePascalCase = common.toPascalCase(widgetName);
                    var filePath = widgetConfig.Path;
                    var viewModelConfig = {require: filePath + widgetNamePascalCase};
                    var templateConfig = {require: "text!" + filePath + widgetNamePascalCase + '.html'};

                    callback({viewModel: viewModelConfig, template: templateConfig});
                }
                else {
                    
                    callback(null);
                }
            }
        };
        ko.components.loaders.push(koNamingConventionLoader);
    }

已经实现的行为

邮箱插件向您的可观察对象添加了一个 dispose 方法来处理它创建的任何依赖项。

The dispose function removes all the subscriptions that an observable has on any topic as well as all the subscriptions used to automatically publish changes to the observable.

This function is attached to the observable when publishOn, subscribeTo or syncWith is called.

Source: postbox github docs

如果您的组件的视图模型有 dispose 方法,knockout 将在删除组件时调用它。

Optionally, your viewmodel class may have a dispose function. If implemented, Knockout will call this whenever the component is being torn down and removed from the DOM

Source: component-binding

自定义处理逻辑

了解这两个 library/plugin 行为后,我们可以得出结论,这个总体思路应该可以解决问题:

MyCustomComponent.prototype.dispose = function() {
  /* call `.dispose` on all properties that support it */
};

我们唯一需要编写的代码是注释掉的部分:

  • 遍历视图模型的属性
  • 检查他们是否支持 dispose 方法
  • 如果他们这样做就调用它

归结为:

MyCustomComponent.prototype.dispose = function() {
  var self = this;
  var propNames = Object.keys(this);

  propNames.forEach(function(key) { // Loop over vm's properties
    var val = self[key];

    if (typeof val.dispose === "function") { // Check of dispose implementation
      val.dispose(); // call dispose
    }
  });
};

或者,换一种方式:

MyCustomComponent.prototype.dispose = function() {
  Object
    .keys(this)
    .filter(k => typeof this[k] === "function")
    .forEach(k => this[k]());
};

确保所有组件实现处理逻辑

我强烈建议使用 "regular" 继承或组合模式来确保您的所有组件都实现此功能。

虽然这会强制您编辑所有组件,但它也会明确向其他readers/future您显示实施的行为

如果你真的想玩,你可以覆盖组件引擎的 register 方法以在实例化时将方法添加到 vm 中,但我不推荐它:

var _register = ko.components.register;
ko.components.register = function(name, opts) {
  var ogVM = opts.viewmodel;
  opts.viewmodel = function(params) {
     ogVM.call(this, params);
     this.dispose = function() { /* ... */ }
  }
  return _register(name, opts);
};

找到了! 我必须从 require 获取 viewmodel,然后附加 dispose 函数,最后在回调中传递它: 感谢@user3297291 指导我正确的方向

var koNamingConventionLoader = {
            getConfig: function (name, callback) {
               
                var widgetName;
                var widgetConfig = common.findComponentConfig(name);
                
                if (widgetConfig != null) {
                    widgetName = name.substr(widgetConfig.Prefix.length);
                    var widgetNamePascalCase = common.toPascalCase(widgetName);
                    var filePath = widgetConfig.Path;
                    
                    require([filePath + widgetNamePascalCase], function (mainViewModel) {
                        mainViewModel.prototype.dispose = function () {
                            var self = this;
                            for (var property in self) {
                                if (Boolean(self[property]) && typeof self[property].dispose === "function") {
                                    
                                    self[property].dispose();
                                }
                            }
                        };
                        
                        var templateConfig = {require: "text!" + filePath + widgetNamePascalCase + '.html'};

                        callback({viewModel: mainViewModel, template: templateConfig});
                    });
                }
                else {
                    
                    console.log("widget name not resolved", name);
                    callback(null);
                }
            }
        };
        ko.components.loaders.push(koNamingConventionLoader);
    }