'callAsyncJavaScript' 可以在 Promise 示例中使用 setTimeout(..., 1000) 吗?

Is use of setTimeout(..., 1000) in Promise example possible with 'callAsyncJavaScript'?

基于WebKit的WKWebView documentation provides a callAsyncJavaScript() method有以下例子:

var p = new Promise(function (f) {
   window.setTimeout("f(42)", 1000);
});
await p;
return p;

运行此示例代码模式的各种尝试return相同的“函数调用的完成处理程序不再可达”错误:

Error Domain=WKErrorDomain Code=4 
"A JavaScript exception occurred" 
UserInfo={
  WKJavaScriptExceptionLineNumber=0, 
  WKJavaScriptExceptionMessage="Completion handler for function call is no longer reachable", 
  WKJavaScriptExceptionColumnNumber=0, 
  NSLocalizedDescription="A JavaScript exception occurred"
}

我已经用 callAsyncJavaScript 尝试对示例代码进行了一些调整。 callAsyncJavaScript 调用总是 return 出现相同的“...不再可达”错误。

这里有一个 运行 在浏览器 JS 控制台中没有错误的变体,它不适用于 callAsyncJavaScript:

为什么会出错?这个例子能以某种方式成功地 运行 和 WKWebView callAsyncJavaScript 吗?如果是,那又如何?


其他详细信息

问题可能是 Promise 在文档示例中没有正确解决。

下面的代码添加了 console.log() 跟踪信息。

let tracer = "a"
console.log("tracer:", tracer);

f = function (nProperty) {
  let nResult = nProperty * 2
  console.log("nResult:", nResult);
  tracer = tracer + "d"
  console.log("tracer:", tracer);
  return nResult;
};

var p = new Promise(function (f) {
  tracer = tracer + "b"
  console.log("tracer:", tracer);
  window.setTimeout("f(42)", 500);
});

console.log("BEFORE await:", p);
tracer = tracer + "c"
console.log("tracer:", tracer);
await p;

console.log("AFTER await:", p);
tracer = tracer + "e"
console.log("tracer:", tracer);

当在 Firefox JS 控制台中 运行 以上时,执行在 await 语句处停止。这是 Firefox JS 控制台输出:

tracer: a
tracer: ab
BEFORE await: Promise { <state>: "pending" }
tracer: abc
nResult: 84
tracer: abcd

语句 console.log("AFTER await:", p); 从未执行过。


还有一个细节...

从 Swift 程序员的角度来看,人们会期望从 Swift callAsyncJavaScript(…) 调用的成功 JavaScript 示例会 明确地 return 一个有用的结果 从 JavaScript 回到 Swift Result<Any, Error>).

请注意,callAsyncJavaScript(…) 作为异步 JavaScript 函数执行提供的 String of JavaScript。

// do some JavaScript actions, await a result, and then..
return somthingUseful;

“函数调用的完成处理程序不再可达” 错误 callAsyncJavaScript() 文档示例中的问题可能是 Promise没有 resolve.

这是一个有效的 callAsyncJavaScript 示例(Swift 5.4、Xcode 15.5、macOS 11.4),它使用 PromisesetTimeoutawait 并解析为 returned 数据对象。

function getPromise() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve({
              'word': 'epistemology'
            });
        }, 2000);
    });
}

function getResult() {
    return getPromise()
        .then(function(response) {
            return response;
        });
}

let data = await getResult()
    .then(function(result) {
        return result;
    });

return data;

更新

阅读 VLAZ 提供的 了解 "f(32)" 破坏 Apple callAsyncJavaScript() 文档中提供的示例的真正原因(写在这个问题的时间)。

此外,Apple 示例在 FireFox JavaScript 控制台和 Swift callAsyncJavaScript(…) 方法,当根据 the answer provide by VLAZ.

修改示例时

从 JavaScript 到 Swift 的 return 值,如在 Swift LLDB 调试器中所见:

(lldb) po result
▿ Result<Any, Error>
  - success : 42

附录:一些有用的提示

Swift callAsyncJavaScript() 方法 将提供的 JavaScript 字符串作为 WebKit 实例中的异步 JavaScript 函数 执行。因此,JavaScript 调试器和 console.log() 不适用于 Swift callAsyncJavaScript() 方法。

下面的方法可以帮助为计划的 JavaScript String.

提供调试可见性

FireFox JavaScript 控制台可用于在 async 函数体中执行字符串:

// --- name browser window/tab ---
document.title = "Example 42";

async function callAsyncJavaScript() {
  // --- begin Swift string literal section ---
  let mocklog = "LOG: ";
  
  var p = new Promise(function (f) {
    mocklog += "A ";  
    window.setTimeout(() => f(42), 1000); 
  });
  
  // p: Promise { <state>: "pending" }
  mocklog += "B ";  
  let pResult = await p;
  
  // p: Promise { <state>: "fulfilled", <value>: 42 }
  mocklog += `C ${pResult}`;
  return {"pResult": pResult, "mocklog": mocklog}
  // --- end Swift string literal section ---
}

let result = await callAsyncJavaScript();
console.log("Result: ", result)
// Result: Object { pResult: 42, mocklog: "LOG: A B C 42" }

同一个JavaScript可以多行放置SwiftString字面量:

let javaScript_42 = """
    let mocklog = "LOG: ";
    
    var p = new Promise(function (f) {
      mocklog += "A ";
      window.setTimeout(() => f(42), 1000);
    });
    
    // p: Promise { <state>: "pending" }
    mocklog += "B ";
    let pResult = await p;
    
    // p: Promise { <state>: "fulfilled", <value>: 42 }
    mocklog += `C ${pResult}`;
    return {"pResult": pResult, "mocklog": mocklog}
    """

Swift 调试器结果:

(lldb) po result
▿ Result<Any, Error>
  ▿ success : 2 elements
    ▿ 0 : 2 elements
      - key : pResult
      - value : 42
    ▿ 1 : 2 elements
      - key : mocklog
      - value : LOG: A B C 42

脚注:添加了 mocklog 字符串作为 console.log() 的替代,因为 console.log 在 Swift callAsyncJavaScript() 中不可用。常规 JavaString mocklog 字符串中的 \n 在相应的 Swift 多行 String 文字中将是 \n

该文档有一个非常大的问题,因为它直接显示的代码不起作用,也不能它起作用。

var p = new Promise(function (f) {
   window.setTimeout("f(42)", 1000);
});

首先,这是非常糟糕的做法。不要将字符串传递给 setTimeout,因为它们将被评估为代码,这很危险并且很容易出错。而是传递一个函数。

当前的问题是,当您传递一个字符串时,它不会在当前范围内评估,而是在全局范围内评估:

const foo = "global";

function test() {
  const foo = "local";
  window.setTimeout("console.log(foo)", 1000);
}

test();

这意味着 window.setTimeout("f(42)", 1000); 不会调用函数参数 f(解析函数)但会尝试使用名为 f 的全局变量并且很可能会失败,因为它不会存在。然后承诺永远悬而未决。

var p = new Promise(function (f) {
   window.setTimeout("f(42)", 1000);
});

p
  .then(value => console.log(`Completed successfully, value: ${value}`)) 
  .catch(error => console.log(`Promise resulted in error`, error));

//neither of the above triggers because of the error in the console

创建稍后将通过 setTimeout 解决的承诺的正确方法是:

var p1 = new Promise(function (f) {
   window.setTimeout(() => f(42), 1000);
});

var p2 = new Promise(function (f) {
   window.setTimeout(f, 1000, 42);
});

var p3 = new Promise(function (f) {
   window.setTimeout(f.bind(null, 42), 1000);
});


p1.then(value => console.log(`p1: Completed successfully, value: ${value}`));
p2.then(value => console.log(`p2: Completed successfully, value: ${value}`));
p3.then(value => console.log(`p3: Completed successfully, value: ${value}`));

参见:How can I pass a parameter to a setTimeout() callback?