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



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

之后,我想"预加载"额外的信息,使我能够以更详细的方式与每个单独的可视化进行交互。该信息存储在模式中。一个或多个可视化可以共享相同的模式。

因此,我想加载一次模式,然后返回缓存的模式,以避免多个服务器请求。

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

解决这个问题的最好方法是什么?我可以做一些非常讨厌的事情,比如跟踪正在进行的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返回(对于给定的模式)并在执行任何操作之前等待对同一模式的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代码中最常见的约定是只有构造函数以大写字母开头。所以LoadGet通常写成loadget
  • 在构造函数中替换 prototype属性不是最佳实践,尤其是因为您会弄乱由JavaScript引擎设置的对象放在那里的constructor属性。相反,应该写入它的属性—或者更好的是,使用class语法。

这是一个常见的问题…

你可以尝试重构你的方法,这样:

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

作为结果,Get()的调用者将接收到相同的单个承诺,当请求完成时应该为所有它们解析。之后的调用者可能会收到一个即时解析的承诺。

下面是演示该场景的最小沙盒:https://codesandbox.io/s/compassionate-wu-x8vgl6?file=/src/index.js

最新更新