zam*_*ezi 1 applescript javascript-automation
我\xe2\x80\x99m 尝试在 JXA 中编写一批 OmniFocus 任务脚本,但遇到了一些严重的速度问题。我不认为这个问题是 OmniFocus 或 JXA 特有的;相反,我认为这是对获取对象如何工作的更普遍的误解 - 我希望它像单个 SQL 查询一样工作,加载内存中的所有对象,但它似乎按需执行每个操作。
\n\n这里\xe2\x80\x99s是一个简单的例子 - 让\xe2\x80\x99s获取所有未完成任务的名称(这些任务存储在后端的SQLite DB中):
\n\nvar tasks = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false})\nvar totalTasks = tasks.length\nfor (var i = 0; i < totalTasks; i++) {\n tasks[i].name()\n}\nRun Code Online (Sandbox Code Playgroud)\n\n[Finished in 46.68s]
实际上获取 900 个任务的列表大约需要 7 秒——已经很慢了——但是循环和读取基本属性又需要 40 秒,大概是因为每个任务都会访问数据库。(此外,它tasks的行为不像数组 - 它似乎在每次访问时都会重新计算。)
有什么方法可以快速做到这一点 - 一次将一批对象及其所有属性读入内存?
\n通过 AppleEvents(JavaScript for Automation (JXA) 构建的 IPC 技术),您从另一个应用程序请求信息的方式是向其发送一个“对象说明符”,其工作方式有点像用于访问对象属性的点表示法,以及一个有点像 SQL 或 GraphQL 查询。
接收应用程序评估对象说明符并确定它引用哪些对象(如果有)。然后它返回一个表示所引用对象的值。如果引用的对象是对象的集合,则返回的值可以是值的列表。对象说明符还可以指对象的属性。返回的值可以是字符串、数字,甚至是新的对象说明符。
用 AppleScript 编写的完全限定对象说明符的示例是:
a reference to the name of the first window of application "Safari"
Run Code Online (Sandbox Code Playgroud)
在 JXA 中,将表达相同的对象说明符:
Application("Safari").windows[0].name
Run Code Online (Sandbox Code Playgroud)
要向 Safari 发送 IPC 请求以要求其评估此对象说明符并以值进行响应,您可以.get()在对象说明符上调用该函数:
Application("Safari").windows[0].name.get()
Run Code Online (Sandbox Code Playgroud)
作为函数的简写.get(),您可以直接调用对象说明符:
Application("Safari").windows[0].name()
Run Code Online (Sandbox Code Playgroud)
向 Safari 发送单个请求,并返回单个值(在本例中为字符串)。
通过这种方式,对象说明符的工作方式有点像访问对象属性的点表示法。但对象说明符比这更强大。
您可以有效地对集合执行映射或理解。在 AppleScript 中,这看起来像:
get the name of every window of Application "Safari"
Run Code Online (Sandbox Code Playgroud)
在 JXA 中,它看起来像:
Application("Safari").windows.name.get()
Run Code Online (Sandbox Code Playgroud)
即使这请求多个值,它也只需要向 Safari 发送一个请求,然后 Safari 遍历自己的窗口,收集每个值的名称,然后发回包含所有名称字符串的单个列表值。无论 Safari 打开了多少个窗口,此语句只会产生单个请求/响应。
将该方法与 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)
}
Run Code Online (Sandbox Code Playgroud)
此方法可能需要更长的时间,因为它需要length+1 次请求才能获取名称集合。
(请注意,length集合对象说明符的属性是经过特殊处理的,因为 JXA 中的集合对象说明符尝试表现得像本机 JavaScript 数组。不需要.get()(或允许)对 length 属性进行任何调用。)
AppleEvents 真正有趣的部分是所谓的“whose 子句”。这允许您提供条件来过滤将从中返回值的对象。
在您的问题中包含的代码中,tasks是一个对象说明符,它指的是已使用 who 子句过滤为仅包含未完成任务的对象集合。请注意,此时这仍然只是参考;.get()在调用对象说明符之前,它只是一个指向某物的指针,而不是该物本身。
然后,您包含的代码实现了 for 循环反模式,这可能就是您观察到的性能如此缓慢的原因。您正在向 OmniFocus 发送length+1 请求。每次调用都会产生.name()另一个 AppleEvent。
此外,您每次都要求 OmniFocus 重新过滤任务集合,因为您每次发送的对象说明符都包含 who 子句。
试试这个:
var taskNames = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false}).name.get()
Run Code Online (Sandbox Code Playgroud)
这应该向 OmniFocus 发送单个请求,并返回每个未完成任务的名称数组。
另一种尝试方法是要求 OmniFocus 计算一次“whose 子句”,并返回一个对象说明符数组:
var taskSpecifiers = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false})()
Run Code Online (Sandbox Code Playgroud)
迭代返回的对象指定数组并调用.name.get()每个对象可能会比原始方法更快。
虽然 JXA 可以获取对象集合的单个属性的数组,但由于作者的疏忽,JXA 似乎不支持获取集合中所有对象的所有属性。
因此,为了回答您的实际问题,使用 JXA,没有办法一次性将一批对象及其所有属性读入内存。
也就是说,AppleScript 确实支持它:
tell app "OmniFocus" to get the properties of every flattened task of default document whose completed is false
Run Code Online (Sandbox Code Playgroud)
使用 JXA,如果您确实想要对象的所有属性,则必须退回到 for 循环反模式,但我们可以通过将其评估拉到 for 循环之外来避免多次评估 who 子句:
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()
}
Run Code Online (Sandbox Code Playgroud)
最后,应该注意的是,AppleScript 还允许您请求特定的属性集:
get the {name, zoomable} of every window of application "Safari"
Run Code Online (Sandbox Code Playgroud)
但是 JXA 无法发送对对象或对象集合的多个属性的单个请求。
| 归档时间: |
|
| 查看次数: |
430 次 |
| 最近记录: |