【问题标题】:How to detect whether function return value discarded or not?如何检测函数返回值是否被丢弃?
【发布时间】:2021-05-28 02:34:54
【问题描述】:

用例:这允许区分用户是使用基于承诺的样式还是回调样式,因此我可以避免双重计算。我使用 ES6 Proxy 对函数和完成的 setter 进行猴子修补,现在我在两者中都进行了昂贵的计算。我想避免这种情况。无法知道网站使用的是基于 Promise 的版本还是回调版本,因为当网站调用基于 Promise 的版本时,on-completed setter 为空。

相关的原生函数过去没有基于 promise 的版本。

我无法使用静态分析,因为我在修补网站使用的原生函数(其代码不受我控制)

// 2009

// The function is void in 2009
// Today it can return a promise with a value but websites still can use 
// 'nativeFooOnCompleted' to get result

nativeFooStart();

nativeFooOnCompleted = function() {};

// Today
// "nativeFooStart" is still required and without that on-completed 
// event won't fire

let result = await nativeFooStart();

// or 
nativeFooStart();
nativeFooOnCompleted = function() {};

我需要优化运行时。否则,我的现实生活中的函数将在函数内部进行复杂昂贵的计算,无论是否丢弃。 这是 V8 引擎无法处理的事情。我正在修改本机函数(猴子补丁),甚至不是我自己的函数。 在我看来,这是一个简单的问题,因为浏览器 API 允许直接访问脚本的源代码,这样人们就可以遍历代码并确定函数返回值是否被丢弃。

这是突出显示两个函数调用的代码,一个调用者丢弃了返回值,而另一个调用者没有。

function foo() {
  return "bar";
}

foo();  // I need to detect this

let bar = foo();

我需要在运行时检测到这一点。根据我的研究,我发现 Perl 有 wantarray,它不仅会告诉您是否分配了返回值。

其他语言只能在编译时完成。

自从创建问题以来,我已经取得了重大进展。我已经能够提出一种方法并且它是有效的,但是它缺少一个可以考虑作为真正解决方案的东西。

   function foo() {
       // Increase Stacktrace Limit
       Error.stackTraceLimit = Infinity;
        
         // Get the stack trace
       let stackTrace = (new Error()).stack.split("\n"); 
                     
       // Get the Last Item of Trace and Trim it
       let lastLine = stackTrace.pop().trim();
       
       // Get Index of "at "
       let index = lastLine.indexOf("at ");
       
       // Get Normalized Line
       let normalizedLine = lastLine.slice(index + 2, lastLine.length).trim();
       // Regex Pattern to extract line number
       let lineNumberPatternRegex =  new RegExp(/:(\d+):(?:\d+)[^\d]*$/);
       
       // Get Line Number
       let lineNumber = lineNumberPatternRegex.exec(normalizedLine)[1];
       
       // Get the Source Code
       let sourceCode = document.currentScript.text.split("\n");
       
       // Store Caller Line Here
       let callerLine;
       
       // Test whether we have to count HTML lines
       // See, https://stackoverflow.com/q/66388806/14659574
       if(sourceCode.length < lineNumber) {
          // Get HTML Source Code as String
            let HTML = new XMLSerializer().serializeToString(document)
          
          // Get HTML Source Code as Lines
          
          let HTMLSourceLines = HTML.split("\n");

            // This part is stuck because Devtools see diff HTML
          // I still yet to figure how to grab that
          // See, https://stackoverflow.com/q/66390056/14659574
       } else {
          callerLine = sourceCode[lineNumber - 1];
       }
       
       // Detect Variable and Object Assignments 
       // Minified cases not yet handled here
       if(callerLine.includes("=") || callerLine.includes(":")) {
            console.log("Not Discarded")
       } else {
          console.log("Discarded")
       }
       
       return "bar"
    }
      
foo();

用户@poke 在这里回答了这个问题的子问题Link for Sub Problem

据他说,

serializeToString(document) 将序列化当前文档状态, 包括可能在运行时应用的所有更改。在 在这种情况下,页面添加后添加了其他样式 渲染,但也可能有更剧烈的变化 删除或重新排序。

当您从 JavaScript 查看堆栈跟踪时,浏览器的 JavaScript 引擎将尝试为您提供密切相关的信息 与原始源相关,因为那是您的代码的来源 从。如果您使用带有缩小代码的源映射,则浏览器是 通常甚至可以告诉你某件特定的东西是从哪里来的 原始未缩小的代码,即使该代码甚至不紧密 匹配正在执行的代码(例如,使用转译器时)。

最后,你无法真正弄清楚浏览器会告诉你什么 您只需查看文档即可查看代码行的来源 在运行时。如果您的代码遵循非常严格的规则,您可以 通过一些计算来估计这一点,但这不是一种安全的方法。

【问题讨论】:

标签: javascript


【解决方案1】:

Tl;dr 安排一个微任务

关键是使用 await 将函数的其余部分安排为微任务。

请注意,此答案不会尝试以任何方式检测值是否已被丢弃。这仅是对第一段(用例)的回答,不再需要静态代码分析和运行时源代码解析。

目的只是将控制权交给调用例程。

await nonPromiseValueawait Promise.resolve(nonPromiseValue) 相同。它“立即”完成,但仍在等待表达式之后安排代码稍后运行。因此,使用f = async () =&gt; { await 1; 2;} 并调用f(); g() 代码将首先到达await 1 -&gt; sleep f 并将其余部分安排在微任务queue -&gt; call g() -&gt; 上(最终当微任务队列到达它时)resume f() 继续2

它改变的值,或者它是否改变,没有区别。

let onCompleted; // This would be a property of some request object but that's not needed for the demo

function takeHoursToCompute() { console.log('computing'); return 'Done'; }

function takeMinutesToProcess() { console.log('processing'); }

async function f() {
  // We want to see whether the caller sets onComplete. The problem is that it happens only after calling f().
  // However, if we await it (even though it's not a Promise), this will let the calling function finish before continuing here.
  // Note that *at this point* await(onCompleted) would give undefined, but...
  await onCompleted;
  //...but *by now* onCompleted is already what the caller had set.
  
  const result = takeHoursToCompute();
  if(typeof onCompleted === 'function') {
    // non-Promised call
    takeMinutesToProcess();
    onCompleted(result);
  } else
    console.log('skipping processing');
  
  return result; // returns Promise (because async)
}

window.addEventListener('DOMContentLoaded', () => { // calling code
/* Don't call both branches, because I don't clear onComplete anywhere and that would break the purpose. */
if(true) {

// callback style
  f();
  onCompleted = result => document.getElementById('result').textContent = result;

} else {

  // async style
  (async() => {
    document.getElementById('result').textContent = await f();
  })();

}
});
Result: <span id="result"></span>
<br>
See the console too!

致谢:@TheVee 和 @Vlaz

【讨论】:

    猜你喜欢
    • 2021-12-31
    • 2018-05-02
    • 2019-05-04
    • 1970-01-01
    • 2016-09-04
    • 2013-06-21
    • 2010-09-26
    • 1970-01-01
    • 2018-02-16
    相关资源
    最近更新 更多