避免同时承诺,最佳实践?

Avoiding simultaneous promise, best practices?

我有一个分析仪表板。仪表板由一个或多个可视化构建而成,在初始呈现仪表板时,我检索呈现可视化所需的最少信息。

之后,我想要 'preload' 其他信息,使我能够以更详细的方式与每个单独的可视化进行交互。此信息存储在模式中。一个或多个可视化可以共享相同的架构。

因此我想加载一次架构,然后 return 缓存的架构以避免多个服务器请求。

我要解决的挑战是避免多个可视化同时请求此信息。当一个可视化正在请求模式并且我们有多个请求相同的模式时,我不想调用多个服务器请求但让它们等待第一个完成然后使用缓存版本。

解决这个问题的最佳方法是什么?我可以做一些非常讨厌的事情,比如跟踪正在进行的 XHR 请求,然后执行 setTimeout(retry,500);在所有后续请求上,直到第一个 XHR 请求完成...但也许有一种方法可以重组承诺,从而在同一模式的后续请求之间创建依赖关系?

如有任何提示,我们将不胜感激。我知道如何以丑陋的方式伪造它,但我想以正确的方式做到这一点。

<script type="text/javascript">
    function SchemaManager() {
        this.schemas = {}; // precached schemas
    }
    SchemaManager.prototype = {
        Load: async function (schemaId) {
            this.schemas[schemaId] = "loading";
            return new Promise((resolve, reject) => {
                Xhr({
                    url: `/api/lightweightfield/${schemaId}`,
                    type: 'json',
                    action: 'GET',
                    callbackOk: function () { resolve(this); },
                    callbackError: function () { reject(); }
                });
            });
        },
        Get: async function (schemaId) {
            if (!this.schemas[schemaId]) {
                let result = await this.Load(schemaId).catch((e) => {
                    new ErrorDialog({ title: 'Failed to get schema', content: schemaId });
                    return null;
                });
                this.schemas[schemaId] = result.response;
            } else {
                console.log('cache hit');
            }
            return this.schemas[schemaId];
        }
    }

    let sm = new SchemaManager();
        
    (async function () {
        var schema = await sm.Get("schemas-1953-A");
        console.log('visualization 1',schema);
    })();
    (async function () {
        var schema = await sm.Get("schemas-1953-A");
        console.log('visualization 2', schema);
    })();
    (async function () {
        var schema = await sm.Get("schemas-1953-A");
        console.log('visualization 3', schema);
    })();
    (async function () {
        var schema = await sm.Get("schemas-1953-A");
        console.log('visualization 4', schema);
    })();
    (async function () {
        var schema = await sm.Get("schemas-1953-A");
        console.log('visualization 5',schema);
    })();

    
</script>

基本上,您想要 序列化 Get 的调用。 (我也可能将 Load 设为私有,以便 Get 是唯一用于从 SchemaManager 获取模式的外部 API。)您可以通过跟踪承诺来做到这一点Get returns(对于给定的模式)并为同一模式下一次调用 Get 等待先前的承诺在执行任何操作之前解决。

您在评论中同意 Load 应该是私有的,因此我已将其移出 SchemaManagers 的 API。

类似的内容(参见 *** 评论):

function SchemaManager() {
    this.schemas = {}; // precached schemas
}
function loadSchema(schemaId) {
    console.log(`Loading ${schemaId}`);
    // *** Probably worth promise-enabling `Xhr` so we don't need a wrapper every time
    return new Promise((resolve, reject) => {
        Xhr({
            url: `/api/lightweightfield/${schemaId}`,
            type: "json",
            action: "GET",
            callbackOk: function() {
                console.log(`Loaded ${schemaId}`);
                resolve(this.response);
            },
            callbackError: (error) => { // *** I assume an error is provided?
                // *** Failed to load, so clear the promise from cache
                reject(error); // *** Pass along the error if provided
            }
        });
    });
}
SchemaManager.prototype = {
    Get: async function (schemaId) {
        let cacheEntry = this.schemas[schemaId];
        if (cacheEntry instanceof Promise) {
            // *** Previous load in progress, wait for it to complete
            try {
                console.log("Waiting for previous load");
                const schema = await cacheEntry;
                console.log("Using previous load result");
                return schema;
            } catch {
                // *** Previous load failed, try again
            }
        } else if (cacheEntry) {
            // *** The cache entry is a schema
            console.log("Cache hit");
            return cacheEntry;
        }
        // *** Need to load the schema
        console.log("Start load");
        cacheEntry = this.schemas[schemaId] = loadSchema(schemaId);
        try {
            const schema = await cacheEntry;
            console.log("Caching result for next time");
            this.schemas[schemaId] = schema;
            return schema;
        } catch {
            // *** Didn't get it
            this.schemas[schemaId] = null;
            throw error;
        }
    }
}

let sm = new SchemaManager();

(async function () {
    var schema = await sm.Get("schemas-1953-A");
    console.log("visualization 1",schema);
})();
(async function () {
    var schema = await sm.Get("schemas-1953-A");
    console.log("visualization 2", schema);
})();
(async function () {
    var schema = await sm.Get("schemas-1953-A");
    console.log("visualization 3", schema);
})();
(async function () {
    var schema = await sm.Get("schemas-1953-A");
    console.log("visualization 4", schema);
})();
(async function () {
    var schema = await sm.Get("schemas-1953-A");
    console.log("visualization 5",schema);
})();

实例:

// Stand-in for Xhr
function Xhr({url, callbackOk}) {
    setTimeout(() => {
        const n = url.lastIndexOf("/");
        const id = url.substring(n + 1);
        callbackOk.call({response: {id}});
    }, Math.random() * 100);
}
// Stand-in for ErrorDialog
function ErrorDialog({content}) {
    console.error(content);
}
function SchemaManager() {
    this.schemas = {}; // precached schemas
}
function loadSchema(schemaId) {
    console.log(`Loading ${schemaId}`);
    // *** Probably worth promise-enabling `Xhr` so we don't need a wrapper every time
    return new Promise((resolve, reject) => {
        Xhr({
            url: `/api/lightweightfield/${schemaId}`,
            type: "json",
            action: "GET",
            callbackOk: function() {
                console.log(`Loaded ${schemaId}`);
                resolve(this.response);
            },
            callbackError: (error) => { // *** I assume an error is provided?
                // *** Failed to load, so clear the promise from cache
                reject(error); // *** Pass along the error if provided
            }
        });
    });
}
SchemaManager.prototype = {
    Get: async function (schemaId) {
        let cacheEntry = this.schemas[schemaId];
        if (cacheEntry instanceof Promise) {
            // *** Previous load in progress, wait for it to complete
            try {
                console.log("Waiting for previous load");
                const schema = await cacheEntry;
                console.log("Using previous load result");
                return schema;
            } catch {
                // *** Previous load failed, try again
            }
        } else if (cacheEntry) {
            // *** The cache entry is a schema
            console.log("Cache hit");
            return cacheEntry;
        }
        // *** Need to load the schema
        console.log("Start load");
        cacheEntry = this.schemas[schemaId] = loadSchema(schemaId);
        try {
            const schema = await cacheEntry;
            console.log("Caching result for next time");
            this.schemas[schemaId] = schema;
            return schema;
        } catch {
            // *** Didn't get it
            this.schemas[schemaId] = null;
            throw error;
        }
    }
}

let sm = new SchemaManager();

(async function () {
    var schema = await sm.Get("schemas-1953-A");
    console.log("visualization 1",schema);
})();
(async function () {
    var schema = await sm.Get("schemas-1953-A");
    console.log("visualization 2", schema);
})();
(async function () {
    var schema = await sm.Get("schemas-1953-A");
    console.log("visualization 3", schema);
})();
(async function () {
    var schema = await sm.Get("schemas-1953-A");
    console.log("visualization 4", schema);
})();
(async function () {
    var schema = await sm.Get("schemas-1953-A");
    console.log("visualization 5",schema);
})();
.as-console-wrapper {
    max-height: 100% !important;
}

在那里,我设置了如果前一个失败,Get 会再次尝试 loadSchema。或者,您可以在缓存中存储某种“失败”对象以指示无法加载架构。

一些其他的小笔记:

  • JavaScript 代码中的 overwhelmingly-common 约定是只有构造函数以大写字母开头。所以 LoadGet 通常会写成 loadget.
  • 在构造函数上替换 prototype 属性 不是最佳实践,尤其是因为你弄乱了 constructor 属性 JavaScript 引擎设置放置在那里的对象。相反,写入它的属性——或者更好的是,使用 class 语法。

这是一个熟悉的问题……

您可以尝试重构您的方法,以便:

  • this.schemas 将模式 ID 映射到检索承诺,而不是模式数据。
  • Get() returns this.schemas[schemaId],如果需要,通过调用 Load() 来填充它。
  • Load() 根本不触及 this.schemas ,它的职责是严格构造查询和 return 模式数据的承诺.作为一个side-effect,它变得更容易测试。 (如果是我,我也会切换到功能模式并将此功能移出管理器,但这取决于您的约定。)

因此,Get() 的调用者将收到同一个承诺,当请求完成时,应该为所有调用者解决该承诺。稍后的调用者可能会收到 instantly-resolving 承诺。

这是一个演示场景的最小沙箱:https://codesandbox.io/s/compassionate-wu-x8vgl6?file=/src/index.js