如何处理对后端服务进行相同调用的多个浏览器脚本
How do I handle multiple browser scripts making the same calls to the back-end service
我有一个网页,其中的不同部分都需要相同的后端数据。每个都是孤立的,因此它们最终都会对后端进行相同的调用。
当调用已在进行中并由同一网页上的不同代码段启动时,避免调用 Web 服务器的最佳方法是什么?
这是一个例子。我将使用 setTimeout 来模拟异步调用。
假设有一个异步函数 returns 联系人列表,在此示例中它基本上是一个简单的字符串数组:
var getContacts = function() {
log('Calling back-end to get contact list.');
return new Promise(function(resolve, reject) {
setTimeout(function() {
log('New data received from back-end.');
resolve(["Mary","Frank","Klaus"]);
}, 3000);
});
};
现在,让我们创建三个独立的函数,每个函数出于不同的目的调用上述函数。
转出联系人列表:
var dumpContacts = function() {
getContacts().then(function(contacts) {
for( var i = 0; i < contacts.length; i++ ) {
log( "Contact " + (i + 1) + ": " + contacts[i] );
}
});
};
确定特定联系人是否在列表中:
var contactExists = function(contactName) {
return getContacts().then(function(contacts) {
return contacts.indexOf(contactName) >= 0 ? true : false;
});
};
获取第一个联系人的姓名:
var getFirstContact = function() {
return getContacts().then(function(contacts) {
if ( contacts.length > 0 ) {
return contacts[0];
}
});
};
下面是一些使用这三个函数的示例代码:
// Show all contacts
dumpContacts();
// Does contact 'Jane' exist?
contactExists("Jane").then(function(exists){
log("Contact 'Jane' exist: " + exists);
});
getFirstContact().then(function(firstContact){
log("first contact: " + firstContact);
});
以上例程使用全局 log() 函数。 console.log() 可以改用。上面的 log() 函数记录到浏览器 window 并且实现如下:
function log() {
var args = Array.prototype.slice.call(arguments).join(", ");
console.log(args);
var output = document.getElementById('output');
output.innerHTML += args + "<br/>";
}
并在 html 中要求以下内容:
<div id='output'><br/></div>
当上面的代码为运行时,你会看到:
Calling back-end to get contact list.
和
New data received from back-end.
三遍,没必要
如何解决?
这个样本在Plunker上可以执行:
http://plnkr.co/edit/6ysbNTf1lSf5b7L3sJxQ?p=preview
如果希望减少对后端的不必要调用的次数,请坚持承诺,在它仍未解决时,return它用于新调用而不是发出另一个调用后端。
这是一个将异步函数(return承诺)转换为仅在承诺尚未解决时才调用的函数的例程。
var makeThrottleFunction = function (asyncFunction) {
var currentPromiser = getPromise = function() {
var promise = new Promise(function(resolve, reject) {
asyncFunction().then(function(value) {
resolve(value);
currentPromiser = getPromise;
}).catch(function(e) {
reject(e);
currentPromiser = getPromise;
});
});
currentPromiser = function() {
return promise;
};
return promise;
}
return function () {
return currentPromiser();
};
};
在您的例程中,您可以像这样转换 getContacts
:
var getContacts = makeThrottleFunction(getContacts);
或者直接传递整个函数体。
请记住,这仅适用于对后端的无参数调用。
示例 plunker 代码:http://plnkr.co/edit/4JTtHmFTZmiHugWNnlo9?p=preview
编辑、更新
删除了"nested"ternary
模式;添加
- a)
dfd.err()
, .catch()
处理Promise.reject(/* reason ? */)
arguments
传递给dfd.fn()
;
- b)
dfd.process()
内的 args === ""
处理 ""
:空 String
作为 argument
传递给 dfd.fn()
- c) 替换 "chaining"
.then()
调用 then.apply(dfd.promise, [contactExists, getFirstContact])
本机 Error()
作为 argument
传递:dfd.fn(new Error("error"))
在 global
范围内处理; dfd.fn()
仍然 returns dfd.promise
。可能在 dfd.process()
之前或之时调整为 return "early" 在 Error
或将 Error
传递给 dfd.err()
;根据要求。未在下面的 js
中解决。
尝试
var dfd = {
// set `active` : `false`
"active": false,
// set `promise`: `undefined`
"promise": void 0,
// process `arguments`, if any, passed to `dfd.fn`
"process": function process(args) {
// return `Function` call, `arguments`,
// or "current" `dfd.promise`;
// were `args`:`arguments` passed ?
// handle `""` empty `String` passed as `args`
return args === "" || !!args
// if `args`:`Function`, call `args` with `this`:`dfd`,
// or, set `args` as `value`, `reason`
// of "next" `dfd.promise`
// return "next" `dfd.promise`
? args instanceof Function && args.call(this) || args
// set `dfd.active`:`false`
// when "current" `dfd.promise`:`Promise` `fulfilled`,
// return "current" `dfd.promise`
: this.active = true && this.promise
},
// handle `fulfilled` `Promise.reject(/* `reason` ? */)`,
// passed as `args` to `dfd.fn`
"err": function err(e) {
// notify , log `reason`:`Promise.reject(/* `reason` ? */)`, if any,
// or, log `undefined` , if no `reason` passed: `Promise.reject()`
console.log("rejected `Promise` reason:", e || void 0);
},
// do stuff
"fn": function fn(args /* , do other stuff */) {
// set `_dfd` : `this` : `dfd` object
var _dfd = this;
// if "current" `dfd.promise`:`Promise` processing,
// wait for `fulfilled` `dfd.promise`;
// return `dfd.promise`
_dfd.promise = !_dfd.active
// set, reset `dfd.promise`
// process call to `dfd.async`;
// `args`:`arguments` passed to `dfd.fn` ?,
// if `args` passed, are `args` `function` ?,
// if `args` `function`, call `args` with
// `this`:`dfd`;
// or, return `args`
? _dfd.process(args)
// if `_dfd.active`, `_dfd.promise` defined,
// return "current" `_dfd.promise`
: _dfd.promise.then(function(deferred) {
// `deferred`:`_dfd.promise`
// do stuff with `deferred`,
// do other stuff,
// return "current", "next" `deferred`
return deferred
})
// handle `args`:`fulfilled`,
// `Promise.reject(/* `reason` ? */)`
.catch(_dfd.err);
return Promise.resolve(_dfd.promise).then(function(data) {
// `data`:`undefined`, `_dfd.promise`
// set `_dfd.active`:`false`,
// return `value` of "current", "next" `_dfd.promise`
_dfd.active = false;
return data
})
// handle `fulfilled` `Promise.reject(/* `reason` ? */),
// if reaches here ?
.catch(_dfd.err)
}
};
function log() {
var args = Array.prototype.slice.call(arguments).join(", ");
console.log(args);
var output = document.getElementById('output');
output.innerHTML += args + "<br/>";
};
var dumpContacts = function () {
log('Calling back-end to get contact list.');
return new Promise(function (resolve, reject) {
setTimeout(function () {
log('New data received from back-end.');
resolve(["Mary", "Frank", "Klaus"]);
}, 3000);
});
};
var getContacts = function () {
return dfd.async().then(function (contacts) {
for (var i = 0; i < contacts.length; i++) {
log("Contact " + (i + 1) + ": " + contacts[i]);
}
});
};
var contactExists = function (contactName) {
return dfd.async().then(function (contacts) {
return contacts.indexOf(contactName) >= 0 ? true : false;
});
};
var getFirstContact = function () {
return dfd.async().then(function (contacts) {
if (contacts.length > 0) {
return contacts[0];
}
return contacts
});
};
// Test:
// Show all contacts
dfd.async(dumpContacts)
.then(getContacts)
.then.apply(dfd.promise, [
// Does contact 'Jane' exist?
contactExists("Jane").then(function (exists) {
log("Contact 'Jane' exist: " + exists);
})
, getFirstContact().then(function (firstContact) {
log("first contact: " + firstContact);
})
]);
function log() {
var args = Array.prototype.slice.call(arguments).join(", ");
console.log(args);
var output = document.getElementById('output');
output.innerHTML += args + "<br/>";
return output
};
var dfd = {
"active": false,
"promise": void 0,
"process": function process(args) {
return args === "" || !!args
? args instanceof Function && args.call(this) || args
: this.active = true && this.promise
},
"err": function err(e) {
console.log("rejected `Promise` reason:", e || void 0);
},
"fn": function fn(args) {
var _dfd = this;
_dfd.promise = !_dfd.active
? _dfd.process(args)
: _dfd.promise.then(function(deferred) {
return deferred
})
.catch(_dfd.err);
return Promise.resolve(_dfd.promise).then(function(data) {
_dfd.active = false;
return data
})
.catch(_dfd.err)
}
};
var dumpContacts = function() {
log('Calling back-end to get contact list.');
return new Promise(function(resolve, reject) {
setTimeout(function() {
log('New data received from back-end.');
resolve(["Mary", "Frank", "Klaus"]);
}, 3000);
});
};
var getContacts = function() {
return dfd.fn().then(function(contacts) {
for (var i = 0; i < contacts.length; i++) {
log("Contact " + (i + 1) + ": " + contacts[i]);
}
});
};
var contactExists = function(contactName) {
return dfd.fn().then(function(contacts) {
return contacts.indexOf(contactName) >= 0 ? true : false;
});
};
var getFirstContact = function() {
return dfd.fn().then(function(contacts) {
if (contacts.length > 0) {
return contacts[0];
}
return contacts
});
};
// Test:
// Show all contacts
dfd.fn(dumpContacts)
.then(getContacts)
.then(function() {
// Does contact 'Jane' exist?
return contactExists("Jane").then(function(exists) {
log("Contact 'Jane' exist: " + exists);
})
})
.then(function() {
return getFirstContact().then(function(firstContact) {
log("first contact: " + firstContact);
})
});
<body>
Must use browser that supportes the Promises API, such as Chrome
<div id='output'>
<br/>
</div>
<hr>
</body>
只需将结果缓存在进行调用的函数中:
function cache(promiseReturningFn){
var cachedVal = null; // start without cached value
function cached(){
if(cachedVal) return cachedVal; // prefer cached result
cachedVal = promiseReturningFn.apply(this, arguments); // delegate
return cachedVal; // after we saved it, return it
}
cached.flush = function(){ cachedVal = undefined; };
return cached;
}
这有一个警告,即实际结果为空时会失败,但除此之外它可以很好地完成工作。
您现在可以缓存任何 promise 返回函数 - 上面的版本只缓存忽略参数 - 但您可以构建一个类似的函数,它也有一个 Map 和基于不同参数的缓存 - 但让我们专注于您的用例。
var getContactsCached = cache(getContacts);
getContactsCached();
getContactsCached();
getContactsCached(); // only one async call ever made
缓存方法实际上什至与 promises 无关——它所做的只是接受一个函数并缓存它的结果——你可以将它用于任何事情。事实上,如果您正在使用像 underscore 这样的库,您已经可以使用 _.memoize
为您完成它了。
我有一个网页,其中的不同部分都需要相同的后端数据。每个都是孤立的,因此它们最终都会对后端进行相同的调用。
当调用已在进行中并由同一网页上的不同代码段启动时,避免调用 Web 服务器的最佳方法是什么?
这是一个例子。我将使用 setTimeout 来模拟异步调用。
假设有一个异步函数 returns 联系人列表,在此示例中它基本上是一个简单的字符串数组:
var getContacts = function() {
log('Calling back-end to get contact list.');
return new Promise(function(resolve, reject) {
setTimeout(function() {
log('New data received from back-end.');
resolve(["Mary","Frank","Klaus"]);
}, 3000);
});
};
现在,让我们创建三个独立的函数,每个函数出于不同的目的调用上述函数。
转出联系人列表:
var dumpContacts = function() {
getContacts().then(function(contacts) {
for( var i = 0; i < contacts.length; i++ ) {
log( "Contact " + (i + 1) + ": " + contacts[i] );
}
});
};
确定特定联系人是否在列表中:
var contactExists = function(contactName) {
return getContacts().then(function(contacts) {
return contacts.indexOf(contactName) >= 0 ? true : false;
});
};
获取第一个联系人的姓名:
var getFirstContact = function() {
return getContacts().then(function(contacts) {
if ( contacts.length > 0 ) {
return contacts[0];
}
});
};
下面是一些使用这三个函数的示例代码:
// Show all contacts
dumpContacts();
// Does contact 'Jane' exist?
contactExists("Jane").then(function(exists){
log("Contact 'Jane' exist: " + exists);
});
getFirstContact().then(function(firstContact){
log("first contact: " + firstContact);
});
以上例程使用全局 log() 函数。 console.log() 可以改用。上面的 log() 函数记录到浏览器 window 并且实现如下:
function log() {
var args = Array.prototype.slice.call(arguments).join(", ");
console.log(args);
var output = document.getElementById('output');
output.innerHTML += args + "<br/>";
}
并在 html 中要求以下内容:
<div id='output'><br/></div>
当上面的代码为运行时,你会看到:
Calling back-end to get contact list.
和
New data received from back-end.
三遍,没必要
如何解决?
这个样本在Plunker上可以执行: http://plnkr.co/edit/6ysbNTf1lSf5b7L3sJxQ?p=preview
如果希望减少对后端的不必要调用的次数,请坚持承诺,在它仍未解决时,return它用于新调用而不是发出另一个调用后端。
这是一个将异步函数(return承诺)转换为仅在承诺尚未解决时才调用的函数的例程。
var makeThrottleFunction = function (asyncFunction) {
var currentPromiser = getPromise = function() {
var promise = new Promise(function(resolve, reject) {
asyncFunction().then(function(value) {
resolve(value);
currentPromiser = getPromise;
}).catch(function(e) {
reject(e);
currentPromiser = getPromise;
});
});
currentPromiser = function() {
return promise;
};
return promise;
}
return function () {
return currentPromiser();
};
};
在您的例程中,您可以像这样转换 getContacts
:
var getContacts = makeThrottleFunction(getContacts);
或者直接传递整个函数体。
请记住,这仅适用于对后端的无参数调用。
示例 plunker 代码:http://plnkr.co/edit/4JTtHmFTZmiHugWNnlo9?p=preview
编辑、更新
删除了"nested"ternary
模式;添加
- a)
dfd.err()
,.catch()
处理Promise.reject(/* reason ? */)
arguments
传递给dfd.fn()
; - b)
dfd.process()
内的args === ""
处理""
:空String
作为argument
传递给dfd.fn()
- c) 替换 "chaining"
.then()
调用then.apply(dfd.promise, [contactExists, getFirstContact])
本机 Error()
作为 argument
传递:dfd.fn(new Error("error"))
在 global
范围内处理; dfd.fn()
仍然 returns dfd.promise
。可能在 dfd.process()
之前或之时调整为 return "early" 在 Error
或将 Error
传递给 dfd.err()
;根据要求。未在下面的 js
中解决。
尝试
var dfd = {
// set `active` : `false`
"active": false,
// set `promise`: `undefined`
"promise": void 0,
// process `arguments`, if any, passed to `dfd.fn`
"process": function process(args) {
// return `Function` call, `arguments`,
// or "current" `dfd.promise`;
// were `args`:`arguments` passed ?
// handle `""` empty `String` passed as `args`
return args === "" || !!args
// if `args`:`Function`, call `args` with `this`:`dfd`,
// or, set `args` as `value`, `reason`
// of "next" `dfd.promise`
// return "next" `dfd.promise`
? args instanceof Function && args.call(this) || args
// set `dfd.active`:`false`
// when "current" `dfd.promise`:`Promise` `fulfilled`,
// return "current" `dfd.promise`
: this.active = true && this.promise
},
// handle `fulfilled` `Promise.reject(/* `reason` ? */)`,
// passed as `args` to `dfd.fn`
"err": function err(e) {
// notify , log `reason`:`Promise.reject(/* `reason` ? */)`, if any,
// or, log `undefined` , if no `reason` passed: `Promise.reject()`
console.log("rejected `Promise` reason:", e || void 0);
},
// do stuff
"fn": function fn(args /* , do other stuff */) {
// set `_dfd` : `this` : `dfd` object
var _dfd = this;
// if "current" `dfd.promise`:`Promise` processing,
// wait for `fulfilled` `dfd.promise`;
// return `dfd.promise`
_dfd.promise = !_dfd.active
// set, reset `dfd.promise`
// process call to `dfd.async`;
// `args`:`arguments` passed to `dfd.fn` ?,
// if `args` passed, are `args` `function` ?,
// if `args` `function`, call `args` with
// `this`:`dfd`;
// or, return `args`
? _dfd.process(args)
// if `_dfd.active`, `_dfd.promise` defined,
// return "current" `_dfd.promise`
: _dfd.promise.then(function(deferred) {
// `deferred`:`_dfd.promise`
// do stuff with `deferred`,
// do other stuff,
// return "current", "next" `deferred`
return deferred
})
// handle `args`:`fulfilled`,
// `Promise.reject(/* `reason` ? */)`
.catch(_dfd.err);
return Promise.resolve(_dfd.promise).then(function(data) {
// `data`:`undefined`, `_dfd.promise`
// set `_dfd.active`:`false`,
// return `value` of "current", "next" `_dfd.promise`
_dfd.active = false;
return data
})
// handle `fulfilled` `Promise.reject(/* `reason` ? */),
// if reaches here ?
.catch(_dfd.err)
}
};
function log() {
var args = Array.prototype.slice.call(arguments).join(", ");
console.log(args);
var output = document.getElementById('output');
output.innerHTML += args + "<br/>";
};
var dumpContacts = function () {
log('Calling back-end to get contact list.');
return new Promise(function (resolve, reject) {
setTimeout(function () {
log('New data received from back-end.');
resolve(["Mary", "Frank", "Klaus"]);
}, 3000);
});
};
var getContacts = function () {
return dfd.async().then(function (contacts) {
for (var i = 0; i < contacts.length; i++) {
log("Contact " + (i + 1) + ": " + contacts[i]);
}
});
};
var contactExists = function (contactName) {
return dfd.async().then(function (contacts) {
return contacts.indexOf(contactName) >= 0 ? true : false;
});
};
var getFirstContact = function () {
return dfd.async().then(function (contacts) {
if (contacts.length > 0) {
return contacts[0];
}
return contacts
});
};
// Test:
// Show all contacts
dfd.async(dumpContacts)
.then(getContacts)
.then.apply(dfd.promise, [
// Does contact 'Jane' exist?
contactExists("Jane").then(function (exists) {
log("Contact 'Jane' exist: " + exists);
})
, getFirstContact().then(function (firstContact) {
log("first contact: " + firstContact);
})
]);
function log() {
var args = Array.prototype.slice.call(arguments).join(", ");
console.log(args);
var output = document.getElementById('output');
output.innerHTML += args + "<br/>";
return output
};
var dfd = {
"active": false,
"promise": void 0,
"process": function process(args) {
return args === "" || !!args
? args instanceof Function && args.call(this) || args
: this.active = true && this.promise
},
"err": function err(e) {
console.log("rejected `Promise` reason:", e || void 0);
},
"fn": function fn(args) {
var _dfd = this;
_dfd.promise = !_dfd.active
? _dfd.process(args)
: _dfd.promise.then(function(deferred) {
return deferred
})
.catch(_dfd.err);
return Promise.resolve(_dfd.promise).then(function(data) {
_dfd.active = false;
return data
})
.catch(_dfd.err)
}
};
var dumpContacts = function() {
log('Calling back-end to get contact list.');
return new Promise(function(resolve, reject) {
setTimeout(function() {
log('New data received from back-end.');
resolve(["Mary", "Frank", "Klaus"]);
}, 3000);
});
};
var getContacts = function() {
return dfd.fn().then(function(contacts) {
for (var i = 0; i < contacts.length; i++) {
log("Contact " + (i + 1) + ": " + contacts[i]);
}
});
};
var contactExists = function(contactName) {
return dfd.fn().then(function(contacts) {
return contacts.indexOf(contactName) >= 0 ? true : false;
});
};
var getFirstContact = function() {
return dfd.fn().then(function(contacts) {
if (contacts.length > 0) {
return contacts[0];
}
return contacts
});
};
// Test:
// Show all contacts
dfd.fn(dumpContacts)
.then(getContacts)
.then(function() {
// Does contact 'Jane' exist?
return contactExists("Jane").then(function(exists) {
log("Contact 'Jane' exist: " + exists);
})
})
.then(function() {
return getFirstContact().then(function(firstContact) {
log("first contact: " + firstContact);
})
});
<body>
Must use browser that supportes the Promises API, such as Chrome
<div id='output'>
<br/>
</div>
<hr>
</body>
只需将结果缓存在进行调用的函数中:
function cache(promiseReturningFn){
var cachedVal = null; // start without cached value
function cached(){
if(cachedVal) return cachedVal; // prefer cached result
cachedVal = promiseReturningFn.apply(this, arguments); // delegate
return cachedVal; // after we saved it, return it
}
cached.flush = function(){ cachedVal = undefined; };
return cached;
}
这有一个警告,即实际结果为空时会失败,但除此之外它可以很好地完成工作。
您现在可以缓存任何 promise 返回函数 - 上面的版本只缓存忽略参数 - 但您可以构建一个类似的函数,它也有一个 Map 和基于不同参数的缓存 - 但让我们专注于您的用例。
var getContactsCached = cache(getContacts);
getContactsCached();
getContactsCached();
getContactsCached(); // only one async call ever made
缓存方法实际上什至与 promises 无关——它所做的只是接受一个函数并缓存它的结果——你可以将它用于任何事情。事实上,如果您正在使用像 underscore 这样的库,您已经可以使用 _.memoize
为您完成它了。