TL; DR:有没有办法重写这个基于回调的JavaScript代码来代替使用promises和生成器?
我使用Firefox Add-on SDK编写了Firefox扩展程序。与SDK一样,代码分为加载项脚本和content script。这两个脚本具有不同类型的权限:附加脚本可以执行奇特的操作,例如,通过js-ctypes接口调用本机代码,而内容脚本可以与网页交互。但是,附加脚本和内容脚本只能通过异步message-passing interface与相互交互。
我希望能够在普通的,无特权的网页上调用用户脚本中的扩展代码。这可以使用一种名为exportFunction
的机制来完成,该机制允许将一个函数从扩展代码导出到用户代码。到现在为止还挺好。 但是,只能在内容脚本中使用这没关系,除了我需要导出的函数需要使用前面提到的js- ctypes接口,只能在附加脚本中完成。exportFunction
,而不能使用附加脚本。
(编辑:事实证明不只能在内容脚本中使用exportFunction
。请参阅下面的评论。) < / p>
为了解决这个问题,我在内容脚本中编写了一个“包装器”函数;这个包装器是我实际通过exportFunction
导出的函数。然后,我通过将消息传递给附加脚本,使包装函数在附加脚本中调用“真实”函数。这是内容脚本的样子;它正在导出函数lengthInBytes
:
// content script
function lengthInBytes(arg, callback) {
self.port.emit("lengthInBytesCalled", arg);
self.port.on("lengthInBytesReturned", function(result) {
callback(result);
});
}
exportFunction(lengthInBytes, unsafeWindow, {defineAs: "lengthInBytes",
allowCallbacks: true});
这是附加脚本,其中定义了lengthInBytes
的“真实”版本。此处的代码侦听内容脚本以向其发送lengthInBytesCalled
消息,然后调用lengthInBytes
的实际版本,并将结果发回lengthInBytesReturned
消息。 (在现实生活中,当然,我可能不需要使用js-ctypes来获取字符串的长度;这只是一个更有趣的C库调用的替身。使用你的想象力。:))< / p>
// add-on script
// Get "chrome privileges" to access the Components object.
var {Cu, Cc, Ci} = require("chrome");
Cu.import("resource://gre/modules/ctypes.jsm");
Cu.import("resource://gre/modules/Services.jsm");
var pageMod = require("sdk/page-mod");
var data = require("sdk/self").data;
pageMod.PageMod({
include: ["*", "file://*"],
attachTo: ["existing", "top"],
contentScriptFile: data.url("content.js"),
contentScriptWhen: "start", // Attach the content script before any page script loads.
onAttach: function(worker) {
worker.port.on("lengthInBytesCalled", function(arg) {
let result = lengthInBytes(arg);
worker.port.emit("lengthInBytesReturned", result);
});
}
});
function lengthInBytes(str) {
// str is a JS string; convert it to a ctypes string.
let cString = ctypes.char.array()(str);
libc.init();
let length = libc.strlen(cString); // defined elsewhere
libc.shutdown();
// `length` is a ctypes.UInt64; turn it into a JSON-serializable
// string before returning it.
return length.toString();
}
最后,用户脚本(仅在安装了扩展程序时才有效)如下所示:
// user script, on an ordinary web page
lengthInBytes("hello", function(result) {
console.log("Length in bytes: " + result);
});
现在,用户脚本中对lengthInBytes
的调用是异步调用;而不是返回结果,它在其回调参数中“返回”其结果。但是,在看到this video关于使用promises和生成器来使异步代码更容易理解之后,我想知道如何以这种方式重写这段代码。
具体来说,我想要的是lengthInBytes
返回Promise
,它以某种方式表示lengthInBytesReturned
消息的最终有效负载。然后,在用户脚本中,我有一个评估yield lengthInBytes("hello")
的生成器以获得结果。
但是,即使在观看上面链接的视频并阅读有关承诺和发电机之后,我仍然难以理解如何解决这个问题。返回lengthInBytes
的{{1}}版本如下所示:
Promise
并且用户脚本将涉及类似
的内容function lengthInBytesPromise(arg) {
self.port.emit("lengthInBytesCalled", arg);
return new Promise(
// do something with `lengthInBytesReturned` event??? idk.
);
}
但这和我能够弄清楚的一样多。我将如何编写此代码,以及调用它的用户脚本是什么样的?我想做什么甚至可能?
A complete working example of what I have so far is here.
感谢您的帮助!
答案 0 :(得分:7)
这个问题的一个非常优雅的解决方案是以async
functions的形式出现在下一个 next 版本的JavaScript ECMAScript 7中,它是Promise
的结合s和发电机,两者的疣糖。更多关于这个答案的最底层。
我是Regenerator的作者,这是一个在今天的浏览器中支持async
函数的转换器,但我意识到建议你在附加开发过程中引入编译步骤可能有点过头了。 ,所以我会把注意力集中在你实际问的问题上:如何设计一个合理的Promise
- 返回API,以及使用这种API最好的方法是什么?
首先,我将如何实施lengthInBytesPromise
:
function lengthInBytesPromise(arg) {
self.port.emit("lengthInBytesCalled", arg);
return new Promise(function(resolve, reject) {
self.port.on("lengthInBytesReturned", function(result) {
resolve(result);
});
});
}
在实例化promise时立即调用function(resolve, reject) { ... }
回调,resolve
和reject
参数是可用于为promise提供最终值的回调函数。 / p>
如果在这个例子中有可能失败,你可以将Error
对象传递给reject
回调,但看起来这个操作是绝对可靠的,所以我们可以在这里忽略这种情况
这就是API如何创建承诺,但消费者如何使用这样的API?在您的内容脚本中,最简单的方法是调用lengthInBytesPromise
并直接与生成的Promise
进行交互:
lengthInBytesPromise("hello").then(function(length) {
console.log(result);
});
在这种风格中,你将依赖于lengthInBytesPromise
结果的代码放在一个回传函数中传递给promise的.then
方法,这可能看起来不像是对回调地狱的巨大改进,但如果您链接更长的一系列异步操作,至少缩进更易于管理:
lengthInBytesPromise("hello").then(function(length) {
console.log(result);
return someOtherPromise(length);
}).then(function(resultOfThatOtherPromise) {
return yetAnotherPromise(resultOfThatOtherPromise + 1);
}).then(function(finalResult) {
console.log(finalResult);
});
生成器可以帮助减少样板,但是需要额外的运行时支持。可能最简单的方法是使用Dave Herman的task.js library:
spawn(function*() { // Note the *; this is a generator function!
var length = yield lengthInBytesPromise("hello");
var resultOfThatOtherPromise = yield someOtherPromise(length);
var finalResult = yield yetAnotherPromise(resultOfThatOtherPromise + 1);
console.log(finalResult);
});
这段代码更短,回调更少,这是肯定的。你可以猜到,大部分魔法都被简单地移到了spawn
函数中,但它的实现实际上非常简单。
spawn
函数接受生成器函数并立即调用它以获取生成器对象,然后调用生成器对象的gen.next()
方法以获得第一个yield
版本的承诺( lengthInBytesPromise("hello")
)的结果,然后等待该承诺得到满足,然后使用结果调用gen.next(result)
,该结果为第一个yield
表达式(分配给{{1}的表达式提供值并且导致生成器函数运行到下一个length
表达式(即yield
),产生下一个承诺,依此类推,直到没有更多的承诺等待,因为生成器函数最终返回。
为了让您了解ES7中的内容,以下是使用yield someOtherPromise(length)
函数实现完全相同的功能的方法:
async
这里真正发生的是async function process(arg) {
var length = await lengthInBytesPromise(arg);
var resultOfThatOtherPromise = await someOtherPromise(length);
var finalResult = await yetAnotherPromise(resultOfThatOtherPromise + 1);
return finalResult;
}
// An async function always returns a Promise for its own return value.
process(arg).then(function(finalResult) {
console.log(finalResult);
});
关键字已取代async
函数(以及spawn
生成器语法),*
已替换await
}。这不是一个巨大的飞跃,但将语法内置到语言中而不必依赖于task.js之类的外部库将非常好。
如果您对使用yield
函数而不是task.js感到兴奋,那么请务必查看Regenerator!
答案 1 :(得分:2)
我认为Promise是通过将原始回调包装在resolve / reject函数中构建的:
function lengthInBytesPromise(arg) {
self.port.emit("lengthInBytesCalled", arg);
let returnVal = new Promise(function(resolve, reject) {
self.port.on("lengthInBytesReturned", function(result) {
if (result) { // maybe some kind of validity check
resolve(result);
} else {
reject("Something went wrong?");
}
}
});
return returnVal;
}
基本上,它会创建Promise并立即返回它,而Promise的内部启动然后处理异步任务。我认为在一天结束时,有人必须采用回调式代码并将其包装起来。
您的用户将执行类似
的操作lengthInBytesPromise(arg).then(function(result) {
// do something with the result
});