我有一个分析仪表板。仪表板由一个或多个可视化构建而成,在初始呈现仪表板时,我检索呈现可视化所需的最小信息。
之后,我想"预加载"额外的信息,使我能够以更详细的方式与每个单独的可视化进行交互。该信息存储在模式中。一个或多个可视化可以共享相同的模式。
因此,我想加载一次模式,然后返回缓存的模式,以避免多个服务器请求。
我正在解决的挑战是避免多个可视化,在同一时间,从请求这个信息。当一个可视化请求模式时,我们有多个请求相同的模式,我不想调用多个服务器请求,而是让它们等待第一个完成,然后使用缓存的版本。
解决这个问题的最好方法是什么?我可以做一些非常讨厌的事情,比如跟踪正在进行的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代码中最常见的约定是只有构造函数以大写字母开头。所以
Load
和Get
通常写成load
和get
。 - 在构造函数中替换
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