读取大量对象的时间过长

时间:2018-02-10 19:33:04

标签: applescript jxa javascript-automation

我正在尝试在JXA中编写一批OmniFocus任务的脚本并遇到一些大的速度问题。我不认为这个问题是特定于OmniFocus或JXA的;相反,我认为这是对获取对象如何工作的更普遍的误解 - 我希望它像单个SQL查询一样工作,将所有对象加载到内存中,而不是似乎按需执行每个操作。

这是一个简单的例子 - 让我们获取所有未完成任务的名称(存储在后端的SQLite DB中):

var tasks = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false})
var totalTasks = tasks.length
for (var i = 0; i < totalTasks; i++) {
    tasks[i].name()
}

[Finished in 46.68s]

实际上获取900个任务的列表需要大约7秒 - 已经很慢 - 但是循环和读取基本属性需要另外40秒,大概是因为它为每个任务击中了数据库。 (另外,tasks的行为与数组不同 - 每次访问时都会重新计算。)

有没有办法快速完成这项工作 - 一次将一批对象及其所有属性读入内存?

2 个答案:

答案 0 :(得分:3)

简介

使用自动化JavaScript(JXA)的IPC技术AppleEvents,您从另一个应用程序请求信息的方式是向它发送一个“对象说明符”,它有点像点符号用于访问对象属性,有点像SQL或GraphQL查询。

接收应用程序评估对象说明符并确定它引用的对象(如果有)。然后返回表示引用对象的值。如果引用的对象是对象的集合,则返回的值可以是值列表。对象说明符还可以引用对象的属性。返回的值可以是字符串,数字,甚至是新的对象说明符。

对象说明符

用AppleScript编写的完全限定对象说明符的示例是:

a reference to the name of the first window of application "Safari"

在JXA中,将表达相同的对象说明符:

Application("Safari").windows[0].name

要向Safari发送IPC请求以要求它评估此对象说明符并使用值进行响应,您可以在对象说明符上调用.get()函数:

Application("Safari").windows[0].name.get()

作为.get()函数的简写,您可以直接调用对象说明符:

Application("Safari").windows[0].name()

向Safari发送一个请求,并返回单个值(在本例中为字符串)。

通过这种方式,对象说明符有点像点表示法来访问对象属性。但是对象说明符比那更强大。

集合

您可以有效地对集合执行地图或理解。在AppleScript中,它看起来像:

get the name of every window of Application "Safari"

在JXA中看起来像:

Application("Safari").windows.name.get()

即使这会请求多个值,它也只需要向Safari发送一个请求,然后在自己的窗口上迭代,收集每个请求的名称,然后发回一个包含所有名称的列表值字符串。无论Safari打开多少个窗口,此语句只会产生一个请求/响应。

For-loop反模式

对比for循环反模式的方法:

var nameOfEveryWindow = []
var everyWindowSpecifier = Application("Safari").windows
var numberOfWindows = everyWindowSpecifier.length
for (var i = 0; i < numberOfWindows; i++) {
    var windowNameSpecifier = everyWindowSpecifier[i].name
    var windowName = windowNameSpecifier.get()
    nameOfEveryWindow.push(windowName)
}

此方法可能需要更长时间,因为它需要length + 1个请求来获取名称集合。

(请注意,集合对象说明符的length属性是专门处理的,因为JXA中的集合对象说明符尝试表现得像本机JavaScript数组。在{x}上需要.get()调用长度属性。)

过滤,以及代码示例缓慢的原因

AppleEvents真正有趣的部分是所谓的“其子句”。这允许您提供过滤从中返回值的对象的标准。

在您的问题中包含的代码中,tasks是一个对象说明符,它引用已过滤的对象集合,仅包含使用其子句的未完成任务。请注意,这仍然只是参考;直到你在对象说明符上调用.get(),它只是指向某事物的指针,而不是事物本身。

您包含的代码然后实现for循环反模式,这可能是您观察到的性能如此之慢的原因。您正在向OmniFocus发送length + 1个请求。每次调用.name()都会产生另一个AppleEvent。

此外,您要求OmniFocus每次都重新过滤任务集合,因为您每次发送的对象说明符都包含其子句。

请改为尝试:

var taskNames = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false}).name.get()

这应该向OmniFocus发送一个请求,并返回每个未完成任务的名称数组。

尝试的另一种方法是让OmniFocus评估“其子句”一次,并返回一个对象说明符数组:

var taskSpecifiers = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false})()

迭代返回的对象数组指定并在每个对象上调用.name.get()可能比原始方法更快。

答案

虽然JXA可以获取对象集合的单个属性数组,但由于作者的疏忽,JXA似乎不支持获取集合中所有对象的所有属性。 / p>

因此,为了回答您的实际问题,使用JXA,无法一次将一批对象及其所有属性读入内存。

也就是说,AppleScript确实支持它:

tell app "OmniFocus" to get the properties of every flattened task of default document whose completed is false

使用JXA,如果你真的想要对象的所有属性,你必须回到for循环反模式,但是我们可以通过将其评估拉到for之外来避免多次评估其子句。循环:

var tasks = []
var taskSpecifiers = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false})()
var totalTasks = taskSpecifiers.length
for (var i = 0; i < totalTasks; i++) {
    tasks[i] = taskSpecifiers[i].properties()
}

最后,应该注意AppleScript还允许您请求特定的属性集:

get the {name, zoomable} of every window of application "Safari"

但是JXA无法为对象的多个属性或对象集合发送单个请求。

答案 1 :(得分:1)

尝试类似:

tell app "OmniFocus"
  tell default document
    get name of every flattened task whose completed is false
  end tell
end tell

Apple事件IPC 不是 OOP,它是RPC +简单的一流关系查询。 AppleScript对此进行了混淆,而且JXA不仅使其更加模糊,而且使其陷入困境;但是一旦你学会了解虚假的OO句法废话,它就会更有意义。 Thisthis可以提供更多见解。

[ETA:Omni最近在其应用程序中实现了自己的基于JavaScriptCore的嵌入式脚本支持;如果JS是你的东西你可能会发现更好的赌注。]